mirror of
https://github.com/immich-app/immich.git
synced 2026-05-23 16:12:30 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 476e73b0c5 |
@@ -1,16 +0,0 @@
|
|||||||
import { Kysely, sql } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
// Delete unauthorized cross-owner asset faces
|
|
||||||
await sql`
|
|
||||||
DELETE FROM asset_face
|
|
||||||
USING person, asset
|
|
||||||
WHERE asset_face."personId" = person.id
|
|
||||||
AND asset_face."assetId" = asset.id
|
|
||||||
AND person."ownerId" <> asset."ownerId"
|
|
||||||
`.execute(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(): Promise<void> {
|
|
||||||
// Not implemented: the deleted rows were unauthorized cross-owner entries
|
|
||||||
}
|
|
||||||
@@ -454,30 +454,6 @@ describe(PersonService.name, () => {
|
|||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject creating a face on an asset the user does not own', async () => {
|
|
||||||
const auth = AuthFactory.create();
|
|
||||||
const asset = AssetFactory.create();
|
|
||||||
const person = PersonFactory.create({ faceAssetId: null });
|
|
||||||
|
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
|
|
||||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.createFace(auth, {
|
|
||||||
assetId: asset.id,
|
|
||||||
personId: person.id,
|
|
||||||
imageHeight: 500,
|
|
||||||
imageWidth: 400,
|
|
||||||
x: 10,
|
|
||||||
y: 20,
|
|
||||||
width: 100,
|
|
||||||
height: 110,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
|
|
||||||
expect(mocks.person.createAssetFace).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createNewFeaturePhoto', () => {
|
describe('createNewFeaturePhoto', () => {
|
||||||
|
|||||||
@@ -627,7 +627,7 @@ export class PersonService extends BaseService {
|
|||||||
// TODO return a asset face response
|
// TODO return a asset face response
|
||||||
async createFace(auth: AuthDto, dto: AssetFaceCreateDto): Promise<void> {
|
async createFace(auth: AuthDto, dto: AssetFaceCreateDto): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [dto.assetId] }),
|
this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.assetId] }),
|
||||||
this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }),
|
this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
+145
-1
@@ -1,17 +1,35 @@
|
|||||||
import { defaultProvider, screencastManager, themeManager, ThemePreference, type ActionItem } from '@immich/ui';
|
import { defaultProvider, screencastManager, themeManager, ThemePreference, type ActionItem } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAccountMultipleOutline,
|
mdiAccountMultipleOutline,
|
||||||
|
mdiAccountOutline,
|
||||||
|
mdiArchiveArrowDownOutline,
|
||||||
mdiBookshelf,
|
mdiBookshelf,
|
||||||
mdiCog,
|
mdiCog,
|
||||||
|
mdiContentDuplicate,
|
||||||
|
mdiCrosshairsGps,
|
||||||
|
mdiFolderOutline,
|
||||||
|
mdiHeartOutline,
|
||||||
|
mdiImageAlbum,
|
||||||
|
mdiImageMultipleOutline,
|
||||||
|
mdiImageSizeSelectLarge,
|
||||||
mdiKeyboard,
|
mdiKeyboard,
|
||||||
|
mdiLink,
|
||||||
|
mdiLockOutline,
|
||||||
|
mdiMagnify,
|
||||||
|
mdiMapOutline,
|
||||||
mdiServer,
|
mdiServer,
|
||||||
|
mdiStateMachine,
|
||||||
mdiSync,
|
mdiSync,
|
||||||
|
mdiTagMultipleOutline,
|
||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
|
mdiToolboxOutline,
|
||||||
|
mdiTrashCanOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
@@ -49,7 +67,133 @@ export const getPagesProvider = ($t: MessageFormatter) => {
|
|||||||
},
|
},
|
||||||
].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
|
].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
|
||||||
|
|
||||||
return defaultProvider({ name: $t('page'), actions: adminPages });
|
const userPages: ActionItem[] = [
|
||||||
|
{
|
||||||
|
title: $t('photos'),
|
||||||
|
icon: mdiImageMultipleOutline,
|
||||||
|
onAction: () => goto(Route.photos()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('explore'),
|
||||||
|
icon: mdiMagnify,
|
||||||
|
onAction: () => goto(Route.explore()),
|
||||||
|
$if: () => authManager.authenticated && featureFlagsManager.value.search,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: $t('map'),
|
||||||
|
icon: mdiMapOutline,
|
||||||
|
onAction: () => goto(Route.map()),
|
||||||
|
$if: () => authManager.authenticated && featureFlagsManager.value.map,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('people'),
|
||||||
|
description: $t('people_feature_description'),
|
||||||
|
icon: mdiAccountOutline,
|
||||||
|
onAction: () => goto(Route.people()),
|
||||||
|
$if: () => authManager.authenticated && authManager.preferences.people.enabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('shared_links'),
|
||||||
|
icon: mdiLink,
|
||||||
|
onAction: () => goto(Route.sharedLinks()),
|
||||||
|
$if: () => authManager.authenticated && authManager.preferences.sharedLinks.enabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('recently_added'),
|
||||||
|
icon: mdiMagnify,
|
||||||
|
onAction: () => goto(Route.recentlyAdded()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('sharing'),
|
||||||
|
icon: mdiAccountMultipleOutline,
|
||||||
|
onAction: () => goto(Route.sharing()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('favorites'),
|
||||||
|
icon: mdiHeartOutline,
|
||||||
|
onAction: () => goto(Route.favorites()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('albums'),
|
||||||
|
description: $t('albums_feature_description'),
|
||||||
|
icon: mdiImageAlbum,
|
||||||
|
onAction: () => goto(Route.albums()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('tags'),
|
||||||
|
description: $t('tag_feature_description'),
|
||||||
|
icon: mdiTagMultipleOutline,
|
||||||
|
onAction: () => goto(Route.tags()),
|
||||||
|
$if: () => authManager.authenticated && authManager.preferences.tags.enabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('folders'),
|
||||||
|
description: $t('folders_feature_description'),
|
||||||
|
icon: mdiFolderOutline,
|
||||||
|
onAction: () => goto(Route.folders()),
|
||||||
|
$if: () => authManager.authenticated && authManager.preferences.folders.enabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('utilities'),
|
||||||
|
icon: mdiToolboxOutline,
|
||||||
|
onAction: () => goto(Route.utilities()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('archive'),
|
||||||
|
icon: mdiArchiveArrowDownOutline,
|
||||||
|
onAction: () => goto(Route.archive()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('locked_folder'),
|
||||||
|
icon: mdiLockOutline,
|
||||||
|
onAction: () => goto(Route.locked()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('trash'),
|
||||||
|
icon: mdiTrashCanOutline,
|
||||||
|
onAction: () => goto(Route.trash()),
|
||||||
|
$if: () => authManager.authenticated && featureFlagsManager.value.trash,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('admin.user_settings'),
|
||||||
|
icon: mdiCog,
|
||||||
|
onAction: () => goto(Route.userSettings()),
|
||||||
|
$if: () => authManager.authenticated,
|
||||||
|
},
|
||||||
|
].map((route) => ({ $if: () => authManager.authenticated, ...route }));
|
||||||
|
|
||||||
|
const utilityPages: ActionItem[] = [
|
||||||
|
{
|
||||||
|
title: $t('review_duplicates'),
|
||||||
|
icon: mdiContentDuplicate,
|
||||||
|
onAction: () => goto(Route.duplicatesUtility()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('review_large_files'),
|
||||||
|
icon: mdiImageSizeSelectLarge,
|
||||||
|
onAction: () => goto(Route.largeFileUtility()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('manage_geolocation'),
|
||||||
|
icon: mdiCrosshairsGps,
|
||||||
|
onAction: () => goto(Route.geolocationUtility()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('workflows'),
|
||||||
|
icon: mdiStateMachine,
|
||||||
|
onAction: () => goto(Route.workflows()),
|
||||||
|
},
|
||||||
|
].map((route) => ({ ...route, $if: () => authManager.authenticated }));
|
||||||
|
|
||||||
|
return defaultProvider({ name: $t('page'), actions: [...userPages, ...utilityPages, ...adminPages] });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMyImmichLink = () => {
|
const getMyImmichLink = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user