diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts index af527bcc0..c4cbae19c 100644 --- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -206,6 +206,13 @@ describe('/shared-links', () => { ); }); + it('should not return album users', async () => { + const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key }); + + expect(status).toBe(200); + expect(body.album.albumUsers).toEqual([]); + }); + it('should return unauthorized for incorrect shared link', async () => { const { status, body } = await request(app) .get('/shared-links/me') diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index f8efd7626..3471d9465 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -6,6 +6,7 @@ import { AlbumUser, AuthSharedLink } from 'src/database.js'; import { HistoryBuilder } from 'src/decorators.js'; import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto.js'; import { MapAsset } from 'src/dtos/asset-response.dto.js'; +import { AuthDto } from 'src/dtos/auth.dto.js'; import { UserResponseSchema, mapUser } from 'src/dtos/user.dto.js'; import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum.js'; import { asDateTimeString } from 'src/utils/date.js'; @@ -229,7 +230,7 @@ export type MapAlbumDto = { order: AssetOrder; }; -export const mapAlbum = (entity: MaybeDehydrated): AlbumResponseDto => { +export const mapAlbum = (entity: MaybeDehydrated, { auth }: { auth?: AuthDto } = {}): AlbumResponseDto => { const albumUsers: AlbumUserResponseDto[] = []; if (entity.albumUsers) { @@ -262,7 +263,7 @@ export const mapAlbum = (entity: MaybeDehydrated): AlbumResponseDto createdAt: asDateTimeString(entity.createdAt), updatedAt: asDateTimeString(entity.updatedAt), id: entity.id, - albumUsers, + albumUsers: auth?.sharedLink ? [] : albumUsers, shared: hasSharedUser || hasSharedLink, hasSharedLink, startDate: asDateTimeString(startDate), diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index f5239bd09..c9825f481 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -114,7 +114,7 @@ export function mapSharedLink(sharedLink: SharedLink, { auth }: { auth: AuthDto createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, assets: assets.map((asset) => mapAsset(asset, { auth })), - album: sharedLink.album ? mapAlbum(sharedLink.album) : undefined, + album: sharedLink.album ? mapAlbum(sharedLink.album, { auth }) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, showMetadata: sharedLink.showExif, diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index f630760da..92ca72f01 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -713,10 +713,12 @@ describe(AlbumService.name, () => { ]); const auth = AuthFactory.from().sharedLink().build(); - await sut.get(auth, album.id); + const result = await sut.get(auth, album.id); expect(mocks.album.getById).toHaveBeenCalledWith(album.id, auth.user.id); expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink!.id, new Set([album.id])); + expect(result).toMatchObject({ albumUsers: [], shared: true, contributorCounts: undefined }); + expect(mocks.album.getContributorCounts).not.toHaveBeenCalled(); }); it('should get a shared album via shared with user', async () => { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index c18ff5f8e..ccbe71baf 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -83,8 +83,9 @@ export class AlbumService extends BaseService { const isShared = hasSharedUsers || hasSharedLink; return { - ...(await this.mapWithMetadata(album)), - contributorCounts: isShared ? await this.albumRepository.getContributorCounts(album.id) : undefined, + ...(await this.mapWithMetadata(album, auth)), + contributorCounts: + isShared && !auth.sharedLink ? await this.albumRepository.getContributorCounts(album.id) : undefined, }; } @@ -356,8 +357,8 @@ export class AlbumService extends BaseService { return findOrFail(() => this.albumRepository.getById(id, authUserId), 'Album'); } - private async mapWithMetadata(album: MapAlbumDto): Promise { + private async mapWithMetadata(album: MapAlbumDto, auth?: AuthDto): Promise { const [metadata] = await this.albumRepository.getMetadataForIds([album.id]); - return { ...mapAlbum(album), ...mapAlbumMetadata(metadata) }; + return { ...mapAlbum(album, { auth }), ...mapAlbumMetadata(metadata) }; } } diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index d773d7b9c..d36540d3b 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -73,6 +73,20 @@ describe(SharedLinkService.name, () => { expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); + it('should not return album users', async () => { + const authDto = authStub.adminSharedLink; + const sharedLink = SharedLinkFactory.from() + .album({}, (builder) => builder.albumUser()) + .build(); + mocks.sharedLink.get.mockResolvedValue(getForSharedLink(sharedLink)); + mocks.album.getMetadataForIds.mockResolvedValue([]); + + const response = await sut.getMine(authDto, []); + + expect(sharedLink.album?.albumUsers.length).toBeGreaterThan(0); + expect(response.album).toMatchObject({ id: sharedLink.album!.id, albumUsers: [], shared: true }); + }); + it('should throw an error for a request without a shared link auth token', async () => { const authDto = authStub.adminSharedLink; mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.passwordRequired);