diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index 5da6765d20baa..3918429e4eda4 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -150,6 +150,30 @@ describe('/shared-links', () => { ); }); + it('should filter on albumId', async () => { + const { status, body } = await request(app) + .get(`/shared-links?albumId=${album.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toHaveLength(2); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: linkWithAlbum.id }), + expect.objectContaining({ id: linkWithPassword.id }), + ]), + ); + }); + + it('should find 0 albums', async () => { + const { status, body } = await request(app) + .get(`/shared-links?albumId=${uuidDto.notFound}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toHaveLength(0); + }); + it('should not get shared links created by other users', async () => { const { status, body } = await request(app) .get('/shared-links') diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 12e0224999375..a6b2978fe2adb 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -127,7 +127,10 @@ class SharedLinksApi { } /// Performs an HTTP 'GET /shared-links' operation and returns the [Response]. - Future getAllSharedLinksWithHttpInfo() async { + /// Parameters: + /// + /// * [String] albumId: + Future getAllSharedLinksWithHttpInfo({ String? albumId, }) async { // ignore: prefer_const_declarations final path = r'/shared-links'; @@ -138,6 +141,10 @@ class SharedLinksApi { final headerParams = {}; final formParams = {}; + if (albumId != null) { + queryParams.addAll(_queryParams('', 'albumId', albumId)); + } + const contentTypes = []; @@ -152,8 +159,11 @@ class SharedLinksApi { ); } - Future?> getAllSharedLinks() async { - final response = await getAllSharedLinksWithHttpInfo(); + /// Parameters: + /// + /// * [String] albumId: + Future?> getAllSharedLinks({ String? albumId, }) async { + final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f1ef466df4c9e..f1613e9026eb8 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -5230,7 +5230,17 @@ "/shared-links": { "get": { "operationId": "getAllSharedLinks", - "parameters": [], + "parameters": [ + { + "name": "albumId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], "responses": { "200": { "content": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 6704a83cc7d6e..e210e73a5eca3 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2745,11 +2745,15 @@ export function deleteSession({ id }: { method: "DELETE" })); } -export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) { +export function getAllSharedLinks({ albumId }: { + albumId?: string; +}, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: SharedLinkResponseDto[]; - }>("/shared-links", { + }>(`/shared-links${QS.query(QS.explode({ + albumId + }))}`, { ...opts })); } diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index 59f81068d8dac..ca978f03da0e6 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -9,6 +9,7 @@ import { SharedLinkEditDto, SharedLinkPasswordDto, SharedLinkResponseDto, + SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; import { ImmichCookie, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; @@ -24,8 +25,8 @@ export class SharedLinkController { @Get() @Authenticated({ permission: Permission.SHARED_LINK_READ }) - getAllSharedLinks(@Auth() auth: AuthDto): Promise { - return this.service.getAll(auth); + getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise { + return this.service.getAll(auth, dto); } @Get('me') diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index b97791db58eb8..e3f8c72e1988a 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -7,6 +7,11 @@ import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +export class SharedLinkSearchDto { + @ValidateUUID({ optional: true }) + albumId?: string; +} + export class SharedLinkCreateDto { @IsEnum(SharedLinkType) @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) diff --git a/server/src/interfaces/shared-link.interface.ts b/server/src/interfaces/shared-link.interface.ts index 25b7237f00558..c030ceb736aec 100644 --- a/server/src/interfaces/shared-link.interface.ts +++ b/server/src/interfaces/shared-link.interface.ts @@ -4,8 +4,13 @@ import { SharedLinkEntity } from 'src/entities/shared-link.entity'; export const ISharedLinkRepository = 'ISharedLinkRepository'; +export type SharedLinkSearchOptions = { + userId: string; + albumId?: string; +}; + export interface ISharedLinkRepository { - getAll(userId: string): Promise; + getAll(options: SharedLinkSearchOptions): Promise; get(userId: string, id: string): Promise; getByKey(key: Buffer): Promise; create(entity: Insertable & { assetIds?: string[] }): Promise; diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index 3ffae4f0676ce..8e2e6976a5fd5 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -7,7 +7,7 @@ import { DB, SharedLinks } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkType } from 'src/enum'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { ISharedLinkRepository, SharedLinkSearchOptions } from 'src/interfaces/shared-link.interface'; @Injectable() export class SharedLinkRepository implements ISharedLinkRepository { @@ -93,7 +93,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - getAll(userId: string): Promise { + getAll({ userId, albumId }: SharedLinkSearchOptions): Promise { return this.db .selectFrom('shared_links') .selectAll('shared_links') @@ -149,6 +149,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { ) .select((eb) => eb.fn.toJson('album').as('album')) .where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) + .$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!)) .orderBy('shared_links.createdAt', 'desc') .distinctOn(['shared_links.createdAt']) .execute() as unknown as Promise; diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 2d673eb7ca945..0e29012876962 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -29,11 +29,11 @@ describe(SharedLinkService.name, () => { describe('getAll', () => { it('should return all shared links for a user', async () => { sharedLinkMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]); - await expect(sut.getAll(authStub.user1)).resolves.toEqual([ + await expect(sut.getAll(authStub.user1, {})).resolves.toEqual([ sharedLinkResponseStub.expired, sharedLinkResponseStub.valid, ]); - expect(sharedLinkMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); + expect(sharedLinkMock.getAll).toHaveBeenCalledWith({ userId: authStub.user1.user.id }); }); }); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index a015bbe3f31fc..74595bb9a2e5f 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -9,6 +9,7 @@ import { SharedLinkEditDto, SharedLinkPasswordDto, SharedLinkResponseDto, + SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { Permission, SharedLinkType } from 'src/enum'; @@ -17,8 +18,10 @@ import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService extends BaseService { - async getAll(auth: AuthDto): Promise { - return this.sharedLinkRepository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link))); + async getAll(auth: AuthDto, { albumId }: SharedLinkSearchDto): Promise { + return this.sharedLinkRepository + .getAll({ userId: auth.user.id, albumId }) + .then((links) => links.map((link) => mapSharedLink(link))); } async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise { diff --git a/web/src/lib/components/album-page/album-shared-link.svelte b/web/src/lib/components/album-page/album-shared-link.svelte new file mode 100644 index 0000000000000..55c08c4d12c88 --- /dev/null +++ b/web/src/lib/components/album-page/album-shared-link.svelte @@ -0,0 +1,40 @@ + + +
+
+ {sharedLink.description || album.albumName} + {[ + DateTime.fromISO(sharedLink.createdAt).toLocaleString( + { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + { locale: $locale }, + ), + sharedLink.allowUpload && $t('upload'), + sharedLink.allowDownload && $t('download'), + sharedLink.showMetadata && $t('exif'), + sharedLink.password && $t('password'), + ] + .filter(Boolean) + .join(' • ')} +
+ +
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 85155866f9df6..96e3a1672eea3 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -1,4 +1,5 @@ - + {#if Object.keys(selectedUsers).length > 0}
-

{$t('selected').toUpperCase()}

+

{$t('selected')}

{#each Object.values(selectedUsers) as { user }} {#key user.id} @@ -117,7 +113,7 @@
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} -

{$t('suggestions').toUpperCase()}

+ {$t('users')}
{#each users as user} @@ -144,9 +140,9 @@ {#if users.length > 0}
{/if} -
- -
- - - {#if sharedLinks.length} - - -

{$t('view_links')}

-
- {/if} -
+
+ + +
+ {$t('shared_links')} + {$t('view_all')} +
+ + + {#each sharedLinks as sharedLink} + + {/each} + + + +
diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte index b7d4da2941812..7b75e0e93662c 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte @@ -19,7 +19,7 @@ let editSharedLink: SharedLinkResponseDto | null = $state(null); const refresh = async () => { - sharedLinks = await getAllSharedLinks(); + sharedLinks = await getAllSharedLinks({}); }; onMount(async () => {