diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index f70d3d548c..fc170386d1 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -1,5 +1,6 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { AuthUserDto } from '../auth'; +import { SharedLinkEntity } from '../../infra/entities'; +import { AuthDto } from '../auth'; import { setDifference, setIsEqual, setUnion } from '../domain.util'; import { IAccessRepository } from '../repositories'; @@ -64,20 +65,20 @@ export class AccessCore { instance = null; } - requireUploadAccess(authUser: AuthUserDto | null): AuthUserDto { - if (!authUser || (authUser.isPublicUser && !authUser.isAllowUpload)) { + requireUploadAccess(auth: AuthDto | null): AuthDto { + if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) { throw new UnauthorizedException(); } - return authUser; + return auth; } /** * Check if user has access to all ids, for the given permission. * Throws error if user does not have access to any of the ids. */ - async requirePermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) { + async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) { ids = Array.isArray(ids) ? ids : [ids]; - const allowedIds = await this.checkAccess(authUser, permission, ids); + const allowedIds = await this.checkAccess(auth, permission, ids); if (!setIsEqual(new Set(ids), allowedIds)) { throw new BadRequestException(`Not found or no ${permission} access`); } @@ -89,23 +90,21 @@ export class AccessCore { * * @returns Set */ - async checkAccess(authUser: AuthUserDto, permission: Permission, ids: Set | string[]) { + async checkAccess(auth: AuthDto, permission: Permission, ids: Set | string[]) { const idSet = Array.isArray(ids) ? new Set(ids) : ids; if (idSet.size === 0) { return new Set(); } - const isSharedLink = authUser.isPublicUser ?? false; - return isSharedLink - ? await this.checkAccessSharedLink(authUser, permission, idSet) - : await this.checkAccessOther(authUser, permission, idSet); + if (auth.sharedLink) { + return this.checkAccessSharedLink(auth.sharedLink, permission, idSet); + } + + return this.checkAccessOther(auth, permission, idSet); } - private async checkAccessSharedLink(authUser: AuthUserDto, permission: Permission, ids: Set) { - const sharedLinkId = authUser.sharedLinkId; - if (!sharedLinkId) { - return new Set(); - } + private async checkAccessSharedLink(sharedLink: SharedLinkEntity, permission: Permission, ids: Set) { + const sharedLinkId = sharedLink.id; switch (permission) { case Permission.ASSET_READ: @@ -115,22 +114,22 @@ export class AccessCore { return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); case Permission.ASSET_DOWNLOAD: - return !!authUser.isAllowDownload + return !!sharedLink.allowDownload ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); case Permission.ASSET_UPLOAD: - return authUser.isAllowUpload ? ids : new Set(); + return sharedLink.allowUpload ? ids : new Set(); case Permission.ASSET_SHARE: - // TODO: fix this to not use authUser.id for shared link access control - return await this.repository.asset.checkOwnerAccess(authUser.id, ids); + // TODO: fix this to not use sharedLink.userId for access control + return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids); case Permission.ALBUM_READ: return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); case Permission.ALBUM_DOWNLOAD: - return !!authUser.isAllowDownload + return !!sharedLink.allowDownload ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); @@ -139,129 +138,129 @@ export class AccessCore { } } - private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set) { + private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set) { switch (permission) { case Permission.ASSET_READ: { - const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await this.repository.asset.checkPartnerAccess( - authUser.id, + auth.user.id, setDifference(ids, isOwner, isAlbum), ); return setUnion(isOwner, isAlbum, isPartner); } case Permission.ASSET_SHARE: { - const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); - const isPartner = await this.repository.asset.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); + const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isPartner); } case Permission.ASSET_VIEW: { - const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await this.repository.asset.checkPartnerAccess( - authUser.id, + auth.user.id, setDifference(ids, isOwner, isAlbum), ); return setUnion(isOwner, isAlbum, isPartner); } case Permission.ASSET_DOWNLOAD: { - const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); const isPartner = await this.repository.asset.checkPartnerAccess( - authUser.id, + auth.user.id, setDifference(ids, isOwner, isAlbum), ); return setUnion(isOwner, isAlbum, isPartner); } case Permission.ASSET_UPDATE: - return await this.repository.asset.checkOwnerAccess(authUser.id, ids); + return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); case Permission.ASSET_DELETE: - return await this.repository.asset.checkOwnerAccess(authUser.id, ids); + return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); case Permission.ASSET_RESTORE: - return await this.repository.asset.checkOwnerAccess(authUser.id, ids); + return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); case Permission.ALBUM_READ: { - const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isShared); } case Permission.ALBUM_UPDATE: - return await this.repository.album.checkOwnerAccess(authUser.id, ids); + return await this.repository.album.checkOwnerAccess(auth.user.id, ids); case Permission.ALBUM_DELETE: - return await this.repository.album.checkOwnerAccess(authUser.id, ids); + return await this.repository.album.checkOwnerAccess(auth.user.id, ids); case Permission.ALBUM_SHARE: - return await this.repository.album.checkOwnerAccess(authUser.id, ids); + return await this.repository.album.checkOwnerAccess(auth.user.id, ids); case Permission.ALBUM_DOWNLOAD: { - const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isShared); } case Permission.ALBUM_REMOVE_ASSET: - return await this.repository.album.checkOwnerAccess(authUser.id, ids); + return await this.repository.album.checkOwnerAccess(auth.user.id, ids); case Permission.ASSET_UPLOAD: - return await this.repository.library.checkOwnerAccess(authUser.id, ids); + return await this.repository.library.checkOwnerAccess(auth.user.id, ids); case Permission.ARCHIVE_READ: - return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); case Permission.AUTH_DEVICE_DELETE: - return await this.repository.authDevice.checkOwnerAccess(authUser.id, ids); + return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids); case Permission.TIMELINE_READ: { - const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); - const isPartner = await this.repository.timeline.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isPartner); } case Permission.TIMELINE_DOWNLOAD: - return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); case Permission.LIBRARY_READ: { - const isOwner = await this.repository.library.checkOwnerAccess(authUser.id, ids); - const isPartner = await this.repository.library.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids); + const isPartner = await this.repository.library.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isPartner); } case Permission.LIBRARY_UPDATE: - return await this.repository.library.checkOwnerAccess(authUser.id, ids); + return await this.repository.library.checkOwnerAccess(auth.user.id, ids); case Permission.LIBRARY_DELETE: - return await this.repository.library.checkOwnerAccess(authUser.id, ids); + return await this.repository.library.checkOwnerAccess(auth.user.id, ids); case Permission.PERSON_READ: - return await this.repository.person.checkOwnerAccess(authUser.id, ids); + return await this.repository.person.checkOwnerAccess(auth.user.id, ids); case Permission.PERSON_WRITE: - return await this.repository.person.checkOwnerAccess(authUser.id, ids); + return await this.repository.person.checkOwnerAccess(auth.user.id, ids); case Permission.PERSON_MERGE: - return await this.repository.person.checkOwnerAccess(authUser.id, ids); + return await this.repository.person.checkOwnerAccess(auth.user.id, ids); case Permission.PERSON_CREATE: - return this.repository.person.hasFaceOwnerAccess(authUser.id, ids); + return this.repository.person.hasFaceOwnerAccess(auth.user.id, ids); case Permission.PERSON_REASSIGN: - return this.repository.person.hasFaceOwnerAccess(authUser.id, ids); + return this.repository.person.hasFaceOwnerAccess(auth.user.id, ids); case Permission.PARTNER_UPDATE: - return await this.repository.partner.checkUpdateAccess(authUser.id, ids); + return await this.repository.partner.checkUpdateAccess(auth.user.id, ids); } const allowedIds = new Set(); for (const id of ids) { - const hasAccess = await this.hasOtherAccess(authUser, permission, id); + const hasAccess = await this.hasOtherAccess(auth, permission, id); if (hasAccess) { allowedIds.add(id); } @@ -270,17 +269,17 @@ export class AccessCore { } // TODO: Migrate logic to checkAccessOther to evaluate permissions in bulk. - private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) { + private async hasOtherAccess(auth: AuthDto, permission: Permission, id: string) { switch (permission) { // uses album id case Permission.ACTIVITY_CREATE: - return await this.repository.activity.hasCreateAccess(authUser.id, id); + return await this.repository.activity.hasCreateAccess(auth.user.id, id); // uses activity id case Permission.ACTIVITY_DELETE: return ( - (await this.repository.activity.hasOwnerAccess(authUser.id, id)) || - (await this.repository.activity.hasAlbumOwnerAccess(authUser.id, id)) + (await this.repository.activity.hasOwnerAccess(auth.user.id, id)) || + (await this.repository.activity.hasAlbumOwnerAccess(auth.user.id, id)) ); default: diff --git a/server/src/domain/activity/activity.service.ts b/server/src/domain/activity/activity.service.ts index 8b601558df..15482acaeb 100644 --- a/server/src/domain/activity/activity.service.ts +++ b/server/src/domain/activity/activity.service.ts @@ -1,7 +1,7 @@ import { ActivityEntity } from '@app/infra/entities'; import { Inject, Injectable } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { IAccessRepository, IActivityRepository } from '../repositories'; import { ActivityCreateDto, @@ -26,8 +26,8 @@ export class ActivityService { this.access = AccessCore.create(accessRepository); } - async getAll(authUser: AuthUserDto, dto: ActivitySearchDto): Promise { - await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId); + async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise { + await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); const activities = await this.repository.search({ userId: dto.userId, albumId: dto.albumId, @@ -38,16 +38,16 @@ export class ActivityService { return activities.map(mapActivity); } - async getStatistics(authUser: AuthUserDto, dto: ActivityDto): Promise { - await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId); + async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { + await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; } - async create(authUser: AuthUserDto, dto: ActivityCreateDto): Promise> { - await this.access.requirePermission(authUser, Permission.ACTIVITY_CREATE, dto.albumId); + async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { + await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId); const common = { - userId: authUser.id, + userId: auth.user.id, assetId: dto.assetId, albumId: dto.albumId, }; @@ -77,8 +77,8 @@ export class ActivityService { return { duplicate, value: mapActivity(activity) }; } - async delete(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.ACTIVITY_DELETE, id); + async delete(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id); await this.repository.delete(id); } } diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index 7fc6344ea6..fa421342c8 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -48,9 +48,9 @@ describe(AlbumService.name, () => { notShared: 0, }); - expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.id); - expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.id); - expect(albumMock.getNotShared).toHaveBeenCalledWith(authStub.admin.id); + expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.user.id); + expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.user.id); + expect(albumMock.getNotShared).toHaveBeenCalledWith(authStub.admin.user.id); }); }); @@ -188,7 +188,7 @@ describe(AlbumService.name, () => { }); expect(albumMock.create).toHaveBeenCalledWith({ - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, albumName: albumStub.empty.albumName, description: albumStub.empty.description, sharedUsers: [{ id: 'user-id' }], @@ -312,7 +312,7 @@ describe(AlbumService.name, () => { accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); await expect( - sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }), + sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.user.id] }), ).rejects.toBeInstanceOf(BadRequestException); expect(albumMock.update).not.toHaveBeenCalled(); }); @@ -332,11 +332,11 @@ describe(AlbumService.name, () => { albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin)); albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); userMock.get.mockResolvedValue(userStub.user2); - await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.id] }); + await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.user.id] }); expect(albumMock.update).toHaveBeenCalledWith({ id: albumStub.sharedWithAdmin.id, updatedAt: expect.any(Date), - sharedUsers: [userStub.admin, { id: authStub.user2.id }], + sharedUsers: [userStub.admin, { id: authStub.user2.user.id }], }); }); }); @@ -370,12 +370,12 @@ describe(AlbumService.name, () => { albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple); await expect( - sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.id), + sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.user.id), ).rejects.toBeInstanceOf(BadRequestException); expect(albumMock.update).not.toHaveBeenCalled(); expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.user1.id, + authStub.user1.user.id, new Set([albumStub.sharedWithMultiple.id]), ); }); @@ -383,7 +383,7 @@ describe(AlbumService.name, () => { it('should allow a shared user to remove themselves', async () => { albumMock.getById.mockResolvedValue(albumStub.sharedWithUser); - await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.id); + await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id); expect(albumMock.update).toHaveBeenCalledTimes(1); expect(albumMock.update).toHaveBeenCalledWith({ @@ -409,7 +409,7 @@ describe(AlbumService.name, () => { it('should not allow the owner to be removed', async () => { albumMock.getById.mockResolvedValue(albumStub.empty); - await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.id)).rejects.toBeInstanceOf( + await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.user.id)).rejects.toBeInstanceOf( BadRequestException, ); @@ -444,7 +444,7 @@ describe(AlbumService.name, () => { expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true }); expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.id, + authStub.admin.user.id, new Set([albumStub.oneAsset.id]), ); }); @@ -465,7 +465,7 @@ describe(AlbumService.name, () => { expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( - authStub.adminSharedLink.sharedLinkId, + authStub.adminSharedLink.sharedLink?.id, new Set(['album-123']), ); }); @@ -485,14 +485,20 @@ describe(AlbumService.name, () => { await sut.get(authStub.user1, 'album-123', {}); expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); - expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['album-123'])); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith( + authStub.user1.user.id, + new Set(['album-123']), + ); }); it('should throw an error for no access', async () => { await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123'])); - expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123'])); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-123'])); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith( + authStub.admin.user.id, + new Set(['album-123']), + ); }); }); @@ -590,7 +596,7 @@ describe(AlbumService.name, () => { }); expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( - authStub.adminSharedLink.sharedLinkId, + authStub.adminSharedLink.sharedLink?.id, new Set(['album-123']), ); }); @@ -610,7 +616,7 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', }); - expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); + expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); }); it('should skip duplicate assets', async () => { @@ -635,8 +641,8 @@ describe(AlbumService.name, () => { { success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION }, ]); - expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); - expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); + expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); }); it('should not allow unauthorized access to the album', async () => { @@ -729,7 +735,7 @@ describe(AlbumService.name, () => { // // await expect( // // sut.removeAssetsFromAlbum( - // // authUser, + // // auth, // // { // // ids: ['1'], // // }, @@ -755,6 +761,6 @@ describe(AlbumService.name, () => { // albumRepositoryMock.get.mockImplementation(() => Promise.resolve(albumEntity)); // albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve(albumResponse)); - // await expect(sut.removeAssets(authUser, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException); + // await expect(sut.removeAssets(auth, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException); // }); }); diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 74f5da7fc2..941fcf4c5c 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -2,7 +2,7 @@ import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { setUnion } from '../domain.util'; import { AlbumAssetCount, @@ -35,11 +35,11 @@ export class AlbumService { this.access = AccessCore.create(accessRepository); } - async getCount(authUser: AuthUserDto): Promise { + async getCount(auth: AuthDto): Promise { const [owned, shared, notShared] = await Promise.all([ - this.albumRepository.getOwned(authUser.id), - this.albumRepository.getShared(authUser.id), - this.albumRepository.getNotShared(authUser.id), + this.albumRepository.getOwned(auth.user.id), + this.albumRepository.getShared(auth.user.id), + this.albumRepository.getNotShared(auth.user.id), ]); return { @@ -49,7 +49,7 @@ export class AlbumService { }; } - async getAll({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise { + async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise { const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail(); for (const albumId of invalidAlbumIds) { const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId); @@ -98,8 +98,8 @@ export class AlbumService { ); } - async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise { - await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); + async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { + await this.access.requirePermission(auth, Permission.ALBUM_READ, id); await this.albumRepository.updateThumbnails(); const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const album = await this.findOrFail(id, { withAssets }); @@ -113,7 +113,7 @@ export class AlbumService { }; } - async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise { + async create(auth: AuthDto, dto: CreateAlbumDto): Promise { for (const userId of dto.sharedWithUserIds || []) { const exists = await this.userRepository.get(userId, {}); if (!exists) { @@ -122,7 +122,7 @@ export class AlbumService { } const album = await this.albumRepository.create({ - ownerId: authUser.id, + ownerId: auth.user.id, albumName: dto.albumName, description: dto.description, sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value }) as UserEntity) ?? [], @@ -133,8 +133,8 @@ export class AlbumService { return mapAlbumWithAssets(album); } - async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise { - await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id); + async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { + await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id); const album = await this.findOrFail(id, { withAssets: true }); @@ -155,22 +155,22 @@ export class AlbumService { return mapAlbumWithoutAssets(updatedAlbum); } - async delete(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id); + async delete(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id); const album = await this.findOrFail(id, { withAssets: false }); await this.albumRepository.delete(album); } - async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise { + async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); + await this.access.requirePermission(auth, Permission.ALBUM_READ, id); const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id)); - const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); const results: BulkIdResponseDto[] = []; for (const assetId of dto.ids) { @@ -202,14 +202,14 @@ export class AlbumService { return results; } - async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise { + async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); + await this.access.requirePermission(auth, Permission.ALBUM_READ, id); const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); - const canRemove = await this.access.checkAccess(authUser, Permission.ALBUM_REMOVE_ASSET, existingAssetIds); - const canShare = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, existingAssetIds); + const canRemove = await this.access.checkAccess(auth, Permission.ALBUM_REMOVE_ASSET, existingAssetIds); + const canShare = await this.access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds); const allowedAssetIds = setUnion(canRemove, canShare); const results: BulkIdResponseDto[] = []; @@ -241,8 +241,8 @@ export class AlbumService { return results; } - async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto): Promise { - await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id); + async addUsers(auth: AuthDto, id: string, dto: AddUsersDto): Promise { + await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); const album = await this.findOrFail(id, { withAssets: false }); @@ -273,9 +273,9 @@ export class AlbumService { .then(mapAlbumWithoutAssets); } - async removeUser(authUser: AuthUserDto, id: string, userId: string | 'me'): Promise { + async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise { if (userId === 'me') { - userId = authUser.id; + userId = auth.user.id; } const album = await this.findOrFail(id, { withAssets: false }); @@ -290,8 +290,8 @@ export class AlbumService { } // non-admin can remove themselves - if (authUser.id !== userId) { - await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id); + if (auth.user.id !== userId) { + await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); } await this.albumRepository.update({ diff --git a/server/src/domain/api-key/api-key.service.spec.ts b/server/src/domain/api-key/api-key.service.spec.ts index adeb5dc010..f6d650c412 100644 --- a/server/src/domain/api-key/api-key.service.spec.ts +++ b/server/src/domain/api-key/api-key.service.spec.ts @@ -21,7 +21,7 @@ describe(APIKeyService.name, () => { expect(keyMock.create).toHaveBeenCalledWith({ key: 'cmFuZG9tLWJ5dGVz (hashed)', name: 'Test Key', - userId: authStub.admin.id, + userId: authStub.admin.user.id, }); expect(cryptoMock.randomBytes).toHaveBeenCalled(); expect(cryptoMock.hashSha256).toHaveBeenCalled(); @@ -35,7 +35,7 @@ describe(APIKeyService.name, () => { expect(keyMock.create).toHaveBeenCalledWith({ key: 'cmFuZG9tLWJ5dGVz (hashed)', name: 'API Key', - userId: authStub.admin.id, + userId: authStub.admin.user.id, }); expect(cryptoMock.randomBytes).toHaveBeenCalled(); expect(cryptoMock.hashSha256).toHaveBeenCalled(); @@ -59,7 +59,7 @@ describe(APIKeyService.name, () => { await sut.update(authStub.admin, 'random-guid', { name: 'New Name' }); - expect(keyMock.update).toHaveBeenCalledWith(authStub.admin.id, 'random-guid', { name: 'New Name' }); + expect(keyMock.update).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid', { name: 'New Name' }); }); }); @@ -77,7 +77,7 @@ describe(APIKeyService.name, () => { await sut.delete(authStub.admin, 'random-guid'); - expect(keyMock.delete).toHaveBeenCalledWith(authStub.admin.id, 'random-guid'); + expect(keyMock.delete).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid'); }); }); @@ -87,7 +87,7 @@ describe(APIKeyService.name, () => { await expect(sut.getById(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException); - expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'random-guid'); + expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid'); }); it('should get a key by id', async () => { @@ -95,7 +95,7 @@ describe(APIKeyService.name, () => { await sut.getById(authStub.admin, 'random-guid'); - expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'random-guid'); + expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid'); }); }); @@ -105,7 +105,7 @@ describe(APIKeyService.name, () => { await expect(sut.getAll(authStub.admin)).resolves.toHaveLength(1); - expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.id); + expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.user.id); }); }); }); diff --git a/server/src/domain/api-key/api-key.service.ts b/server/src/domain/api-key/api-key.service.ts index a77fba7644..2c4b6147af 100644 --- a/server/src/domain/api-key/api-key.service.ts +++ b/server/src/domain/api-key/api-key.service.ts @@ -1,6 +1,6 @@ import { APIKeyEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { ICryptoRepository, IKeyRepository } from '../repositories'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto'; @@ -11,47 +11,47 @@ export class APIKeyService { @Inject(IKeyRepository) private repository: IKeyRepository, ) {} - async create(authUser: AuthUserDto, dto: APIKeyCreateDto): Promise { + async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, ''); const entity = await this.repository.create({ key: this.crypto.hashSha256(secret), name: dto.name || 'API Key', - userId: authUser.id, + userId: auth.user.id, }); return { secret, apiKey: this.map(entity) }; } - async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise { - const exists = await this.repository.getById(authUser.id, id); + async update(auth: AuthDto, id: string, dto: APIKeyCreateDto): Promise { + const exists = await this.repository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); } - const key = await this.repository.update(authUser.id, id, { name: dto.name }); + const key = await this.repository.update(auth.user.id, id, { name: dto.name }); return this.map(key); } - async delete(authUser: AuthUserDto, id: string): Promise { - const exists = await this.repository.getById(authUser.id, id); + async delete(auth: AuthDto, id: string): Promise { + const exists = await this.repository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); } - await this.repository.delete(authUser.id, id); + await this.repository.delete(auth.user.id, id); } - async getById(authUser: AuthUserDto, id: string): Promise { - const key = await this.repository.getById(authUser.id, id); + async getById(auth: AuthDto, id: string): Promise { + const key = await this.repository.getById(auth.user.id, id); if (!key) { throw new BadRequestException('API Key not found'); } return this.map(key); } - async getAll(authUser: AuthUserDto): Promise { - const keys = await this.repository.getByUserId(authUser.id); + async getAll(auth: AuthDto): Promise { + const keys = await this.repository.getByUserId(auth.user.id); return keys.map((key) => this.map(key)); } diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 16d3877d55..c065352f8c 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -59,7 +59,7 @@ const statResponse: AssetStatsResponseDto = { const uploadFile = { nullAuth: { - authUser: null, + auth: null, fieldName: UploadFieldName.ASSET_DATA, file: { checksum: Buffer.from('checksum', 'utf8'), @@ -69,7 +69,7 @@ const uploadFile = { }, filename: (fieldName: UploadFieldName, filename: string) => { return { - authUser: authStub.admin, + auth: authStub.admin, fieldName, file: { mimeType: 'image/jpeg', @@ -328,7 +328,7 @@ describe(AssetService.name, () => { { title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] }, ]); - expect(assetMock.getByDayOfYear.mock.calls).toEqual([[authStub.admin.id, { day: 15, month: 1 }]]); + expect(assetMock.getByDayOfYear.mock.calls).toEqual([[authStub.admin.user.id, { day: 15, month: 1 }]]); }); }); @@ -341,7 +341,7 @@ describe(AssetService.name, () => { size: TimeBucketSize.DAY, }), ).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); - expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.id] }); + expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.user.id] }); }); }); @@ -354,7 +354,7 @@ describe(AssetService.name, () => { sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }), ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-id'])); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id'])); expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { size: TimeBucketSize.DAY, timeBucket: 'bucket', @@ -370,14 +370,14 @@ describe(AssetService.name, () => { size: TimeBucketSize.DAY, timeBucket: 'bucket', isArchived: true, - userId: authStub.admin.id, + userId: authStub.admin.user.id, }), ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { size: TimeBucketSize.DAY, timeBucket: 'bucket', isArchived: true, - userIds: [authStub.admin.id], + userIds: [authStub.admin.user.id], }); }); @@ -388,13 +388,13 @@ describe(AssetService.name, () => { sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', - userId: authStub.admin.id, + userId: authStub.admin.user.id, }), ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { size: TimeBucketSize.DAY, timeBucket: 'bucket', - userIds: [authStub.admin.id], + userIds: [authStub.admin.user.id], }); }); @@ -405,7 +405,7 @@ describe(AssetService.name, () => { timeBucket: 'bucket', isArchived: true, withPartners: true, - userId: authStub.admin.id, + userId: authStub.admin.user.id, }), ).rejects.toThrowError(BadRequestException); @@ -415,7 +415,7 @@ describe(AssetService.name, () => { timeBucket: 'bucket', isArchived: undefined, withPartners: true, - userId: authStub.admin.id, + userId: authStub.admin.user.id, }), ).rejects.toThrowError(BadRequestException); }); @@ -427,7 +427,7 @@ describe(AssetService.name, () => { timeBucket: 'bucket', isFavorite: true, withPartners: true, - userId: authStub.admin.id, + userId: authStub.admin.user.id, }), ).rejects.toThrowError(BadRequestException); @@ -437,7 +437,7 @@ describe(AssetService.name, () => { timeBucket: 'bucket', isFavorite: false, withPartners: true, - userId: authStub.admin.id, + userId: authStub.admin.user.id, }), ).rejects.toThrowError(BadRequestException); }); @@ -449,7 +449,7 @@ describe(AssetService.name, () => { timeBucket: 'bucket', isTrashed: true, withPartners: true, - userId: authStub.admin.id, + userId: authStub.admin.user.id, }), ).rejects.toThrowError(BadRequestException); }); @@ -459,9 +459,9 @@ describe(AssetService.name, () => { it('should require the asset.download permission', async () => { await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); - expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); - expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); + expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); + expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); }); it('should throw an error if the asset is not found', async () => { @@ -550,28 +550,28 @@ describe(AssetService.name, () => { await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse); - expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-1'])); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-1'])); expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1'); }); it('should return a list of archives (userId)', async () => { - accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.user.id])); assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image, assetStub.video], hasNextPage: false, }); - await expect(sut.getDownloadInfo(authStub.admin, { userId: authStub.admin.id })).resolves.toEqual( + await expect(sut.getDownloadInfo(authStub.admin, { userId: authStub.admin.user.id })).resolves.toEqual( downloadResponse, ); - expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.id, { + expect(assetMock.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.user.id, { isVisible: true, }); }); it('should split archives by size', async () => { - accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.user.id])); assetMock.getByUserId.mockResolvedValue({ items: [ @@ -585,7 +585,7 @@ describe(AssetService.name, () => { await expect( sut.getDownloadInfo(authStub.admin, { - userId: authStub.admin.id, + userId: authStub.admin.user.id, archiveSize: 30_000, }), ).resolves.toEqual({ @@ -624,25 +624,25 @@ describe(AssetService.name, () => { it('should get the statistics for a user, excluding archived assets', async () => { assetMock.getStatistics.mockResolvedValue(stats); await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse); - expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: false }); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false }); }); it('should get the statistics for a user for archived assets', async () => { assetMock.getStatistics.mockResolvedValue(stats); await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse); - expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: true }); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true }); }); it('should get the statistics for a user for favorite assets', async () => { assetMock.getStatistics.mockResolvedValue(stats); await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse); - expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isFavorite: true }); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isFavorite: true }); }); it('should get the statistics for a user for all assets', async () => { assetMock.getStatistics.mockResolvedValue(stats); await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse); - expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, {}); + expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {}); }); }); @@ -762,7 +762,7 @@ describe(AssetService.name, () => { stackParentId: 'parent', }); - expect(communicationMock.send).toHaveBeenCalledWith(CommunicationEvent.ASSET_UPDATE, authStub.user1.id, [ + expect(communicationMock.send).toHaveBeenCalledWith(CommunicationEvent.ASSET_UPDATE, authStub.user1.user.id, [ 'asset-1', ]); }); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index f4fb84d546..7a7ec3cef6 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -5,7 +5,7 @@ import { DateTime, Duration } from 'luxon'; import { extname } from 'path'; import sanitize from 'sanitize-filename'; import { AccessCore, Permission } from '../access'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { mimeTypes } from '../domain.constant'; import { HumanReadableSize, usePagination } from '../domain.util'; import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; @@ -63,7 +63,7 @@ export enum UploadFieldName { } export interface UploadRequest { - authUser: AuthUserDto | null; + auth: AuthDto | null; fieldName: UploadFieldName; file: UploadFile; } @@ -93,7 +93,7 @@ export class AssetService { this.configCore = SystemConfigCore.create(configRepository); } - search(authUser: AuthUserDto, dto: AssetSearchDto) { + search(auth: AuthDto, dto: AssetSearchDto) { let checksum: Buffer | undefined = undefined; if (dto.checksum) { @@ -109,7 +109,7 @@ export class AssetService { ...dto, order, checksum, - ownerId: authUser.id, + ownerId: auth.user.id, }) .then((assets) => assets.map((asset) => @@ -121,8 +121,8 @@ export class AssetService { ); } - canUploadFile({ authUser, fieldName, file }: UploadRequest): true { - this.access.requireUploadAccess(authUser); + canUploadFile({ auth, fieldName, file }: UploadRequest): true { + this.access.requireUploadAccess(auth); const filename = file.originalName; @@ -156,8 +156,8 @@ export class AssetService { throw new BadRequestException(`Unsupported file type ${filename}`); } - getUploadFilename({ authUser, fieldName, file }: UploadRequest): string { - this.access.requireUploadAccess(authUser); + getUploadFilename({ auth, fieldName, file }: UploadRequest): string { + this.access.requireUploadAccess(auth); const originalExt = extname(file.originalName); @@ -171,12 +171,12 @@ export class AssetService { return sanitize(`${this.cryptoRepository.randomUUID()}${lookup[fieldName]}`); } - getUploadFolder({ authUser, fieldName }: UploadRequest): string { - authUser = this.access.requireUploadAccess(authUser); + getUploadFolder({ auth, fieldName }: UploadRequest): string { + auth = this.access.requireUploadAccess(auth); - let folder = StorageCore.getFolderLocation(StorageFolder.UPLOAD, authUser.id); + let folder = StorageCore.getFolderLocation(StorageFolder.UPLOAD, auth.user.id); if (fieldName === UploadFieldName.PROFILE_DATA) { - folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, authUser.id); + folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, auth.user.id); } this.storageRepository.mkdirSync(folder); @@ -184,13 +184,13 @@ export class AssetService { return folder; } - getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise { - return this.assetRepository.getMapMarkers(authUser.id, options); + getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise { + return this.assetRepository.getMapMarkers(auth.user.id, options); } - async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise { + async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise { const currentYear = new Date().getFullYear(); - const assets = await this.assetRepository.getByDayOfYear(authUser.id, dto); + const assets = await this.assetRepository.getByDayOfYear(auth.user.id, dto); return _.chain(assets) .filter((asset) => asset.localDateTime.getFullYear() < currentYear) @@ -207,17 +207,17 @@ export class AssetService { .value(); } - private async timeBucketChecks(authUser: AuthUserDto, dto: TimeBucketDto) { + private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { if (dto.albumId) { - await this.access.requirePermission(authUser, Permission.ALBUM_READ, [dto.albumId]); + await this.access.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); } else { - dto.userId = dto.userId || authUser.id; + dto.userId = dto.userId || auth.user.id; } if (dto.userId) { - await this.access.requirePermission(authUser, Permission.TIMELINE_READ, [dto.userId]); + await this.access.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]); if (dto.isArchived !== false) { - await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]); + await this.access.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); } } @@ -234,28 +234,28 @@ export class AssetService { } } - async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise { - await this.timeBucketChecks(authUser, dto); - const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); + async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { + await this.timeBucketChecks(auth, dto); + const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); return this.assetRepository.getTimeBuckets(timeBucketOptions); } async getTimeBucket( - authUser: AuthUserDto, + auth: AuthDto, dto: TimeBucketAssetDto, ): Promise { - await this.timeBucketChecks(authUser, dto); - const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); + await this.timeBucketChecks(auth, dto); + const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); - if (authUser.isShowMetadata) { + if (!auth.sharedLink || auth.sharedLink?.showExif) { return assets.map((asset) => mapAsset(asset, { withStack: true })); } else { return assets.map((asset) => mapAsset(asset, { stripMetadata: true })); } } - async buildTimeBucketOptions(authUser: AuthUserDto, dto: TimeBucketDto): Promise { + async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise { const { userId, ...options } = dto; let userIds: string[] | undefined = undefined; @@ -263,7 +263,7 @@ export class AssetService { userIds = [userId]; if (dto.withPartners) { - const partners = await this.partnerRepository.getAll(authUser.id); + const partners = await this.partnerRepository.getAll(auth.user.id); const partnersIds = partners .filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline) .map((partner) => partner.sharedById); @@ -274,8 +274,8 @@ export class AssetService { return { ...options, userIds }; } - async downloadFile(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id); + async downloadFile(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id); const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { @@ -289,12 +289,12 @@ export class AssetService { return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath)); } - async getDownloadInfo(authUser: AuthUserDto, dto: DownloadInfoDto): Promise { + async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise { const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; const archives: DownloadArchiveInfo[] = []; let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; - const assetPagination = await this.getDownloadAssets(authUser, dto); + const assetPagination = await this.getDownloadAssets(auth, dto); for await (const assets of assetPagination) { // motion part of live photos const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter((id): id is string => !!id); @@ -323,8 +323,8 @@ export class AssetService { }; } - async downloadArchive(authUser: AuthUserDto, dto: AssetIdsDto): Promise { - await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, dto.assetIds); + async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise { + await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds); const zip = this.storageRepository.createZipStream(); const assets = await this.assetRepository.getByIds(dto.assetIds); @@ -347,12 +347,12 @@ export class AssetService { return { stream: zip.stream }; } - private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadInfoDto): Promise> { + private async getDownloadAssets(auth: AuthDto, dto: DownloadInfoDto): Promise> { const PAGINATION_SIZE = 2500; if (dto.assetIds) { const assetIds = dto.assetIds; - await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, assetIds); + await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds); const assets = await this.assetRepository.getByIds(assetIds); return (async function* () { yield assets; @@ -361,13 +361,13 @@ export class AssetService { if (dto.albumId) { const albumId = dto.albumId; - await this.access.requirePermission(authUser, Permission.ALBUM_DOWNLOAD, albumId); + await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId)); } if (dto.userId) { const userId = dto.userId; - await this.access.requirePermission(authUser, Permission.TIMELINE_DOWNLOAD, userId); + await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), ); @@ -376,22 +376,22 @@ export class AssetService { throw new BadRequestException('assetIds, albumId, or userId is required'); } - async getStatistics(authUser: AuthUserDto, dto: AssetStatsDto) { - const stats = await this.assetRepository.getStatistics(authUser.id, dto); + async getStatistics(auth: AuthDto, dto: AssetStatsDto) { + const stats = await this.assetRepository.getStatistics(auth.user.id, dto); return mapStats(stats); } - async getRandom(authUser: AuthUserDto, count: number): Promise { - const assets = await this.assetRepository.getRandom(authUser.id, count); + async getRandom(auth: AuthDto, count: number): Promise { + const assets = await this.assetRepository.getRandom(auth.user.id, count); return assets.map((a) => mapAsset(a)); } - async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { - return this.assetRepository.getAllByDeviceId(authUser.id, deviceId); + async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) { + return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId); } - async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise { - await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id); + async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise { + await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto; await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); @@ -400,9 +400,9 @@ export class AssetService { return mapAsset(asset); } - async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise { + async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto; - await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids); + await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids); if (removeParent) { (options as Partial).stackParentId = null; @@ -411,7 +411,7 @@ export class AssetService { // All the unique parent's -> parent is set to null ids.push(...new Set(assets.filter((a) => !!a.stackParentId).map((a) => a.stackParentId!))); } else if (options.stackParentId) { - await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, options.stackParentId); + await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId); // Merge stacks const assets = await this.assetRepository.getByIds(ids); const assetsWithChildren = assets.filter((a) => a.stack && a.stack.length > 0); @@ -430,7 +430,7 @@ export class AssetService { } await this.assetRepository.updateAll(ids, options); - this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids); + this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, auth.user.id, ids); } async handleAssetDeletionCheck() { @@ -493,10 +493,10 @@ export class AssetService { return true; } - async deleteAll(authUser: AuthUserDto, dto: AssetBulkDeleteDto): Promise { + async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise { const { ids, force } = dto; - await this.access.requirePermission(authUser, Permission.ASSET_DELETE, ids); + await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids); if (force) { for (const id of ids) { @@ -504,20 +504,20 @@ export class AssetService { } } else { await this.assetRepository.softDeleteAll(ids); - this.communicationRepository.send(CommunicationEvent.ASSET_TRASH, authUser.id, ids); + this.communicationRepository.send(CommunicationEvent.ASSET_TRASH, auth.user.id, ids); } } - async handleTrashAction(authUser: AuthUserDto, action: TrashAction): Promise { + async handleTrashAction(auth: AuthDto, action: TrashAction): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, authUser.id, { trashedBefore: DateTime.now().toJSDate() }), + this.assetRepository.getByUserId(pagination, auth.user.id, { trashedBefore: DateTime.now().toJSDate() }), ); if (action == TrashAction.RESTORE_ALL) { for await (const assets of assetPagination) { const ids = assets.map((a) => a.id); await this.assetRepository.restoreAll(ids); - this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids); + this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, auth.user.id, ids); } return; } @@ -532,17 +532,17 @@ export class AssetService { } } - async restoreAll(authUser: AuthUserDto, dto: BulkIdsDto): Promise { + async restoreAll(auth: AuthDto, dto: BulkIdsDto): Promise { const { ids } = dto; - await this.access.requirePermission(authUser, Permission.ASSET_RESTORE, ids); + await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids); await this.assetRepository.restoreAll(ids); - this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids); + this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, auth.user.id, ids); } - async updateStackParent(authUser: AuthUserDto, dto: UpdateStackParentDto): Promise { + async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise { const { oldParentId, newParentId } = dto; - await this.access.requirePermission(authUser, Permission.ASSET_READ, oldParentId); - await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, newParentId); + await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId); + await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId); const childIds: string[] = []; const oldParent = await this.assetRepository.getById(oldParentId); @@ -552,14 +552,14 @@ export class AssetService { childIds.push(...(oldParent.stack?.map((a) => a.id) ?? [])); } - this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, [...childIds, newParentId]); + this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, auth.user.id, [...childIds, newParentId]); await this.assetRepository.updateAll(childIds, { stackParentId: newParentId }); // Remove ParentId of new parent if this was previously a child of some other asset return this.assetRepository.updateAll([newParentId], { stackParentId: null }); } - async run(authUser: AuthUserDto, dto: AssetJobsDto) { - await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds); + async run(auth: AuthDto, dto: AssetJobsDto) { + await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); for (const id of dto.assetIds) { switch (dto.name) { diff --git a/server/src/domain/audit/audit.service.spec.ts b/server/src/domain/audit/audit.service.spec.ts index 5e68250fab..d2f8bb6bcf 100644 --- a/server/src/domain/audit/audit.service.spec.ts +++ b/server/src/domain/audit/audit.service.spec.ts @@ -65,7 +65,7 @@ describe(AuditService.name, () => { expect(auditMock.getAfter).toHaveBeenCalledWith(date, { action: DatabaseAction.DELETE, - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, entityType: EntityType.ASSET, }); }); @@ -81,7 +81,7 @@ describe(AuditService.name, () => { expect(auditMock.getAfter).toHaveBeenCalledWith(date, { action: DatabaseAction.DELETE, - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, entityType: EntityType.ASSET, }); }); diff --git a/server/src/domain/audit/audit.service.ts b/server/src/domain/audit/audit.service.ts index 2b5a304ea1..f994843195 100644 --- a/server/src/domain/audit/audit.service.ts +++ b/server/src/domain/audit/audit.service.ts @@ -3,7 +3,7 @@ import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common' import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AccessCore, Permission } from '../access'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { AUDIT_LOG_MAX_DURATION } from '../domain.constant'; import { usePagination } from '../domain.util'; import { JOBS_ASSET_PAGINATION_SIZE } from '../job'; @@ -48,9 +48,9 @@ export class AuditService { return true; } - async getDeletes(authUser: AuthUserDto, dto: AuditDeletesDto): Promise { - const userId = dto.userId || authUser.id; - await this.access.requirePermission(authUser, Permission.TIMELINE_READ, userId); + async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise { + const userId = dto.userId || auth.user.id; + await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); const audits = await this.repository.getAfter(dto.after, { ownerId: userId, diff --git a/server/src/domain/auth/auth.dto.ts b/server/src/domain/auth/auth.dto.ts index d5ee275ad2..854ac99963 100644 --- a/server/src/domain/auth/auth.dto.ts +++ b/server/src/domain/auth/auth.dto.ts @@ -1,19 +1,14 @@ -import { UserEntity, UserTokenEntity } from '@app/infra/entities'; +import { APIKeyEntity, SharedLinkEntity, UserEntity, UserTokenEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; -export class AuthUserDto { - id!: string; - email!: string; - isAdmin!: boolean; - isPublicUser?: boolean; - sharedLinkId?: string; - isAllowUpload?: boolean; - isAllowDownload?: boolean; - isShowMetadata?: boolean; - accessTokenId?: string; - externalPath?: string | null; +export class AuthDto { + user!: UserEntity; + + apiKey?: APIKeyEntity; + sharedLink?: SharedLinkEntity; + userToken?: UserTokenEntity; } export class LoginCredentialDto { diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index 7ece7bed85..ef3ee64b98 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -31,7 +31,7 @@ import { IUserTokenRepository, } from '../repositories'; import { AuthType } from './auth.constant'; -import { AuthUserDto, SignUpDto } from './auth.dto'; +import { AuthDto, SignUpDto } from './auth.dto'; import { AuthService } from './auth.service'; // const token = Buffer.from('my-api-key', 'utf8').toString('base64'); @@ -145,7 +145,7 @@ describe('AuthService', () => { describe('changePassword', () => { it('should change the password', async () => { - const authUser = { email: 'test@imimch.com' } as UserEntity; + const auth = { user: { email: 'test@imimch.com' } } as AuthDto; const dto = { password: 'old-password', newPassword: 'new-password' }; userMock.getByEmail.mockResolvedValue({ @@ -153,23 +153,23 @@ describe('AuthService', () => { password: 'hash-password', } as UserEntity); - await sut.changePassword(authUser, dto); + await sut.changePassword(auth, dto); - expect(userMock.getByEmail).toHaveBeenCalledWith(authUser.email, true); + expect(userMock.getByEmail).toHaveBeenCalledWith(auth.user.email, true); expect(cryptoMock.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password'); }); it('should throw when auth user email is not found', async () => { - const authUser = { email: 'test@imimch.com' } as UserEntity; + const auth = { user: { email: 'test@imimch.com' } } as AuthDto; const dto = { password: 'old-password', newPassword: 'new-password' }; userMock.getByEmail.mockResolvedValue(null); - await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(UnauthorizedException); + await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(UnauthorizedException); }); it('should throw when password does not match existing password', async () => { - const authUser = { email: 'test@imimch.com' } as UserEntity; + const auth = { user: { email: 'test@imimch.com' } as UserEntity }; const dto = { password: 'old-password', newPassword: 'new-password' }; cryptoMock.compareBcrypt.mockReturnValue(false); @@ -179,11 +179,11 @@ describe('AuthService', () => { password: 'hash-password', } as UserEntity); - await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException); }); it('should throw when user does not have a password', async () => { - const authUser = { email: 'test@imimch.com' } as UserEntity; + const auth = { user: { email: 'test@imimch.com' } } as AuthDto; const dto = { password: 'old-password', newPassword: 'new-password' }; userMock.getByEmail.mockResolvedValue({ @@ -191,33 +191,33 @@ describe('AuthService', () => { password: '', } as UserEntity); - await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException); }); }); describe('logout', () => { it('should return the end session endpoint', async () => { configMock.load.mockResolvedValue(systemConfigStub.enabled); - const authUser = { id: '123' } as AuthUserDto; - await expect(sut.logout(authUser, AuthType.OAUTH)).resolves.toEqual({ + const auth = { user: { id: '123' } } as AuthDto; + await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({ successful: true, redirectUri: 'http://end-session-endpoint', }); }); it('should return the default redirect', async () => { - const authUser = { id: '123' } as AuthUserDto; + const auth = { user: { id: '123' } } as AuthDto; - await expect(sut.logout(authUser, AuthType.PASSWORD)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); }); it('should delete the access token', async () => { - const authUser = { id: '123', accessTokenId: 'token123' } as AuthUserDto; + const auth = { user: { id: '123' }, userToken: { id: 'token123' } } as AuthDto; - await expect(sut.logout(authUser, AuthType.PASSWORD)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); @@ -226,9 +226,9 @@ describe('AuthService', () => { }); it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => { - const authUser = { id: '123' } as AuthUserDto; + const auth = { user: { id: '123' } } as AuthDto; - await expect(sut.logout(authUser, AuthType.OAUTH)).resolves.toEqual({ + await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({ successful: true, redirectUri: '/auth/login?autoLaunch=0', }); @@ -268,7 +268,10 @@ describe('AuthService', () => { userMock.get.mockResolvedValue(userStub.user1); userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; - await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual(userStub.user1); + await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({ + user: userStub.user1, + userToken: userTokenStub.userToken, + }); }); }); @@ -296,7 +299,10 @@ describe('AuthService', () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') }; - await expect(sut.validate(headers, {})).resolves.toEqual(authStub.adminSharedLink); + await expect(sut.validate(headers, {})).resolves.toEqual({ + user: userStub.admin, + sharedLink: sharedLinkStub.valid, + }); expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); }); @@ -304,7 +310,10 @@ describe('AuthService', () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') }; - await expect(sut.validate(headers, {})).resolves.toEqual(authStub.adminSharedLink); + await expect(sut.validate(headers, {})).resolves.toEqual({ + user: userStub.admin, + sharedLink: sharedLinkStub.valid, + }); expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); }); }); @@ -319,14 +328,20 @@ describe('AuthService', () => { it('should return an auth dto', async () => { userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual(userStub.user1); + await expect(sut.validate(headers, {})).resolves.toEqual({ + user: userStub.user1, + userToken: userTokenStub.userToken, + }); }); it('should update when access time exceeds an hour', async () => { userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken); userTokenMock.save.mockResolvedValue(userTokenStub.userToken); const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual(userStub.user1); + await expect(sut.validate(headers, {})).resolves.toEqual({ + user: userStub.user1, + userToken: userTokenStub.userToken, + }); expect(userTokenMock.save.mock.calls[0][0]).toMatchObject({ id: 'not_active', token: 'auth_token', @@ -350,7 +365,7 @@ describe('AuthService', () => { it('should return an auth dto', async () => { keyMock.getKey.mockResolvedValue(keyStub.admin); const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual(authStub.admin); + await expect(sut.validate(headers, {})).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin }); expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); }); }); @@ -377,7 +392,7 @@ describe('AuthService', () => { }, ]); - expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.id); + expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); }); }); @@ -387,7 +402,7 @@ describe('AuthService', () => { await sut.logoutDevices(authStub.user1); - expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.id); + expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); expect(userTokenMock.delete).toHaveBeenCalledWith('not_active'); expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id'); }); @@ -399,7 +414,7 @@ describe('AuthService', () => { await sut.logoutDevice(authStub.user1, 'token-1'); - expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['token-1'])); + expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1'])); expect(userTokenMock.delete).toHaveBeenCalledWith('token-1'); }); }); @@ -506,7 +521,7 @@ describe('AuthService', () => { await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' }); - expect(userMock.update).toHaveBeenCalledWith(authStub.user1.id, { oauthId: sub }); + expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: sub }); }); it('should not link an already linked oauth.sub', async () => { @@ -528,7 +543,7 @@ describe('AuthService', () => { await sut.unlink(authStub.user1); - expect(userMock.update).toHaveBeenCalledWith(authStub.user1.id, { oauthId: '' }); + expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: '' }); }); }); }); diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index 7fa3d69023..b092ea7d31 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -34,7 +34,7 @@ import { } from './auth.constant'; import { AuthDeviceResponseDto, - AuthUserDto, + AuthDto, ChangePasswordDto, LoginCredentialDto, LoginResponseDto, @@ -110,9 +110,9 @@ export class AuthService { return this.createLoginResponse(user, AuthType.PASSWORD, details); } - async logout(authUser: AuthUserDto, authType: AuthType): Promise { - if (authUser.accessTokenId) { - await this.userTokenRepository.delete(authUser.accessTokenId); + async logout(auth: AuthDto, authType: AuthType): Promise { + if (auth.userToken) { + await this.userTokenRepository.delete(auth.userToken.id); } return { @@ -121,9 +121,9 @@ export class AuthService { }; } - async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) { + async changePassword(auth: AuthDto, dto: ChangePasswordDto) { const { password, newPassword } = dto; - const user = await this.userRepository.getByEmail(authUser.email, true); + const user = await this.userRepository.getByEmail(auth.user.email, true); if (!user) { throw new UnauthorizedException(); } @@ -133,7 +133,7 @@ export class AuthService { throw new BadRequestException('Wrong password'); } - return this.userCore.updateUser(authUser, authUser.id, { password: newPassword }); + return this.userCore.updateUser(auth.user, auth.user.id, { password: newPassword }); } async adminSignUp(dto: SignUpDto): Promise { @@ -154,7 +154,7 @@ export class AuthService { return mapUser(admin); } - async validate(headers: IncomingHttpHeaders, params: Record): Promise { + async validate(headers: IncomingHttpHeaders, params: Record): Promise { const shareKey = (headers['x-immich-share-key'] || params.key) as string; const userToken = (headers['x-immich-user-token'] || params.userToken || @@ -177,20 +177,20 @@ export class AuthService { throw new UnauthorizedException('Authentication required'); } - async getDevices(authUser: AuthUserDto): Promise { - const userTokens = await this.userTokenRepository.getAll(authUser.id); - return userTokens.map((userToken) => mapUserToken(userToken, authUser.accessTokenId)); + async getDevices(auth: AuthDto): Promise { + const userTokens = await this.userTokenRepository.getAll(auth.user.id); + return userTokens.map((userToken) => mapUserToken(userToken, auth.userToken?.id)); } - async logoutDevice(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.AUTH_DEVICE_DELETE, id); + async logoutDevice(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id); await this.userTokenRepository.delete(id); } - async logoutDevices(authUser: AuthUserDto): Promise { - const devices = await this.userTokenRepository.getAll(authUser.id); + async logoutDevices(auth: AuthDto): Promise { + const devices = await this.userTokenRepository.getAll(auth.user.id); for (const device of devices) { - if (device.id === authUser.accessTokenId) { + if (device.id === auth.userToken?.id) { continue; } await this.userTokenRepository.delete(device.id); @@ -284,19 +284,19 @@ export class AuthService { return this.createLoginResponse(user, AuthType.OAUTH, loginDetails); } - async link(user: AuthUserDto, dto: OAuthCallbackDto): Promise { + async link(auth: AuthDto, dto: OAuthCallbackDto): Promise { const config = await this.configCore.getConfig(); const { sub: oauthId } = await this.getOAuthProfile(config, dto.url); const duplicate = await this.userRepository.getByOAuthId(oauthId); - if (duplicate && duplicate.id !== user.id) { + if (duplicate && duplicate.id !== auth.user.id) { this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`); throw new BadRequestException('This OAuth account has already been linked to another user.'); } - return mapUser(await this.userRepository.update(user.id, { oauthId })); + return mapUser(await this.userRepository.update(auth.user.id, { oauthId })); } - async unlink(user: AuthUserDto): Promise { - return mapUser(await this.userRepository.update(user.id, { oauthId: '' })); + async unlink(auth: AuthDto): Promise { + return mapUser(await this.userRepository.update(auth.user.id, { oauthId: '' })); } private async getLogoutEndpoint(authType: AuthType): Promise { @@ -371,45 +371,27 @@ export class AuthService { return cookies[IMMICH_ACCESS_COOKIE] || null; } - private async validateSharedLink(key: string | string[]): Promise { + private async validateSharedLink(key: string | string[]): Promise { key = Array.isArray(key) ? key[0] : key; const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url'); - const link = await this.sharedLinkRepository.getByKey(bytes); - if (link) { - if (!link.expiresAt || new Date(link.expiresAt) > new Date()) { - const user = link.user; + const sharedLink = await this.sharedLinkRepository.getByKey(bytes); + if (sharedLink) { + if (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date()) { + const user = sharedLink.user; if (user) { - return { - id: user.id, - email: user.email, - isAdmin: user.isAdmin, - isPublicUser: true, - sharedLinkId: link.id, - isAllowUpload: link.allowUpload, - isAllowDownload: link.allowDownload, - isShowMetadata: link.showExif, - }; + return { user, sharedLink }; } } } throw new UnauthorizedException('Invalid share key'); } - private async validateApiKey(key: string): Promise { + private async validateApiKey(key: string): Promise { const hashedKey = this.cryptoRepository.hashSha256(key); - const keyEntity = await this.keyRepository.getKey(hashedKey); - if (keyEntity?.user) { - const user = keyEntity.user; - - return { - id: user.id, - email: user.email, - isAdmin: user.isAdmin, - isPublicUser: false, - isAllowUpload: true, - externalPath: user.externalPath, - }; + const apiKey = await this.keyRepository.getKey(hashedKey); + if (apiKey?.user) { + return { user: apiKey.user, apiKey }; } throw new UnauthorizedException('Invalid API key'); @@ -422,26 +404,19 @@ export class AuthService { return this.cryptoRepository.compareBcrypt(inputPassword, user.password); } - private async validateUserToken(tokenValue: string): Promise { + private async validateUserToken(tokenValue: string): Promise { const hashedToken = this.cryptoRepository.hashSha256(tokenValue); - let token = await this.userTokenRepository.getByToken(hashedToken); + let userToken = await this.userTokenRepository.getByToken(hashedToken); - if (token?.user) { + if (userToken?.user) { const now = DateTime.now(); - const updatedAt = DateTime.fromJSDate(token.updatedAt); + const updatedAt = DateTime.fromJSDate(userToken.updatedAt); const diff = now.diff(updatedAt, ['hours']); if (diff.hours > 1) { - token = await this.userTokenRepository.save({ ...token, updatedAt: new Date() }); + userToken = await this.userTokenRepository.save({ ...userToken, updatedAt: new Date() }); } - return { - ...token.user, - isPublicUser: false, - isAllowUpload: true, - isAllowDownload: true, - isShowMetadata: true, - accessTokenId: token.id, - }; + return { user: userToken.user, userToken }; } throw new UnauthorizedException('Invalid user token'); diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index f49600a365..d8fac9bf7e 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -632,7 +632,7 @@ describe(LibraryService.name, () => { await expect(sut.getCount(authStub.admin)).resolves.toBe(17); - expect(libraryMock.getCountForUser).toHaveBeenCalledWith(authStub.admin.id); + expect(libraryMock.getCountForUser).toHaveBeenCalledWith(authStub.admin.user.id); }); }); @@ -673,7 +673,7 @@ describe(LibraryService.name, () => { }), ]); - expect(libraryMock.getAllByUserId).toHaveBeenCalledWith(authStub.admin.id); + expect(libraryMock.getAllByUserId).toHaveBeenCalledWith(authStub.admin.user.id); }); }); @@ -963,10 +963,10 @@ describe(LibraryService.name, () => { describe('update', () => { it('can update library ', async () => { libraryMock.update.mockResolvedValue(libraryStub.uploadLibrary1); - await expect(sut.update(authStub.admin, authStub.admin.id, {})).resolves.toBeTruthy(); + await expect(sut.update(authStub.admin, authStub.admin.user.id, {})).resolves.toBeTruthy(); expect(libraryMock.update).toHaveBeenCalledWith( expect.objectContaining({ - id: authStub.admin.id, + id: authStub.admin.user.id, }), ); }); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 6bec17c6b0..3c31482f33 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -5,7 +5,7 @@ import { Stats } from 'node:fs'; import path from 'node:path'; import { basename, parse } from 'path'; import { AccessCore, Permission } from '../access'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { mimeTypes } from '../domain.constant'; import { usePagination, validateCronExpression } from '../domain.util'; import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; @@ -70,22 +70,22 @@ export class LibraryService { }); } - async getStatistics(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.LIBRARY_READ, id); + async getStatistics(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.LIBRARY_READ, id); return this.repository.getStatistics(id); } - async getCount(authUser: AuthUserDto): Promise { - return this.repository.getCountForUser(authUser.id); + async getCount(auth: AuthDto): Promise { + return this.repository.getCountForUser(auth.user.id); } - async getAllForUser(authUser: AuthUserDto): Promise { - const libraries = await this.repository.getAllByUserId(authUser.id); + async getAllForUser(auth: AuthDto): Promise { + const libraries = await this.repository.getAllByUserId(auth.user.id); return libraries.map((library) => mapLibrary(library)); } - async get(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.LIBRARY_READ, id); + async get(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.LIBRARY_READ, id); const library = await this.findOrFail(id); return mapLibrary(library); } @@ -99,7 +99,7 @@ export class LibraryService { return true; } - async create(authUser: AuthUserDto, dto: CreateLibraryDto): Promise { + async create(auth: AuthDto, dto: CreateLibraryDto): Promise { switch (dto.type) { case LibraryType.EXTERNAL: if (!dto.name) { @@ -120,7 +120,7 @@ export class LibraryService { } const library = await this.repository.create({ - ownerId: authUser.id, + ownerId: auth.user.id, name: dto.name, type: dto.type, importPaths: dto.importPaths ?? [], @@ -131,17 +131,17 @@ export class LibraryService { return mapLibrary(library); } - async update(authUser: AuthUserDto, id: string, dto: UpdateLibraryDto): Promise { - await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id); + async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise { + await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id); const library = await this.repository.update({ id, ...dto }); return mapLibrary(library); } - async delete(authUser: AuthUserDto, id: string) { - await this.access.requirePermission(authUser, Permission.LIBRARY_DELETE, id); + async delete(auth: AuthDto, id: string) { + await this.access.requirePermission(auth, Permission.LIBRARY_DELETE, id); const library = await this.findOrFail(id); - const uploadCount = await this.repository.getUploadLibraryCount(authUser.id); + const uploadCount = await this.repository.getUploadLibraryCount(auth.user.id); if (library.type === LibraryType.UPLOAD && uploadCount <= 1) { throw new BadRequestException('Cannot delete the last upload library'); } @@ -294,8 +294,8 @@ export class LibraryService { return true; } - async queueScan(authUser: AuthUserDto, id: string, dto: ScanLibraryDto) { - await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id); + async queueScan(auth: AuthDto, id: string, dto: ScanLibraryDto) { + await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id); const library = await this.repository.get(id); if (!library || library.type !== LibraryType.EXTERNAL) { @@ -312,9 +312,9 @@ export class LibraryService { }); } - async queueRemoveOffline(authUser: AuthUserDto, id: string) { + async queueRemoveOffline(auth: AuthDto, id: string) { this.logger.verbose(`Removing offline files from library: ${id}`); - await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id); + await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id); await this.jobRepository.queue({ name: JobName.LIBRARY_REMOVE_OFFLINE, diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index c632cc8da3..1d28ee8921 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -60,13 +60,13 @@ describe(PartnerService.name, () => { it("should return a list of partners with whom I've shared my library", async () => { partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toEqual([responseDto.admin]); - expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.id); + expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); }); it('should return a list of partners who have shared their libraries with me', async () => { partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toEqual([responseDto.admin]); - expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.id); + expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); }); }); @@ -75,18 +75,18 @@ describe(PartnerService.name, () => { partnerMock.get.mockResolvedValue(null); partnerMock.create.mockResolvedValue(partnerStub.adminToUser1); - await expect(sut.create(authStub.admin, authStub.user1.id)).resolves.toEqual(responseDto.user1); + await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toEqual(responseDto.user1); expect(partnerMock.create).toHaveBeenCalledWith({ - sharedById: authStub.admin.id, - sharedWithId: authStub.user1.id, + sharedById: authStub.admin.user.id, + sharedWithId: authStub.user1.user.id, }); }); it('should throw an error when the partner already exists', async () => { partnerMock.get.mockResolvedValue(partnerStub.adminToUser1); - await expect(sut.create(authStub.admin, authStub.user1.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.create(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException); expect(partnerMock.create).not.toHaveBeenCalled(); }); @@ -96,7 +96,7 @@ describe(PartnerService.name, () => { it('should remove a partner', async () => { partnerMock.get.mockResolvedValue(partnerStub.adminToUser1); - await sut.remove(authStub.admin, authStub.user1.id); + await sut.remove(authStub.admin, authStub.user1.user.id); expect(partnerMock.remove).toHaveBeenCalledWith(partnerStub.adminToUser1); }); @@ -104,7 +104,7 @@ describe(PartnerService.name, () => { it('should throw an error when the partner does not exist', async () => { partnerMock.get.mockResolvedValue(null); - await expect(sut.remove(authStub.admin, authStub.user1.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.remove(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException); expect(partnerMock.remove).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/partner/partner.service.ts b/server/src/domain/partner/partner.service.ts index 93600a5c0d..7a9cf182b4 100644 --- a/server/src/domain/partner/partner.service.ts +++ b/server/src/domain/partner/partner.service.ts @@ -1,7 +1,7 @@ import { PartnerEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; import { mapUser } from '../user'; import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto'; @@ -16,8 +16,8 @@ export class PartnerService { this.access = AccessCore.create(accessRepository); } - async create(authUser: AuthUserDto, sharedWithId: string): Promise { - const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; + async create(auth: AuthDto, sharedWithId: string): Promise { + const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; const exists = await this.repository.get(partnerId); if (exists) { throw new BadRequestException(`Partner already exists`); @@ -27,8 +27,8 @@ export class PartnerService { return this.map(partner, PartnerDirection.SharedBy); } - async remove(authUser: AuthUserDto, sharedWithId: string): Promise { - const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; + async remove(auth: AuthDto, sharedWithId: string): Promise { + const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; const partner = await this.repository.get(partnerId); if (!partner) { throw new BadRequestException('Partner not found'); @@ -37,18 +37,18 @@ export class PartnerService { await this.repository.remove(partner); } - async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise { - const partners = await this.repository.getAll(authUser.id); + async getAll(auth: AuthDto, direction: PartnerDirection): Promise { + const partners = await this.repository.getAll(auth.user.id); const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; return partners .filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users - .filter((partner) => partner[key] === authUser.id) + .filter((partner) => partner[key] === auth.user.id) .map((partner) => this.map(partner, direction)); } - async update(authUser: AuthUserDto, sharedById: string, dto: UpdatePartnerDto): Promise { - await this.access.requirePermission(authUser, Permission.PARTNER_UPDATE, sharedById); - const partnerId: PartnerIds = { sharedById, sharedWithId: authUser.id }; + async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { + await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById); + const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); return this.map(entity, PartnerDirection.SharedWith); diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index c4a5bb68ea..b9d1ea0776 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -2,7 +2,7 @@ import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { Optional, ValidateUUID, toBoolean } from '../domain.util'; export class PersonUpdateDto { @@ -156,9 +156,9 @@ export function mapFacesWithoutPerson(face: AssetFaceEntity): AssetFaceWithoutPe }; } -export function mapFaces(face: AssetFaceEntity, authUser: AuthUserDto): AssetFaceResponseDto { +export function mapFaces(face: AssetFaceEntity, auth: AuthDto): AssetFaceResponseDto { return { ...mapFacesWithoutPerson(face), - person: face.person?.ownerId === authUser.id ? mapPerson(face.person) : null, + person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null, }; } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 6a63634252..81baa6a835 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -113,7 +113,7 @@ describe(PersonService.name, () => { visible: 1, people: [responseDto], }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, { + expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { minimumFaceCount: 1, withHidden: false, }); @@ -125,7 +125,7 @@ describe(PersonService.name, () => { visible: 1, people: [responseDto], }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, { + expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { minimumFaceCount: 1, withHidden: false, }); @@ -146,7 +146,7 @@ describe(PersonService.name, () => { }, ], }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, { + expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { minimumFaceCount: 1, withHidden: true, }); @@ -157,14 +157,14 @@ describe(PersonService.name, () => { it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.withName); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should throw a bad request when person is not found', async () => { personMock.getById.mockResolvedValue(null); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should get a person by id', async () => { @@ -172,7 +172,7 @@ describe(PersonService.name, () => { accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto); expect(personMock.getById).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); }); @@ -181,7 +181,7 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValue(personStub.noName); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { @@ -189,7 +189,7 @@ describe(PersonService.name, () => { accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should throw an error when person has no thumbnail', async () => { @@ -197,7 +197,7 @@ describe(PersonService.name, () => { accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should serve the thumbnail', async () => { @@ -205,7 +205,7 @@ describe(PersonService.name, () => { accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getThumbnail(authStub.admin, 'person-1'); expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg'); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); }); @@ -214,7 +214,7 @@ describe(PersonService.name, () => { personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]); await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(personMock.getAssets).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should return a person's assets", async () => { @@ -222,7 +222,7 @@ describe(PersonService.name, () => { accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getAssets(authStub.admin, 'person-1'); expect(personMock.getAssets).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); }); @@ -233,7 +233,7 @@ describe(PersonService.name, () => { BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { @@ -243,7 +243,7 @@ describe(PersonService.name, () => { BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should update a person's name", async () => { @@ -256,7 +256,7 @@ describe(PersonService.name, () => { expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' }); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should update a person's date of birth", async () => { @@ -276,7 +276,7 @@ describe(PersonService.name, () => { expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); expect(jobMock.queue).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should update a person visibility', async () => { @@ -289,7 +289,7 @@ describe(PersonService.name, () => { expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false }); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should update a person's thumbnailPath", async () => { @@ -312,7 +312,7 @@ describe(PersonService.name, () => { }, ]); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } }); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should throw an error when the face feature assetId is invalid', async () => { @@ -323,7 +323,7 @@ describe(PersonService.name, () => { BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); }); @@ -336,7 +336,7 @@ describe(PersonService.name, () => { sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }), ).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); }); @@ -761,7 +761,7 @@ describe(PersonService.name, () => { expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should merge two people', async () => { @@ -784,7 +784,7 @@ describe(PersonService.name, () => { name: JobName.PERSON_DELETE, data: { id: personStub.mergePerson.id }, }); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should throw an error when the primary person is not found', async () => { @@ -796,7 +796,7 @@ describe(PersonService.name, () => { ); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should handle invalid merge ids', async () => { @@ -811,7 +811,7 @@ describe(PersonService.name, () => { expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should handle an error reassigning faces', async () => { @@ -826,7 +826,7 @@ describe(PersonService.name, () => { ]); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); }); @@ -836,19 +836,19 @@ describe(PersonService.name, () => { personMock.getStatistics.mockResolvedValue(statistics); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 }); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); }); describe('mapFace', () => { it('should map a face', () => { - expect(mapFaces(faceStub.face1, personStub.withName.owner)).toEqual({ + expect(mapFaces(faceStub.face1, { user: personStub.withName.owner })).toEqual({ boundingBoxX1: 0, boundingBoxX2: 1, boundingBoxY1: 0, diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index ffd1cc149f..1d7ef86f18 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -3,7 +3,7 @@ import { PersonPathType } from '@app/infra/entities/move.entity'; import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { mimeTypes } from '../domain.constant'; import { usePagination } from '../domain.util'; import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; @@ -65,9 +65,9 @@ export class PersonService { this.storageCore = StorageCore.create(assetRepository, moveRepository, repository, storageRepository); } - async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise { + async getAll(auth: AuthDto, dto: PersonSearchDto): Promise { const { machineLearning } = await this.configCore.getConfig(); - const people = await this.repository.getAllForUser(authUser.id, { + const people = await this.repository.getAllForUser(auth.user.id, { minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden: dto.withHidden || false, }); @@ -83,12 +83,12 @@ export class PersonService { }; } - createPerson(authUser: AuthUserDto): Promise { - return this.repository.create({ ownerId: authUser.id }); + createPerson(auth: AuthDto): Promise { + return this.repository.create({ ownerId: auth.user.id }); } - async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); + async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { + await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); const person = await this.findOrFail(personId); const result: PersonResponseDto[] = []; const changeFeaturePhoto: string[] = []; @@ -96,7 +96,7 @@ export class PersonService { const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); for (const face of faces) { - await this.access.requirePermission(authUser, Permission.PERSON_CREATE, face.id); + await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id); if (person.faceAssetId === null) { changeFeaturePhoto.push(person.id); } @@ -116,10 +116,10 @@ export class PersonService { return result; } - async reassignFacesById(authUser: AuthUserDto, personId: string, dto: FaceDto): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); + async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { + await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); - await this.access.requirePermission(authUser, Permission.PERSON_CREATE, dto.id); + await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id); const face = await this.repository.getFaceById(dto.id); const person = await this.findOrFail(personId); @@ -134,10 +134,10 @@ export class PersonService { return await this.findOrFail(personId).then(mapPerson); } - async getFacesById(authUser: AuthUserDto, dto: FaceDto): Promise { - await this.access.requirePermission(authUser, Permission.ASSET_READ, dto.id); + async getFacesById(auth: AuthDto, dto: FaceDto): Promise { + await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id); const faces = await this.repository.getFaces(dto.id); - return faces.map((asset) => mapFaces(asset, authUser)); + return faces.map((asset) => mapFaces(asset, auth)); } async createNewFeaturePhoto(changeFeaturePhoto: string[]) { @@ -163,18 +163,18 @@ export class PersonService { } } - async getById(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_READ, id); + async getById(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.PERSON_READ, id); return this.findOrFail(id).then(mapPerson); } - async getStatistics(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_READ, id); + async getStatistics(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.PERSON_READ, id); return this.repository.getStatistics(id); } - async getThumbnail(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_READ, id); + async getThumbnail(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.PERSON_READ, id); const person = await this.repository.getById(id); if (!person || !person.thumbnailPath) { throw new NotFoundException(); @@ -183,14 +183,14 @@ export class PersonService { return this.storageRepository.createReadStream(person.thumbnailPath, mimeTypes.lookup(person.thumbnailPath)); } - async getAssets(authUser: AuthUserDto, id: string): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_READ, id); + async getAssets(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.PERSON_READ, id); const assets = await this.repository.getAssets(id); return assets.map((asset) => mapAsset(asset)); } - async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise { - await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id); + async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { + await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); let person = await this.findOrFail(id); const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; @@ -200,7 +200,7 @@ export class PersonService { } if (assetId) { - await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId); + await this.access.requirePermission(auth, Permission.ASSET_READ, assetId); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); if (!face) { throw new BadRequestException('Invalid assetId for feature face'); @@ -213,11 +213,11 @@ export class PersonService { return mapPerson(person); } - async updatePeople(authUser: AuthUserDto, dto: PeopleUpdateDto): Promise { + async updatePeople(auth: AuthDto, dto: PeopleUpdateDto): Promise { const results: BulkIdResponseDto[] = []; for (const person of dto.people) { try { - await this.update(authUser, person.id, { + await this.update(auth, person.id, { isHidden: person.isHidden, name: person.name, birthDate: person.birthDate, @@ -438,15 +438,15 @@ export class PersonService { return true; } - async mergePerson(authUser: AuthUserDto, id: string, dto: MergePersonDto): Promise { + async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise { const mergeIds = dto.ids; - await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id); + await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); const primaryPerson = await this.findOrFail(id); const primaryName = primaryPerson.name || primaryPerson.id; const results: BulkIdResponseDto[] = []; - const allowedIds = await this.access.checkAccess(authUser, Permission.PERSON_MERGE, mergeIds); + const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds); for (const mergeId of mergeIds) { const hasAccess = allowedIds.has(mergeId); diff --git a/server/src/domain/search/search.service.spec.ts b/server/src/domain/search/search.service.spec.ts index ae0b0935da..c6fe2abf66 100644 --- a/server/src/domain/search/search.service.spec.ts +++ b/server/src/domain/search/search.service.spec.ts @@ -49,11 +49,11 @@ describe(SearchService.name, () => { await sut.searchPerson(authStub.user1, { name, withHidden: false }); - expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.id, name, { withHidden: false }); + expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false }); await sut.searchPerson(authStub.user1, { name, withHidden: true }); - expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.id, name, { withHidden: true }); + expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true }); }); }); @@ -105,7 +105,7 @@ describe(SearchService.name, () => { const result = await sut.search(authStub.user1, dto); expect(result).toEqual(expectedResponse); - expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, authStub.user1.id, { numResults: 250 }); + expect(assetMock.searchMetadata).toHaveBeenCalledWith(dto.q, authStub.user1.user.id, { numResults: 250 }); expect(smartInfoMock.searchCLIP).not.toHaveBeenCalled(); }); @@ -132,7 +132,11 @@ describe(SearchService.name, () => { const result = await sut.search(authStub.user1, dto); expect(result).toEqual(expectedResponse); - expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ ownerId: authStub.user1.id, embedding, numResults: 100 }); + expect(smartInfoMock.searchCLIP).toHaveBeenCalledWith({ + ownerId: authStub.user1.user.id, + embedding, + numResults: 100, + }); expect(assetMock.searchMetadata).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 3347909fcc..cd454ad17d 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -1,7 +1,7 @@ import { AssetEntity } from '@app/infra/entities'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { AssetResponseDto, mapAsset } from '../asset'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { PersonResponseDto } from '../person'; import { IAssetRepository, @@ -31,16 +31,16 @@ export class SearchService { this.configCore = SystemConfigCore.create(configRepository); } - async searchPerson(authUser: AuthUserDto, dto: SearchPeopleDto): Promise { - return this.personRepository.getByName(authUser.id, dto.name, { withHidden: dto.withHidden }); + async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise { + return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden }); } - async getExploreData(authUser: AuthUserDto): Promise[]> { + async getExploreData(auth: AuthDto): Promise[]> { await this.configCore.requireFeature(FeatureFlag.SEARCH); const options = { maxFields: 12, minAssetsPerField: 5 }; const results = await Promise.all([ - this.assetRepository.getAssetIdByCity(authUser.id, options), - this.assetRepository.getAssetIdByTag(authUser.id, options), + this.assetRepository.getAssetIdByCity(auth.user.id, options), + this.assetRepository.getAssetIdByTag(auth.user.id, options), ]); const assetIds = new Set(results.flatMap((field) => field.items.map((item) => item.data))); const assets = await this.assetRepository.getByIds(Array.from(assetIds)); @@ -52,7 +52,7 @@ export class SearchService { })); } - async search(authUser: AuthUserDto, dto: SearchDto): Promise { + async search(auth: AuthDto, dto: SearchDto): Promise { const { machineLearning } = await this.configCore.getConfig(); const query = dto.q || dto.query; if (!query) { @@ -73,10 +73,10 @@ export class SearchService { { text: query }, machineLearning.clip, ); - assets = await this.smartInfoRepository.searchCLIP({ ownerId: authUser.id, embedding, numResults: 100 }); + assets = await this.smartInfoRepository.searchCLIP({ ownerId: auth.user.id, embedding, numResults: 100 }); break; case SearchStrategy.TEXT: - assets = await this.assetRepository.searchMetadata(query, authUser.id, { numResults: 250 }); + assets = await this.assetRepository.searchMetadata(query, auth.user.id, { numResults: 250 }); default: break; } diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index bfc74e8245..cb50f9ba57 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/domain/shared-link/shared-link.service.spec.ts @@ -41,7 +41,7 @@ describe(SharedLinkService.name, () => { sharedLinkResponseStub.expired, sharedLinkResponseStub.valid, ]); - expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.id); + expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); }); }); @@ -55,21 +55,21 @@ describe(SharedLinkService.name, () => { const authDto = authStub.adminSharedLink; shareMock.get.mockResolvedValue(sharedLinkStub.valid); await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid); - expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); + expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); it('should not return metadata', async () => { const authDto = authStub.adminSharedLinkNoExif; shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); - expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); + expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); it('should throw an error for an password protected shared link', async () => { const authDto = authStub.adminSharedLink; shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired); await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException); - expect(shareMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); + expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); }); @@ -77,14 +77,14 @@ describe(SharedLinkService.name, () => { it('should throw an error for an invalid shared link', async () => { shareMock.get.mockResolvedValue(null); await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id'); + expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(shareMock.update).not.toHaveBeenCalled(); }); it('should get a shared link by id', async () => { shareMock.get.mockResolvedValue(sharedLinkStub.valid); await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id); + expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); }); }); @@ -120,12 +120,12 @@ describe(SharedLinkService.name, () => { await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id }); expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.id, + authStub.admin.user.id, new Set([albumStub.oneAsset.id]), ); expect(shareMock.create).toHaveBeenCalledWith({ type: SharedLinkType.ALBUM, - userId: authStub.admin.id, + userId: authStub.admin.user.id, albumId: albumStub.oneAsset.id, allowDownload: true, allowUpload: true, @@ -149,10 +149,13 @@ describe(SharedLinkService.name, () => { allowUpload: true, }); - expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id])); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.user.id, + new Set([assetStub.image.id]), + ); expect(shareMock.create).toHaveBeenCalledWith({ type: SharedLinkType.INDIVIDUAL, - userId: authStub.admin.id, + userId: authStub.admin.user.id, albumId: null, allowDownload: true, allowUpload: true, @@ -169,7 +172,7 @@ describe(SharedLinkService.name, () => { it('should throw an error for an invalid shared link', async () => { shareMock.get.mockResolvedValue(null); await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id'); + expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(shareMock.update).not.toHaveBeenCalled(); }); @@ -177,10 +180,10 @@ describe(SharedLinkService.name, () => { shareMock.get.mockResolvedValue(sharedLinkStub.valid); shareMock.update.mockResolvedValue(sharedLinkStub.valid); await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false }); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id); + expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); expect(shareMock.update).toHaveBeenCalledWith({ id: sharedLinkStub.valid.id, - userId: authStub.user1.id, + userId: authStub.user1.user.id, allowDownload: false, }); }); @@ -190,14 +193,14 @@ describe(SharedLinkService.name, () => { it('should throw an error for an invalid shared link', async () => { shareMock.get.mockResolvedValue(null); await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, 'missing-id'); + expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(shareMock.update).not.toHaveBeenCalled(); }); it('should remove a key', async () => { shareMock.get.mockResolvedValue(sharedLinkStub.valid); await sut.remove(authStub.user1, sharedLinkStub.valid.id); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.id, sharedLinkStub.valid.id); + expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid); }); }); diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index bd9aaea6a3..ee53e95080 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/domain/shared-link/shared-link.service.ts @@ -2,7 +2,7 @@ import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entiti import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories'; import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto'; import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from './shared-link.dto'; @@ -19,42 +19,36 @@ export class SharedLinkService { this.access = AccessCore.create(accessRepository); } - getAll(authUser: AuthUserDto): Promise { - return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink)); + getAll(auth: AuthDto): Promise { + return this.repository.getAll(auth.user.id).then((links) => links.map(mapSharedLink)); } - async getMine(authUser: AuthUserDto, dto: SharedLinkPasswordDto): Promise { - const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser; - - if (!isPublicUser || !id) { + async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise { + if (!auth.sharedLink) { throw new ForbiddenException(); } - const sharedLink = await this.findOrFail(authUser, id); - - let newToken; + const sharedLink = await this.findOrFail(auth, auth.sharedLink.id); + const response = this.map(sharedLink, { withExif: sharedLink.showExif }); if (sharedLink.password) { - newToken = this.validateAndRefreshToken(sharedLink, dto); + response.token = this.validateAndRefreshToken(sharedLink, dto); } - return { - ...this.map(sharedLink, { withExif: isShowExif ?? true }), - token: newToken, - }; + return response; } - async get(authUser: AuthUserDto, id: string): Promise { - const sharedLink = await this.findOrFail(authUser, id); + async get(auth: AuthDto, id: string): Promise { + const sharedLink = await this.findOrFail(auth, id); return this.map(sharedLink, { withExif: true }); } - async create(authUser: AuthUserDto, dto: SharedLinkCreateDto): Promise { + async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise { switch (dto.type) { case SharedLinkType.ALBUM: if (!dto.albumId) { throw new BadRequestException('Invalid albumId'); } - await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, dto.albumId); + await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId); break; case SharedLinkType.INDIVIDUAL: @@ -62,14 +56,14 @@ export class SharedLinkService { throw new BadRequestException('Invalid assetIds'); } - await this.access.requirePermission(authUser, Permission.ASSET_SHARE, dto.assetIds); + await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds); break; } const sharedLink = await this.repository.create({ key: this.cryptoRepository.randomBytes(50), - userId: authUser.id, + userId: auth.user.id, type: dto.type, albumId: dto.albumId || null, assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity), @@ -84,11 +78,11 @@ export class SharedLinkService { return this.map(sharedLink, { withExif: true }); } - async update(authUser: AuthUserDto, id: string, dto: SharedLinkEditDto) { - await this.findOrFail(authUser, id); + async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) { + await this.findOrFail(auth, id); const sharedLink = await this.repository.update({ id, - userId: authUser.id, + userId: auth.user.id, description: dto.description, password: dto.password, expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt, @@ -99,21 +93,21 @@ export class SharedLinkService { return this.map(sharedLink, { withExif: true }); } - async remove(authUser: AuthUserDto, id: string): Promise { - const sharedLink = await this.findOrFail(authUser, id); + async remove(auth: AuthDto, id: string): Promise { + const sharedLink = await this.findOrFail(auth, id); await this.repository.remove(sharedLink); } - private async findOrFail(authUser: AuthUserDto, id: string) { - const sharedLink = await this.repository.get(authUser.id, id); + private async findOrFail(auth: AuthDto, id: string) { + const sharedLink = await this.repository.get(auth.user.id, id); if (!sharedLink) { throw new BadRequestException('Shared link not found'); } return sharedLink; } - async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { - const sharedLink = await this.findOrFail(authUser, id); + async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { + const sharedLink = await this.findOrFail(auth, id); if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { throw new BadRequestException('Invalid shared link type'); @@ -121,7 +115,7 @@ export class SharedLinkService { const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); - const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -146,8 +140,8 @@ export class SharedLinkService { return results; } - async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { - const sharedLink = await this.findOrFail(authUser, id); + async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { + const sharedLink = await this.findOrFail(auth, id); if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { throw new BadRequestException('Invalid shared link type'); diff --git a/server/src/domain/tag/tag.service.spec.ts b/server/src/domain/tag/tag.service.spec.ts index bb03f88f6d..e987beb6a7 100644 --- a/server/src/domain/tag/tag.service.spec.ts +++ b/server/src/domain/tag/tag.service.spec.ts @@ -23,7 +23,7 @@ describe(TagService.name, () => { it('should return all tags for a user', async () => { tagMock.getAll.mockResolvedValue([tagStub.tag1]); await expect(sut.getAll(authStub.admin)).resolves.toEqual([tagResponseStub.tag1]); - expect(tagMock.getAll).toHaveBeenCalledWith(authStub.admin.id); + expect(tagMock.getAll).toHaveBeenCalledWith(authStub.admin.user.id); }); }); @@ -31,13 +31,13 @@ describe(TagService.name, () => { it('should throw an error for an invalid id', async () => { tagMock.getById.mockResolvedValue(null); await expect(sut.getById(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); }); it('should return a tag for a user', async () => { tagMock.getById.mockResolvedValue(tagStub.tag1); await expect(sut.getById(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); }); }); @@ -47,7 +47,7 @@ describe(TagService.name, () => { await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).rejects.toBeInstanceOf( BadRequestException, ); - expect(tagMock.hasName).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.hasName).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.create).not.toHaveBeenCalled(); }); @@ -57,7 +57,7 @@ describe(TagService.name, () => { tagResponseStub.tag1, ); expect(tagMock.create).toHaveBeenCalledWith({ - userId: authStub.admin.id, + userId: authStub.admin.user.id, name: 'tag-1', type: TagType.CUSTOM, }); @@ -68,7 +68,7 @@ describe(TagService.name, () => { it('should throw an error for an invalid id', async () => { tagMock.getById.mockResolvedValue(null); await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.remove).not.toHaveBeenCalled(); }); @@ -76,7 +76,7 @@ describe(TagService.name, () => { tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.update.mockResolvedValue(tagStub.tag1); await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).resolves.toEqual(tagResponseStub.tag1); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', name: 'tag-2' }); }); }); @@ -85,14 +85,14 @@ describe(TagService.name, () => { it('should throw an error for an invalid id', async () => { tagMock.getById.mockResolvedValue(null); await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.remove).not.toHaveBeenCalled(); }); it('should remove a tag', async () => { tagMock.getById.mockResolvedValue(tagStub.tag1); await sut.remove(authStub.admin, 'tag-1'); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.remove).toHaveBeenCalledWith(tagStub.tag1); }); }); @@ -101,7 +101,7 @@ describe(TagService.name, () => { it('should throw an error for an invalid id', async () => { tagMock.getById.mockResolvedValue(null); await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.remove).not.toHaveBeenCalled(); }); @@ -109,8 +109,8 @@ describe(TagService.name, () => { tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.getAssets.mockResolvedValue([assetStub.image]); await sut.getAssets(authStub.admin, 'tag-1'); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); - expect(tagMock.getAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + expect(tagMock.getAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); }); }); @@ -120,15 +120,15 @@ describe(TagService.name, () => { await expect(sut.addAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( BadRequestException, ); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.addAssets).not.toHaveBeenCalled(); }); it('should reject duplicate asset ids and accept new ones', async () => { tagMock.getById.mockResolvedValue(tagStub.tag1); - when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true); - when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-2').mockResolvedValue(false); + when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-1').mockResolvedValue(true); + when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-2').mockResolvedValue(false); await expect( sut.addAssets(authStub.admin, 'tag-1', { @@ -139,9 +139,9 @@ describe(TagService.name, () => { { assetId: 'asset-2', success: true }, ]); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); - expect(tagMock.addAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1', ['asset-2']); + expect(tagMock.addAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-2']); }); }); @@ -151,15 +151,15 @@ describe(TagService.name, () => { await expect(sut.removeAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( BadRequestException, ); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.removeAssets).not.toHaveBeenCalled(); }); it('should accept accept ids that are tagged and reject the rest', async () => { tagMock.getById.mockResolvedValue(tagStub.tag1); - when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true); - when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-2').mockResolvedValue(false); + when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-1').mockResolvedValue(true); + when(tagMock.hasAsset).calledWith(authStub.admin.user.id, 'tag-1', 'asset-2').mockResolvedValue(false); await expect( sut.removeAssets(authStub.admin, 'tag-1', { @@ -170,9 +170,9 @@ describe(TagService.name, () => { { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, ]); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.id, 'tag-1'); + expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); - expect(tagMock.removeAssets).toHaveBeenCalledWith(authStub.admin.id, 'tag-1', ['asset-1']); + expect(tagMock.removeAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-1']); }); }); }); diff --git a/server/src/domain/tag/tag.service.ts b/server/src/domain/tag/tag.service.ts index ea6dae9a0d..f7f06c4177 100644 --- a/server/src/domain/tag/tag.service.ts +++ b/server/src/domain/tag/tag.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto, AssetResponseDto, mapAsset } from '../asset'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { ITagRepository } from '../repositories'; import { TagResponseDto, mapTag } from './tag-response.dto'; import { CreateTagDto, UpdateTagDto } from './tag.dto'; @@ -9,23 +9,23 @@ import { CreateTagDto, UpdateTagDto } from './tag.dto'; export class TagService { constructor(@Inject(ITagRepository) private repository: ITagRepository) {} - getAll(authUser: AuthUserDto) { - return this.repository.getAll(authUser.id).then((tags) => tags.map(mapTag)); + getAll(auth: AuthDto) { + return this.repository.getAll(auth.user.id).then((tags) => tags.map(mapTag)); } - async getById(authUser: AuthUserDto, id: string): Promise { - const tag = await this.findOrFail(authUser, id); + async getById(auth: AuthDto, id: string): Promise { + const tag = await this.findOrFail(auth, id); return mapTag(tag); } - async create(authUser: AuthUserDto, dto: CreateTagDto) { - const duplicate = await this.repository.hasName(authUser.id, dto.name); + async create(auth: AuthDto, dto: CreateTagDto) { + const duplicate = await this.repository.hasName(auth.user.id, dto.name); if (duplicate) { throw new BadRequestException(`A tag with that name already exists`); } const tag = await this.repository.create({ - userId: authUser.id, + userId: auth.user.id, name: dto.name, type: dto.type, }); @@ -33,29 +33,29 @@ export class TagService { return mapTag(tag); } - async update(authUser: AuthUserDto, id: string, dto: UpdateTagDto): Promise { - await this.findOrFail(authUser, id); + async update(auth: AuthDto, id: string, dto: UpdateTagDto): Promise { + await this.findOrFail(auth, id); const tag = await this.repository.update({ id, name: dto.name }); return mapTag(tag); } - async remove(authUser: AuthUserDto, id: string): Promise { - const tag = await this.findOrFail(authUser, id); + async remove(auth: AuthDto, id: string): Promise { + const tag = await this.findOrFail(auth, id); await this.repository.remove(tag); } - async getAssets(authUser: AuthUserDto, id: string): Promise { - await this.findOrFail(authUser, id); - const assets = await this.repository.getAssets(authUser.id, id); + async getAssets(auth: AuthDto, id: string): Promise { + await this.findOrFail(auth, id); + const assets = await this.repository.getAssets(auth.user.id, id); return assets.map((asset) => mapAsset(asset)); } - async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { - await this.findOrFail(authUser, id); + async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { + await this.findOrFail(auth, id); const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { - const hasAsset = await this.repository.hasAsset(authUser.id, id, assetId); + const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId); if (hasAsset) { results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); } else { @@ -64,7 +64,7 @@ export class TagService { } await this.repository.addAssets( - authUser.id, + auth.user.id, id, results.filter((result) => result.success).map((result) => result.assetId), ); @@ -72,12 +72,12 @@ export class TagService { return results; } - async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise { - await this.findOrFail(authUser, id); + async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { + await this.findOrFail(auth, id); const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { - const hasAsset = await this.repository.hasAsset(authUser.id, id, assetId); + const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId); if (!hasAsset) { results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); } else { @@ -86,7 +86,7 @@ export class TagService { } await this.repository.removeAssets( - authUser.id, + auth.user.id, id, results.filter((result) => result.success).map((result) => result.assetId), ); @@ -94,8 +94,8 @@ export class TagService { return results; } - private async findOrFail(authUser: AuthUserDto, id: string) { - const tag = await this.repository.getById(authUser.id, id); + private async findOrFail(auth: AuthDto, id: string) { + const tag = await this.repository.getById(auth.user.id, id); if (!tag) { throw new BadRequestException('Tag not found'); } diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index c8e77a5004..240869b863 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -2,8 +2,8 @@ import { LibraryType, UserEntity } from '@app/infra/entities'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import path from 'path'; import sanitize from 'sanitize-filename'; -import { AuthUserDto } from '../auth'; import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories'; +import { UserResponseDto } from './response-dto'; const SALT_ROUNDS = 10; @@ -32,17 +32,18 @@ export class UserCore { instance = null; } - async updateUser(authUser: AuthUserDto, id: string, dto: Partial): Promise { - if (!authUser.isAdmin && authUser.id !== id) { + // TODO: move auth related checks to the service layer + async updateUser(user: UserEntity | UserResponseDto, id: string, dto: Partial): Promise { + if (!user.isAdmin && user.id !== id) { throw new ForbiddenException('You are not allowed to update this user'); } - if (!authUser.isAdmin) { + if (!user.isAdmin) { // Users can never update the isAdmin property. delete dto.isAdmin; delete dto.storageLabel; delete dto.externalPath; - } else if (dto.isAdmin && authUser.id !== id) { + } else if (dto.isAdmin && user.id !== id) { // Admin cannot create another admin. throw new BadRequestException('The server already has an admin'); } diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index 04b4206cab..2758e3eda5 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -60,10 +60,10 @@ describe(UserService.name, () => { sut = new UserService(albumMock, assetMock, cryptoRepositoryMock, jobMock, libraryMock, storageMock, userMock); - when(userMock.get).calledWith(authStub.admin.id, {}).mockResolvedValue(userStub.admin); - when(userMock.get).calledWith(authStub.admin.id, { withDeleted: true }).mockResolvedValue(userStub.admin); - when(userMock.get).calledWith(authStub.user1.id, {}).mockResolvedValue(userStub.user1); - when(userMock.get).calledWith(authStub.user1.id, { withDeleted: true }).mockResolvedValue(userStub.user1); + when(userMock.get).calledWith(authStub.admin.user.id, {}).mockResolvedValue(userStub.admin); + when(userMock.get).calledWith(authStub.admin.user.id, { withDeleted: true }).mockResolvedValue(userStub.admin); + when(userMock.get).calledWith(authStub.user1.user.id, {}).mockResolvedValue(userStub.user1); + when(userMock.get).calledWith(authStub.user1.user.id, { withDeleted: true }).mockResolvedValue(userStub.user1); }); describe('getAll', () => { @@ -71,8 +71,8 @@ describe(UserService.name, () => { userMock.getList.mockResolvedValue([userStub.admin]); await expect(sut.getAll(authStub.admin, false)).resolves.toEqual([ expect.objectContaining({ - id: authStub.admin.id, - email: authStub.admin.email, + id: authStub.admin.user.id, + email: authStub.admin.user.email, }), ]); expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true }); @@ -82,14 +82,14 @@ describe(UserService.name, () => { describe('get', () => { it('should get a user by id', async () => { userMock.get.mockResolvedValue(userStub.admin); - await sut.get(authStub.admin.id); - expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false }); + await sut.get(authStub.admin.user.id); + expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false }); }); it('should throw an error if a user is not found', async () => { userMock.get.mockResolvedValue(null); - await expect(sut.get(authStub.admin.id)).rejects.toBeInstanceOf(NotFoundException); - expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false }); + await expect(sut.get(authStub.admin.user.id)).rejects.toBeInstanceOf(NotFoundException); + expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false }); }); }); @@ -97,13 +97,13 @@ describe(UserService.name, () => { it("should get the auth user's info", async () => { userMock.get.mockResolvedValue(userStub.admin); await sut.getMe(authStub.admin); - expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, {}); + expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, {}); }); it('should throw an error if a user is not found', async () => { userMock.get.mockResolvedValue(null); await expect(sut.getMe(authStub.admin)).rejects.toBeInstanceOf(BadRequestException); - expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, {}); + expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, {}); }); }); @@ -119,7 +119,7 @@ describe(UserService.name, () => { userMock.getByStorageLabel.mockResolvedValue(null); userMock.update.mockResolvedValue(userStub.user1); - await sut.update({ ...authStub.user1, isAdmin: true }, update); + await sut.update({ user: { ...authStub.user1.user, isAdmin: true } }, update); expect(userMock.getByEmail).toHaveBeenCalledWith(update.email); expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel); @@ -127,13 +127,16 @@ describe(UserService.name, () => { it('should not set an empty string for storage label', async () => { userMock.update.mockResolvedValue(userStub.user1); - await sut.update(userStub.admin, { id: userStub.user1.id, storageLabel: '' }); - expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.id, storageLabel: null }); + await sut.update(authStub.admin, { id: userStub.user1.id, storageLabel: '' }); + expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { + id: userStub.user1.id, + storageLabel: null, + }); }); it('should omit a storage label set by non-admin users', async () => { userMock.update.mockResolvedValue(userStub.user1); - await sut.update(userStub.user1, { id: userStub.user1.id, storageLabel: 'admin' }); + await sut.update({ user: userStub.user1 }, { id: userStub.user1.id, storageLabel: 'admin' }); expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.id }); }); @@ -145,10 +148,13 @@ describe(UserService.name, () => { id: 'not_immich_auth_user_id', }); - const result = sut.update(userStub.user1, { - id: 'not_immich_auth_user_id', - password: 'I take over your account now', - }); + const result = sut.update( + { user: userStub.user1 }, + { + id: 'not_immich_auth_user_id', + password: 'I take over your account now', + }, + ); await expect(result).rejects.toBeInstanceOf(ForbiddenException); }); @@ -158,7 +164,7 @@ describe(UserService.name, () => { userMock.get.mockResolvedValue(userStub.user1); userMock.update.mockResolvedValue(userStub.user1); - await sut.update(userStub.user1, dto); + await sut.update({ user: userStub.user1 }, dto); expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: 'user-id', @@ -172,7 +178,7 @@ describe(UserService.name, () => { userMock.get.mockResolvedValue(userStub.user1); userMock.getByEmail.mockResolvedValue(userStub.admin); - await expect(sut.update(userStub.user1, dto)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update({ user: userStub.user1 }, dto)).rejects.toBeInstanceOf(BadRequestException); expect(userMock.update).not.toHaveBeenCalled(); }); @@ -183,7 +189,7 @@ describe(UserService.name, () => { userMock.get.mockResolvedValue(userStub.user1); userMock.getByStorageLabel.mockResolvedValue(userStub.admin); - await expect(sut.update(userStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(authStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException); expect(userMock.update).not.toHaveBeenCalled(); }); @@ -195,7 +201,7 @@ describe(UserService.name, () => { }; when(userMock.update).calledWith(userStub.user1.id, update).mockResolvedValueOnce(userStub.user1); - await sut.update(userStub.admin, update); + await sut.update(authStub.admin, update); expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: 'user-id', shouldChangePassword: true, @@ -205,7 +211,7 @@ describe(UserService.name, () => { it('update user information should throw error if user not found', async () => { when(userMock.get).calledWith(userStub.user1.id, {}).mockResolvedValueOnce(null); - const result = sut.update(userStub.admin, { + const result = sut.update(authStub.admin, { id: userStub.user1.id, shouldChangePassword: true, }); @@ -218,7 +224,7 @@ describe(UserService.name, () => { when(userMock.update).calledWith(userStub.admin.id, dto).mockResolvedValueOnce(userStub.admin); - await sut.update(userStub.admin, dto); + await sut.update(authStub.admin, dto); expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, dto); }); @@ -228,7 +234,7 @@ describe(UserService.name, () => { when(userMock.get).calledWith(userStub.user1.id, {}).mockResolvedValueOnce(userStub.user1); - await expect(sut.update(userStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(authStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException); }); }); @@ -239,11 +245,6 @@ describe(UserService.name, () => { expect(userMock.restore).not.toHaveBeenCalled(); }); - it('should require an admin', async () => { - when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(userStub.admin); - await expect(sut.restore(authStub.user1, userStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException); - }); - it('should restore an user', async () => { userMock.get.mockResolvedValue(userStub.user1); userMock.restore.mockResolvedValue(userStub.user1); @@ -267,7 +268,7 @@ describe(UserService.name, () => { }); it('should require the auth user be an admin', async () => { - await expect(sut.delete(authStub.user1, authStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException); + await expect(sut.delete(authStub.user1, authStub.admin.user.id)).rejects.toBeInstanceOf(ForbiddenException); expect(userMock.delete).not.toHaveBeenCalled(); }); @@ -276,7 +277,7 @@ describe(UserService.name, () => { userMock.get.mockResolvedValue(userStub.user1); userMock.delete.mockResolvedValue(userStub.user1); - await expect(sut.delete(userStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1)); + await expect(sut.delete(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1)); expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, {}); expect(userMock.delete).toHaveBeenCalledWith(userStub.user1); }); @@ -323,7 +324,7 @@ describe(UserService.name, () => { const file = { path: '/profile/path' } as Express.Multer.File; userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); - await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(BadRequestException); + await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(BadRequestException); }); it('should throw an error if the user profile could not be updated with the new image', async () => { @@ -331,7 +332,7 @@ describe(UserService.name, () => { userMock.get.mockResolvedValue(userStub.profilePath); userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error')); - await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(InternalServerErrorException); + await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(InternalServerErrorException); }); it('should delete the previous profile image', async () => { @@ -340,7 +341,7 @@ describe(UserService.name, () => { const files = [userStub.profilePath.profileImagePath]; userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); - await sut.createProfileImage(userStub.admin, file); + await sut.createProfileImage(authStub.admin, file); await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); }); @@ -349,7 +350,7 @@ describe(UserService.name, () => { userMock.get.mockResolvedValue(userStub.admin); userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); - await sut.createProfileImage(userStub.admin, file); + await sut.createProfileImage(authStub.admin, file); expect(jobMock.queue).not.toHaveBeenCalled(); }); }); @@ -358,7 +359,7 @@ describe(UserService.name, () => { it('should send an http error has no profile image', async () => { userMock.get.mockResolvedValue(userStub.admin); - await expect(sut.deleteProfileImage(userStub.admin)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.deleteProfileImage(authStub.admin)).rejects.toBeInstanceOf(BadRequestException); expect(jobMock.queue).not.toHaveBeenCalled(); }); @@ -366,7 +367,7 @@ describe(UserService.name, () => { userMock.get.mockResolvedValue(userStub.profilePath); const files = [userStub.profilePath.profileImagePath]; - await sut.deleteProfileImage(userStub.admin); + await sut.deleteProfileImage(authStub.admin); await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); }); }); diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index 3232a6f94d..cd4d4547c0 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -1,7 +1,7 @@ import { UserEntity } from '@app/infra/entities'; import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { randomBytes } from 'crypto'; -import { AuthUserDto } from '../auth'; +import { AuthDto } from '../auth'; import { IEntityJob, JobName } from '../job'; import { IAlbumRepository, @@ -36,7 +36,7 @@ export class UserService { this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository); } - async getAll(authUser: AuthUserDto, isAll: boolean): Promise { + async getAll(auth: AuthDto, isAll: boolean): Promise { const users = await this.userRepository.getList({ withDeleted: !isAll }); return users.map(mapUser); } @@ -50,24 +50,20 @@ export class UserService { return mapUser(user); } - getMe(authUser: AuthUserDto): Promise { - return this.findOrFail(authUser.id, {}).then(mapUser); + getMe(auth: AuthDto): Promise { + return this.findOrFail(auth.user.id, {}).then(mapUser); } create(createUserDto: CreateUserDto): Promise { return this.userCore.createUser(createUserDto).then(mapUser); } - async update(authUser: AuthUserDto, dto: UpdateUserDto): Promise { + async update(auth: AuthDto, dto: UpdateUserDto): Promise { await this.findOrFail(dto.id, {}); - return this.userCore.updateUser(authUser, dto.id, dto).then(mapUser); + return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser); } - async delete(authUser: AuthUserDto, id: string): Promise { - if (!authUser.isAdmin) { - throw new ForbiddenException('Unauthorized'); - } - + async delete(auth: AuthDto, id: string): Promise { const user = await this.findOrFail(id, {}); if (user.isAdmin) { throw new ForbiddenException('Cannot delete admin user'); @@ -78,35 +74,28 @@ export class UserService { return this.userRepository.delete(user).then(mapUser); } - async restore(authUser: AuthUserDto, id: string): Promise { - if (!authUser.isAdmin) { - throw new ForbiddenException('Unauthorized'); - } - + async restore(auth: AuthDto, id: string): Promise { let user = await this.findOrFail(id, { withDeleted: true }); user = await this.userRepository.restore(user); await this.albumRepository.restoreAll(id); return mapUser(user); } - async createProfileImage( - authUser: AuthUserDto, - fileInfo: Express.Multer.File, - ): Promise { - const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false }); - const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path }); + async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise { + const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false }); + const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path }); if (oldpath !== '') { await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); } return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath); } - async deleteProfileImage(authUser: AuthUserDto): Promise { - const user = await this.findOrFail(authUser.id, { withDeleted: false }); + async deleteProfileImage(auth: AuthDto): Promise { + const user = await this.findOrFail(auth.user.id, { withDeleted: false }); if (user.profileImagePath === '') { throw new BadRequestException("Can't delete a missing profile Image"); } - await this.userRepository.update(authUser.id, { profileImagePath: '' }); + await this.userRepository.update(auth.user.id, { profileImagePath: '' }); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } }); } diff --git a/server/src/immich-admin/constants.ts b/server/src/immich-admin/constants.ts index cc9d1c72c7..44fbfdf772 100644 --- a/server/src/immich-admin/constants.ts +++ b/server/src/immich-admin/constants.ts @@ -1,9 +1,10 @@ -import { AuthUserDto } from '@app/domain'; +import { AuthDto } from '@app/domain'; +import { UserEntity } from '@app/infra/entities'; -export const CLI_USER: AuthUserDto = { - id: 'cli', - email: 'cli@immich.app', - isAdmin: true, - isPublicUser: false, - isAllowUpload: true, +export const CLI_USER: AuthDto = { + user: { + id: 'cli', + email: 'cli@immich.app', + isAdmin: true, + } as UserEntity, }; diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index ad17ccf319..1060598740 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -1,4 +1,4 @@ -import { AssetResponseDto, AuthUserDto } from '@app/domain'; +import { AssetResponseDto, AuthDto } from '@app/domain'; import { Body, Controller, @@ -16,7 +16,7 @@ import { } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Response as Res } from 'express'; -import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard'; +import { Auth, Authenticated, SharedLinkRoute } from '../../app.guard'; import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors'; import FileNotEmptyValidator from '../validation/file-not-empty-validator'; @@ -55,7 +55,7 @@ export class AssetController { type: CreateAssetDto, }) async uploadFile( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles, @Body(new ValidationPipe({ transform: true })) dto: CreateAssetDto, @Response({ passthrough: true }) res: Res, @@ -73,7 +73,7 @@ export class AssetController { sidecarFile = mapToUploadFile(_sidecarFile); } - const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile, sidecarFile); + const responseDto = await this.assetService.uploadFile(auth, dto, file, livePhotoFile, sidecarFile); if (responseDto.duplicate) { res.status(HttpStatus.OK); } @@ -89,12 +89,12 @@ export class AssetController { }, }) async serveFile( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Response() res: Res, @Query(new ValidationPipe({ transform: true })) query: ServeFileDto, @Param() { id }: UUIDParamDto, ) { - await this.assetService.serveFile(authUser, id, query, res); + await this.assetService.serveFile(auth, id, query, res); } @SharedLinkRoute() @@ -106,27 +106,27 @@ export class AssetController { }, }) async getAssetThumbnail( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Response() res: Res, @Param() { id }: UUIDParamDto, @Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto, ) { - await this.assetService.serveThumbnail(authUser, id, query, res); + await this.assetService.serveThumbnail(auth, id, query, res); } @Get('/curated-objects') - getCuratedObjects(@AuthUser() authUser: AuthUserDto): Promise { - return this.assetService.getCuratedObject(authUser); + getCuratedObjects(@Auth() auth: AuthDto): Promise { + return this.assetService.getCuratedObject(auth); } @Get('/curated-locations') - getCuratedLocations(@AuthUser() authUser: AuthUserDto): Promise { - return this.assetService.getCuratedLocation(authUser); + getCuratedLocations(@Auth() auth: AuthDto): Promise { + return this.assetService.getCuratedLocation(auth); } @Get('/search-terms') - getAssetSearchTerms(@AuthUser() authUser: AuthUserDto): Promise { - return this.assetService.getAssetSearchTerm(authUser); + getAssetSearchTerms(@Auth() auth: AuthDto): Promise { + return this.assetService.getAssetSearchTerm(auth); } /** @@ -140,10 +140,10 @@ export class AssetController { schema: { type: 'string' }, }) getAllAssets( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto, ): Promise { - return this.assetService.getAllAssets(authUser, dto); + return this.assetService.getAllAssets(auth, dto); } /** @@ -151,8 +151,8 @@ export class AssetController { */ @Get('/:deviceId') @ApiOperation({ deprecated: true, summary: 'Use /asset/device/:deviceId instead - Remove in 1.92 release' }) - getUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { - return this.assetService.getUserAssetsByDeviceId(authUser, deviceId); + getUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) { + return this.assetService.getUserAssetsByDeviceId(auth, deviceId); } /** @@ -160,8 +160,8 @@ export class AssetController { */ @SharedLinkRoute() @Get('/assetById/:id') - getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.assetService.getAssetById(authUser, id) as Promise; + getAssetById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.assetService.getAssetById(auth, id) as Promise; } /** @@ -170,10 +170,10 @@ export class AssetController { @Post('/exist') @HttpCode(HttpStatus.OK) checkExistingAssets( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Body(ValidationPipe) dto: CheckExistingAssetsDto, ): Promise { - return this.assetService.checkExistingAssets(authUser, dto); + return this.assetService.checkExistingAssets(auth, dto); } /** @@ -182,9 +182,9 @@ export class AssetController { @Post('/bulk-upload-check') @HttpCode(HttpStatus.OK) checkBulkUpload( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Body(ValidationPipe) dto: AssetBulkUploadCheckDto, ): Promise { - return this.assetService.bulkUploadCheck(authUser, dto); + return this.assetService.bulkUploadCheck(auth, dto); } } diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts index d6ce0efad7..9e85dfc58d 100644 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ b/server/src/immich/api-v1/asset/asset.core.ts @@ -1,4 +1,4 @@ -import { AuthUserDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain'; +import { AuthDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain'; import { AssetEntity } from '@app/infra/entities'; import { parse } from 'node:path'; import { IAssetRepository } from './asset-repository'; @@ -11,14 +11,14 @@ export class AssetCore { ) {} async create( - authUser: AuthUserDto, + auth: AuthDto, dto: CreateAssetDto & { libraryId: string }, file: UploadFile, livePhotoAssetId?: string, sidecarPath?: string, ): Promise { const asset = await this.repository.create({ - ownerId: authUser.id, + ownerId: auth.user.id, libraryId: dto.libraryId, checksum: file.checksum, diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index 9b1e02b9cf..cb7a930859 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -226,7 +226,7 @@ describe('AssetService', () => { ], }); - expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.id, [file1, file2]); + expect(assetRepositoryMock.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]); }); }); @@ -235,7 +235,10 @@ describe('AssetService', () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.admin, assetStub.image.id); - expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id])); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.user.id, + new Set([assetStub.image.id]), + ); }); it('should allow shared link access', async () => { @@ -243,7 +246,7 @@ describe('AssetService', () => { assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.adminSharedLink, assetStub.image.id); expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith( - authStub.adminSharedLink.sharedLinkId, + authStub.adminSharedLink.sharedLink?.id, new Set([assetStub.image.id]), ); }); @@ -253,7 +256,7 @@ describe('AssetService', () => { assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.admin, assetStub.image.id); expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith( - authStub.admin.id, + authStub.admin.user.id, new Set([assetStub.image.id]), ); }); @@ -262,7 +265,10 @@ describe('AssetService', () => { accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id])); assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.admin, assetStub.image.id); - expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id])); + expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith( + authStub.admin.user.id, + new Set([assetStub.image.id]), + ); }); it('should throw an error for no access', async () => { diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index f21c75befd..80cbd5a5d0 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -1,7 +1,7 @@ import { AccessCore, AssetResponseDto, - AuthUserDto, + AuthDto, getLivePhotoMotionFilename, IAccessRepository, IJobRepository, @@ -65,7 +65,7 @@ export class AssetService { } public async uploadFile( - authUser: AuthUserDto, + auth: AuthDto, dto: CreateAssetDto, file: UploadFile, livePhotoFile?: UploadFile, @@ -81,15 +81,15 @@ export class AssetService { let livePhotoAsset: AssetEntity | null = null; try { - const libraryId = await this.getLibraryId(authUser, dto.libraryId); - await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId); + const libraryId = await this.getLibraryId(auth, dto.libraryId); + await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId); if (livePhotoFile) { const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId }; - livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile); + livePhotoAsset = await this.assetCore.create(auth, livePhotoDto, livePhotoFile); } const asset = await this.assetCore.create( - authUser, + auth, { ...dto, libraryId }, file, livePhotoAsset?.id, @@ -107,7 +107,7 @@ export class AssetService { // handle duplicates with a success response if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) { const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum); - const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums); + const [duplicate] = await this._assetRepository.getAssetsByChecksums(auth.user.id, checksums); return { id: duplicate.id, duplicate: true }; } @@ -116,33 +116,29 @@ export class AssetService { } } - public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { - return this._assetRepository.getAllByDeviceId(authUser.id, deviceId); + public async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) { + return this._assetRepository.getAllByDeviceId(auth.user.id, deviceId); } - public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise { - const userId = dto.userId || authUser.id; - await this.access.requirePermission(authUser, Permission.TIMELINE_READ, userId); + public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise { + const userId = dto.userId || auth.user.id; + await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); const assets = await this._assetRepository.getAllByUserId(userId, dto); return assets.map((asset) => mapAsset(asset)); } - public async getAssetById( - authUser: AuthUserDto, - assetId: string, - ): Promise { - await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId); + public async getAssetById(auth: AuthDto, assetId: string): Promise { + await this.access.requirePermission(auth, Permission.ASSET_READ, assetId); - const includeMetadata = this.getExifPermission(authUser); const asset = await this._assetRepository.getById(assetId); - if (includeMetadata) { + if (!auth.sharedLink || auth.sharedLink?.showExif) { const data = mapAsset(asset, { withStack: true }); - if (data.ownerId !== authUser.id) { + if (data.ownerId !== auth.user.id) { data.people = []; } - if (authUser.isPublicUser) { + if (auth.sharedLink) { delete data.owner; } @@ -152,8 +148,8 @@ export class AssetService { } } - async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) { - await this.access.requirePermission(authUser, Permission.ASSET_VIEW, assetId); + async serveThumbnail(auth: AuthDto, assetId: string, query: GetAssetThumbnailDto, res: Res) { + await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId); const asset = await this._assetRepository.get(assetId); if (!asset) { @@ -172,16 +168,16 @@ export class AssetService { } } - public async serveFile(authUser: AuthUserDto, assetId: string, query: ServeFileDto, res: Res) { + public async serveFile(auth: AuthDto, assetId: string, query: ServeFileDto, res: Res) { // this is not quite right as sometimes this returns the original still - await this.access.requirePermission(authUser, Permission.ASSET_VIEW, assetId); + await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId); const asset = await this._assetRepository.getById(assetId); if (!asset) { throw new NotFoundException('Asset does not exist'); } - const allowOriginalFile = !!(!authUser.isPublicUser || authUser.isAllowDownload); + const allowOriginalFile = !!(!auth.sharedLink || auth.sharedLink?.allowDownload); const filepath = asset.type === AssetType.IMAGE @@ -191,10 +187,10 @@ export class AssetService { await this.sendFile(res, filepath); } - async getAssetSearchTerm(authUser: AuthUserDto): Promise { + async getAssetSearchTerm(auth: AuthDto): Promise { const possibleSearchTerm = new Set(); - const rows = await this._assetRepository.getSearchPropertiesByUserId(authUser.id); + const rows = await this._assetRepository.getSearchPropertiesByUserId(auth.user.id); rows.forEach((row: SearchPropertiesDto) => { // tags row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); @@ -224,24 +220,24 @@ export class AssetService { return Array.from(possibleSearchTerm).filter((x) => x != null && x != ''); } - async getCuratedLocation(authUser: AuthUserDto): Promise { - return this._assetRepository.getLocationsByUserId(authUser.id); + async getCuratedLocation(auth: AuthDto): Promise { + return this._assetRepository.getLocationsByUserId(auth.user.id); } - async getCuratedObject(authUser: AuthUserDto): Promise { - return this._assetRepository.getDetectedObjectsByUserId(authUser.id); + async getCuratedObject(auth: AuthDto): Promise { + return this._assetRepository.getDetectedObjectsByUserId(auth.user.id); } async checkExistingAssets( - authUser: AuthUserDto, + auth: AuthDto, checkExistingAssetsDto: CheckExistingAssetsDto, ): Promise { return { - existingIds: await this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto), + existingIds: await this._assetRepository.getExistingAssets(auth.user.id, checkExistingAssetsDto), }; } - async bulkUploadCheck(authUser: AuthUserDto, dto: AssetBulkUploadCheckDto): Promise { + async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise { // support base64 and hex checksums for (const asset of dto.assets) { if (asset.checksum.length === 28) { @@ -250,7 +246,7 @@ export class AssetService { } const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex')); - const results = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums); + const results = await this._assetRepository.getAssetsByChecksums(auth.user.id, checksums); const checksumMap: Record = {}; for (const { id, checksum } of results) { @@ -279,10 +275,6 @@ export class AssetService { }; } - getExifPermission(authUser: AuthUserDto) { - return !authUser.isPublicUser || authUser.isShowMetadata; - } - private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { switch (format) { case GetAssetThumbnailFormatEnum.WEBP: @@ -354,15 +346,15 @@ export class AssetService { } } - private async getLibraryId(authUser: AuthUserDto, libraryId?: string) { + private async getLibraryId(auth: AuthDto, libraryId?: string) { if (libraryId) { return libraryId; } - let library = await this.libraryRepository.getDefaultUploadLibrary(authUser.id); + let library = await this.libraryRepository.getDefaultUploadLibrary(auth.user.id); if (!library) { library = await this.libraryRepository.create({ - ownerId: authUser.id, + ownerId: auth.user.id, name: 'Default Library', assets: [], type: LibraryType.UPLOAD, diff --git a/server/src/immich/app.guard.ts b/server/src/immich/app.guard.ts index 0a9fe2dc15..e54a9c4d28 100644 --- a/server/src/immich/app.guard.ts +++ b/server/src/immich/app.guard.ts @@ -1,4 +1,4 @@ -import { AuthService, AuthUserDto, IMMICH_API_KEY_NAME, LoginDetails } from '@app/domain'; +import { AuthDto, AuthService, IMMICH_API_KEY_NAME, LoginDetails } from '@app/domain'; import { CanActivate, ExecutionContext, @@ -50,8 +50,8 @@ export const SharedLinkRoute = () => applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false })); export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value); -export const AuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => { - return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user; +export const Auth = createParamDecorator((data, ctx: ExecutionContext): AuthDto => { + return ctx.switchToHttp().getRequest<{ user: AuthDto }>().user; }); export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => { @@ -67,7 +67,7 @@ export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext }); export interface AuthRequest extends Request { - user?: AuthUserDto; + user?: AuthDto; } @Injectable() @@ -93,12 +93,12 @@ export class AppGuard implements CanActivate { const req = context.switchToHttp().getRequest(); const authDto = await this.authService.validate(req.headers, req.query as Record); - if (authDto.isPublicUser && !isSharedRoute) { + if (authDto.sharedLink && !isSharedRoute) { this.logger.warn(`Denied access to non-shared route: ${req.path}`); return false; } - if (isAdminRoute && !authDto.isAdmin) { + if (isAdminRoute && !authDto.user.isAdmin) { this.logger.warn(`Denied access to admin only route: ${req.path}`); return false; } diff --git a/server/src/immich/controllers/activity.controller.ts b/server/src/immich/controllers/activity.controller.ts index d6c2ea7629..d01d4e903a 100644 --- a/server/src/immich/controllers/activity.controller.ts +++ b/server/src/immich/controllers/activity.controller.ts @@ -1,4 +1,4 @@ -import { AuthUserDto } from '@app/domain'; +import { AuthDto } from '@app/domain'; import { ActivityDto, ActivitySearchDto, @@ -10,7 +10,7 @@ import { import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -22,22 +22,22 @@ export class ActivityController { constructor(private service: ActivityService) {} @Get() - getActivities(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivitySearchDto): Promise { - return this.service.getAll(authUser, dto); + getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { + return this.service.getAll(auth, dto); } @Get('statistics') - getActivityStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivityDto): Promise { - return this.service.getStatistics(authUser, dto); + getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { + return this.service.getStatistics(auth, dto); } @Post() async createActivity( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Body() dto: CreateDto, @Res({ passthrough: true }) res: Response, ): Promise { - const { duplicate, value } = await this.service.create(authUser, dto); + const { duplicate, value } = await this.service.create(auth, dto); if (duplicate) { res.status(HttpStatus.OK); } @@ -46,7 +46,7 @@ export class ActivityController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - deleteActivity(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.delete(authUser, id); + deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); } } diff --git a/server/src/immich/controllers/album.controller.ts b/server/src/immich/controllers/album.controller.ts index 4216ca331e..d388dba771 100644 --- a/server/src/immich/controllers/album.controller.ts +++ b/server/src/immich/controllers/album.controller.ts @@ -4,7 +4,7 @@ import { AlbumInfoDto, AlbumResponseDto, AlbumService, - AuthUserDto, + AuthDto, BulkIdResponseDto, BulkIdsDto, CreateAlbumDto as CreateDto, @@ -14,7 +14,7 @@ import { import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe'; -import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard'; +import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -26,78 +26,78 @@ export class AlbumController { constructor(private service: AlbumService) {} @Get('count') - getAlbumCount(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getCount(authUser); + getAlbumCount(@Auth() auth: AuthDto): Promise { + return this.service.getCount(auth); } @Get() - getAllAlbums(@AuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto): Promise { - return this.service.getAll(authUser, query); + getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { + return this.service.getAll(auth, query); } @Post() - createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise { - return this.service.create(authUser, dto); + createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise { + return this.service.create(auth, dto); } @SharedLinkRoute() @Get(':id') getAlbumInfo( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Query() dto: AlbumInfoDto, ): Promise { - return this.service.get(authUser, id, dto); + return this.service.get(auth, id, dto); } @Patch(':id') updateAlbumInfo( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto, ): Promise { - return this.service.update(authUser, id, dto); + return this.service.update(auth, id, dto); } @Delete(':id') - deleteAlbum(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { - return this.service.delete(authUser, id); + deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { + return this.service.delete(auth, id); } @SharedLinkRoute() @Put(':id/assets') addAssetsToAlbum( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: BulkIdsDto, ): Promise { - return this.service.addAssets(authUser, id, dto); + return this.service.addAssets(auth, id, dto); } @Delete(':id/assets') removeAssetFromAlbum( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Body() dto: BulkIdsDto, @Param() { id }: UUIDParamDto, ): Promise { - return this.service.removeAssets(authUser, id, dto); + return this.service.removeAssets(auth, id, dto); } @Put(':id/users') addUsersToAlbum( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto, ): Promise { - return this.service.addUsers(authUser, id, dto); + return this.service.addUsers(auth, id, dto); } @Delete(':id/user/:userId') removeUserFromAlbum( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string, ) { - return this.service.removeUser(authUser, id, userId); + return this.service.removeUser(auth, id, userId); } } diff --git a/server/src/immich/controllers/api-key.controller.ts b/server/src/immich/controllers/api-key.controller.ts index 10e0394cf5..ba0aaf661a 100644 --- a/server/src/immich/controllers/api-key.controller.ts +++ b/server/src/immich/controllers/api-key.controller.ts @@ -4,11 +4,11 @@ import { APIKeyResponseDto, APIKeyService, APIKeyUpdateDto, - AuthUserDto, + AuthDto, } from '@app/domain'; import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -20,31 +20,31 @@ export class APIKeyController { constructor(private service: APIKeyService) {} @Post() - createApiKey(@AuthUser() authUser: AuthUserDto, @Body() dto: APIKeyCreateDto): Promise { - return this.service.create(authUser, dto); + createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { + return this.service.create(auth, dto); } @Get() - getApiKeys(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getAll(authUser); + getApiKeys(@Auth() auth: AuthDto): Promise { + return this.service.getAll(auth); } @Get(':id') - getApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getById(authUser, id); + getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getById(auth, id); } @Put(':id') updateApiKey( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: APIKeyUpdateDto, ): Promise { - return this.service.update(authUser, id, dto); + return this.service.update(auth, id, dto); } @Delete(':id') - deleteApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.delete(authUser, id); + deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); } } diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 3a652c2e50..ae72d39922 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -8,7 +8,7 @@ import { AssetService, AssetStatsDto, AssetStatsResponseDto, - AuthUserDto, + AuthDto, BulkIdsDto, DownloadInfoDto, DownloadResponseDto, @@ -39,7 +39,7 @@ import { } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { DeviceIdDto } from '../api-v1/asset/dto/device-id.dto'; -import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard'; +import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; import { UseValidation, asStreamableFile } from '../app.utils'; import { Route } from '../interceptors'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -52,8 +52,8 @@ export class AssetsController { constructor(private service: AssetService) {} @Get() - searchAssets(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetSearchDto): Promise { - return this.service.search(authUser, dto); + searchAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { + return this.service.search(auth, dto); } } @@ -65,115 +65,111 @@ export class AssetController { constructor(private service: AssetService) {} @Get('map-marker') - getMapMarkers(@AuthUser() authUser: AuthUserDto, @Query() options: MapMarkerDto): Promise { - return this.service.getMapMarkers(authUser, options); + getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise { + return this.service.getMapMarkers(auth, options); } @Get('memory-lane') - getMemoryLane(@AuthUser() authUser: AuthUserDto, @Query() dto: MemoryLaneDto): Promise { - return this.service.getMemoryLane(authUser, dto); + getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise { + return this.service.getMemoryLane(auth, dto); } @Get('random') - getRandom(@AuthUser() authUser: AuthUserDto, @Query() dto: RandomAssetsDto): Promise { - return this.service.getRandom(authUser, dto.count ?? 1); + getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise { + return this.service.getRandom(auth, dto.count ?? 1); } @SharedLinkRoute() @Post('download/info') - getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Body() dto: DownloadInfoDto): Promise { - return this.service.getDownloadInfo(authUser, dto); + getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise { + return this.service.getDownloadInfo(auth, dto); } @SharedLinkRoute() @Post('download/archive') @HttpCode(HttpStatus.OK) @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) - downloadArchive(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetIdsDto): Promise { - return this.service.downloadArchive(authUser, dto).then(asStreamableFile); + downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise { + return this.service.downloadArchive(auth, dto).then(asStreamableFile); } @SharedLinkRoute() @Post('download/:id') @HttpCode(HttpStatus.OK) @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) - downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { - return this.service.downloadFile(authUser, id).then(asStreamableFile); + downloadFile(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { + return this.service.downloadFile(auth, id).then(asStreamableFile); } /** * Get all asset of a device that are in the database, ID only. */ @Get('/device/:deviceId') - getAllUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { - return this.service.getUserAssetsByDeviceId(authUser, deviceId); + getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) { + return this.service.getUserAssetsByDeviceId(auth, deviceId); } @Get('statistics') - getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise { - return this.service.getStatistics(authUser, dto); + getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise { + return this.service.getStatistics(auth, dto); } @Authenticated({ isShared: true }) @Get('time-buckets') - getTimeBuckets(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketDto): Promise { - return this.service.getTimeBuckets(authUser, dto); + getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise { + return this.service.getTimeBuckets(auth, dto); } @Authenticated({ isShared: true }) @Get('time-bucket') - getTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise { - return this.service.getTimeBucket(authUser, dto) as Promise; + getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise { + return this.service.getTimeBucket(auth, dto) as Promise; } @Post('jobs') @HttpCode(HttpStatus.NO_CONTENT) - runAssetJobs(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetJobsDto): Promise { - return this.service.run(authUser, dto); + runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise { + return this.service.run(auth, dto); } @Put() @HttpCode(HttpStatus.NO_CONTENT) - updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise { - return this.service.updateAll(authUser, dto); + updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise { + return this.service.updateAll(auth, dto); } @Delete() @HttpCode(HttpStatus.NO_CONTENT) - deleteAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkDeleteDto): Promise { - return this.service.deleteAll(authUser, dto); + deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise { + return this.service.deleteAll(auth, dto); } @Post('restore') @HttpCode(HttpStatus.NO_CONTENT) - restoreAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: BulkIdsDto): Promise { - return this.service.restoreAll(authUser, dto); + restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + return this.service.restoreAll(auth, dto); } @Post('trash/empty') @HttpCode(HttpStatus.NO_CONTENT) - emptyTrash(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.handleTrashAction(authUser, TrashAction.EMPTY_ALL); + emptyTrash(@Auth() auth: AuthDto): Promise { + return this.service.handleTrashAction(auth, TrashAction.EMPTY_ALL); } @Post('trash/restore') @HttpCode(HttpStatus.NO_CONTENT) - restoreTrash(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.handleTrashAction(authUser, TrashAction.RESTORE_ALL); + restoreTrash(@Auth() auth: AuthDto): Promise { + return this.service.handleTrashAction(auth, TrashAction.RESTORE_ALL); } @Put('stack/parent') @HttpCode(HttpStatus.OK) - updateStackParent(@AuthUser() authUser: AuthUserDto, @Body() dto: UpdateStackParentDto): Promise { - return this.service.updateStackParent(authUser, dto); + updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise { + return this.service.updateStackParent(auth, dto); } @Put(':id') - updateAsset( - @AuthUser() authUser: AuthUserDto, - @Param() { id }: UUIDParamDto, - @Body() dto: UpdateDto, - ): Promise { - return this.service.update(authUser, id, dto); + updateAsset(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise { + return this.service.update(auth, id, dto); } } diff --git a/server/src/immich/controllers/audit.controller.ts b/server/src/immich/controllers/audit.controller.ts index a50d33741d..785d48bd25 100644 --- a/server/src/immich/controllers/audit.controller.ts +++ b/server/src/immich/controllers/audit.controller.ts @@ -2,7 +2,7 @@ import { AuditDeletesDto, AuditDeletesResponseDto, AuditService, - AuthUserDto, + AuthDto, FileChecksumDto, FileChecksumResponseDto, FileReportDto, @@ -10,7 +10,7 @@ import { } from '@app/domain'; import { Body, Controller, Get, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AdminRoute, AuthUser, Authenticated } from '../app.guard'; +import { AdminRoute, Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @ApiTags('Audit') @@ -21,8 +21,8 @@ export class AuditController { constructor(private service: AuditService) {} @Get('deletes') - getAuditDeletes(@AuthUser() authUser: AuthUserDto, @Query() dto: AuditDeletesDto): Promise { - return this.service.getDeletes(authUser, dto); + getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise { + return this.service.getDeletes(auth, dto); } @AdminRoute() diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/immich/controllers/auth.controller.ts index dda546cf04..38cf8f23dc 100644 --- a/server/src/immich/controllers/auth.controller.ts +++ b/server/src/immich/controllers/auth.controller.ts @@ -1,7 +1,7 @@ import { AuthDeviceResponseDto, + AuthDto, AuthService, - AuthUserDto, ChangePasswordDto, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, @@ -17,7 +17,7 @@ import { import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { AuthUser, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; +import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -47,20 +47,20 @@ export class AuthController { } @Get('devices') - getAuthDevices(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getDevices(authUser); + getAuthDevices(@Auth() auth: AuthDto): Promise { + return this.service.getDevices(auth); } @Delete('devices') @HttpCode(HttpStatus.NO_CONTENT) - logoutAuthDevices(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.logoutDevices(authUser); + logoutAuthDevices(@Auth() auth: AuthDto): Promise { + return this.service.logoutDevices(auth); } @Delete('devices/:id') @HttpCode(HttpStatus.NO_CONTENT) - logoutAuthDevice(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.logoutDevice(authUser, id); + logoutAuthDevice(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.logoutDevice(auth, id); } @Post('validateToken') @@ -71,8 +71,8 @@ export class AuthController { @Post('change-password') @HttpCode(HttpStatus.OK) - changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise { - return this.service.changePassword(authUser, dto).then(mapUser); + changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise { + return this.service.changePassword(auth, dto).then(mapUser); } @Post('logout') @@ -80,11 +80,11 @@ export class AuthController { logout( @Req() req: Request, @Res({ passthrough: true }) res: Response, - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, ): Promise { res.clearCookie(IMMICH_ACCESS_COOKIE); res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); - return this.service.logout(authUser, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); + return this.service.logout(auth, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]); } } diff --git a/server/src/immich/controllers/face.controller.ts b/server/src/immich/controllers/face.controller.ts index 5fd2dff276..59473950c1 100644 --- a/server/src/immich/controllers/face.controller.ts +++ b/server/src/immich/controllers/face.controller.ts @@ -1,7 +1,7 @@ -import { AssetFaceResponseDto, AuthUserDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain'; +import { AssetFaceResponseDto, AuthDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain'; import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -13,16 +13,16 @@ export class FaceController { constructor(private service: PersonService) {} @Get() - getFaces(@AuthUser() authUser: AuthUserDto, @Query() dto: FaceDto): Promise { - return this.service.getFacesById(authUser, dto); + getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise { + return this.service.getFacesById(auth, dto); } @Put(':id') reassignFacesById( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: FaceDto, ): Promise { - return this.service.reassignFacesById(authUser, id, dto); + return this.service.reassignFacesById(auth, id, dto); } } diff --git a/server/src/immich/controllers/library.controller.ts b/server/src/immich/controllers/library.controller.ts index ad1676ee0a..56dd5d8e76 100644 --- a/server/src/immich/controllers/library.controller.ts +++ b/server/src/immich/controllers/library.controller.ts @@ -1,5 +1,5 @@ import { - AuthUserDto, + AuthDto, CreateLibraryDto as CreateDto, LibraryService, LibraryStatsResponseDto, @@ -9,7 +9,7 @@ import { } from '@app/domain'; import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -21,49 +21,42 @@ export class LibraryController { constructor(private service: LibraryService) {} @Get() - getLibraries(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getAllForUser(authUser); + getLibraries(@Auth() auth: AuthDto): Promise { + return this.service.getAllForUser(auth); } @Post() - createLibrary(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise { - return this.service.create(authUser, dto); + createLibrary(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise { + return this.service.create(auth, dto); } @Put(':id') - updateLibrary( - @AuthUser() authUser: AuthUserDto, - @Param() { id }: UUIDParamDto, - @Body() dto: UpdateDto, - ): Promise { - return this.service.update(authUser, id, dto); + updateLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise { + return this.service.update(auth, id, dto); } @Get(':id') - getLibraryInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.get(authUser, id); + getLibraryInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); } @Delete(':id') - deleteLibrary(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.delete(authUser, id); + deleteLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); } @Get(':id/statistics') - getLibraryStatistics( - @AuthUser() authUser: AuthUserDto, - @Param() { id }: UUIDParamDto, - ): Promise { - return this.service.getStatistics(authUser, id); + getLibraryStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getStatistics(auth, id); } @Post(':id/scan') - scanLibrary(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) { - return this.service.queueScan(authUser, id, dto); + scanLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) { + return this.service.queueScan(auth, id, dto); } @Post(':id/removeOffline') - removeOfflineFiles(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { - return this.service.queueRemoveOffline(authUser, id); + removeOfflineFiles(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { + return this.service.queueRemoveOffline(auth, id); } } diff --git a/server/src/immich/controllers/oauth.controller.ts b/server/src/immich/controllers/oauth.controller.ts index a55cc03ce0..b7fd0fe021 100644 --- a/server/src/immich/controllers/oauth.controller.ts +++ b/server/src/immich/controllers/oauth.controller.ts @@ -1,6 +1,6 @@ import { + AuthDto, AuthService, - AuthUserDto, LoginDetails, LoginResponseDto, OAuthAuthorizeResponseDto, @@ -12,7 +12,7 @@ import { import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { AuthUser, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; +import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; import { UseValidation } from '../app.utils'; @ApiTags('OAuth') @@ -58,12 +58,12 @@ export class OAuthController { } @Post('link') - linkOAuthAccount(@AuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise { - return this.service.link(authUser, dto); + linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise { + return this.service.link(auth, dto); } @Post('unlink') - unlinkOAuthAccount(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.unlink(authUser); + unlinkOAuthAccount(@Auth() auth: AuthDto): Promise { + return this.service.unlink(auth); } } diff --git a/server/src/immich/controllers/partner.controller.ts b/server/src/immich/controllers/partner.controller.ts index 5f9f004f90..75f716f58d 100644 --- a/server/src/immich/controllers/partner.controller.ts +++ b/server/src/immich/controllers/partner.controller.ts @@ -1,8 +1,8 @@ -import { AuthUserDto, PartnerDirection, PartnerService } from '@app/domain'; +import { AuthDto, PartnerDirection, PartnerService } from '@app/domain'; import { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partner.dto'; import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiQuery, ApiTags } from '@nestjs/swagger'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -15,29 +15,26 @@ export class PartnerController { @Get() @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) - getPartners( - @AuthUser() authUser: AuthUserDto, - @Query('direction') direction: PartnerDirection, - ): Promise { - return this.service.getAll(authUser, direction); + getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise { + return this.service.getAll(auth, direction); } @Post(':id') - createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.create(authUser, id); + createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.create(auth, id); } @Put(':id') updatePartner( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdatePartnerDto, ): Promise { - return this.service.update(authUser, id, dto); + return this.service.update(auth, id, dto); } @Delete(':id') - removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.remove(authUser, id); + removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.remove(auth, id); } } diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index 51f222c7d6..da1bd88ff0 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -1,7 +1,7 @@ import { AssetFaceUpdateDto, AssetResponseDto, - AuthUserDto, + AuthDto, BulkIdResponseDto, ImmichReadStream, MergePersonDto, @@ -15,7 +15,7 @@ import { } from '@app/domain'; import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -31,49 +31,46 @@ export class PersonController { constructor(private service: PersonService) {} @Get() - getAllPeople(@AuthUser() authUser: AuthUserDto, @Query() withHidden: PersonSearchDto): Promise { - return this.service.getAll(authUser, withHidden); + getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise { + return this.service.getAll(auth, withHidden); } @Post() - createPerson(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.createPerson(authUser); + createPerson(@Auth() auth: AuthDto): Promise { + return this.service.createPerson(auth); } @Put(':id/reassign') reassignFaces( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceUpdateDto, ): Promise { - return this.service.reassignFaces(authUser, id, dto); + return this.service.reassignFaces(auth, id, dto); } @Put() - updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise { - return this.service.updatePeople(authUser, dto); + updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise { + return this.service.updatePeople(auth, dto); } @Get(':id') - getPerson(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getById(authUser, id); + getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getById(auth, id); } @Put(':id') updatePerson( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: PersonUpdateDto, ): Promise { - return this.service.update(authUser, id, dto); + return this.service.update(auth, id, dto); } @Get(':id/statistics') - getPersonStatistics( - @AuthUser() authUser: AuthUserDto, - @Param() { id }: UUIDParamDto, - ): Promise { - return this.service.getStatistics(authUser, id); + getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getStatistics(auth, id); } @Get(':id/thumbnail') @@ -82,21 +79,21 @@ export class PersonController { 'image/jpeg': { schema: { type: 'string', format: 'binary' } }, }, }) - getPersonThumbnail(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { - return this.service.getThumbnail(authUser, id).then(asStreamableFile); + getPersonThumbnail(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { + return this.service.getThumbnail(auth, id).then(asStreamableFile); } @Get(':id/assets') - getPersonAssets(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getAssets(authUser, id); + getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getAssets(auth, id); } @Post(':id/merge') mergePerson( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: MergePersonDto, ): Promise { - return this.service.mergePerson(authUser, id, dto); + return this.service.mergePerson(auth, id, dto); } } diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index b3de4b26c5..51ee900eec 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -1,5 +1,5 @@ import { - AuthUserDto, + AuthDto, PersonResponseDto, SearchDto, SearchExploreResponseDto, @@ -9,7 +9,7 @@ import { } from '@app/domain'; import { Controller, Get, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @ApiTags('Search') @@ -20,17 +20,17 @@ export class SearchController { constructor(private service: SearchService) {} @Get() - search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise { - return this.service.search(authUser, dto); + search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise { + return this.service.search(auth, dto); } @Get('explore') - getExploreData(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getExploreData(authUser) as Promise; + getExploreData(@Auth() auth: AuthDto): Promise { + return this.service.getExploreData(auth) as Promise; } @Get('person') - searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise { - return this.service.searchPerson(authUser, dto); + searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise { + return this.service.searchPerson(auth, dto); } } diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/immich/controllers/shared-link.controller.ts index 15c0803dd4..25d4bdca46 100644 --- a/server/src/immich/controllers/shared-link.controller.ts +++ b/server/src/immich/controllers/shared-link.controller.ts @@ -1,7 +1,7 @@ import { AssetIdsDto, AssetIdsResponseDto, - AuthUserDto, + AuthDto, IMMICH_SHARED_LINK_ACCESS_COOKIE, SharedLinkCreateDto, SharedLinkEditDto, @@ -12,7 +12,7 @@ import { import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard'; +import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -24,14 +24,14 @@ export class SharedLinkController { constructor(private readonly service: SharedLinkService) {} @Get() - getAllSharedLinks(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getAll(authUser); + getAllSharedLinks(@Auth() auth: AuthDto): Promise { + return this.service.getAll(auth); } @SharedLinkRoute() @Get('me') async getMySharedLink( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Query() dto: SharedLinkPasswordDto, @Req() req: Request, @Res({ passthrough: true }) res: Response, @@ -40,58 +40,58 @@ export class SharedLinkController { if (sharedLinkToken) { dto.token = sharedLinkToken; } - const sharedLinkResponse = await this.service.getMine(authUser, dto); - if (sharedLinkResponse.token) { - res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, sharedLinkResponse.token, { + const response = await this.service.getMine(auth, dto); + if (response.token) { + res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, { expires: new Date(Date.now() + 1000 * 60 * 60 * 24), httpOnly: true, sameSite: 'lax', }); } - return sharedLinkResponse; + return response; } @Get(':id') - getSharedLinkById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.get(authUser, id); + getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); } @Post() - createSharedLink(@AuthUser() authUser: AuthUserDto, @Body() dto: SharedLinkCreateDto) { - return this.service.create(authUser, dto); + createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { + return this.service.create(auth, dto); } @Patch(':id') updateSharedLink( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: SharedLinkEditDto, ): Promise { - return this.service.update(authUser, id, dto); + return this.service.update(auth, id, dto); } @Delete(':id') - removeSharedLink(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.remove(authUser, id); + removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.remove(auth, id); } @SharedLinkRoute() @Put(':id/assets') addSharedLinkAssets( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetIdsDto, ): Promise { - return this.service.addAssets(authUser, id, dto); + return this.service.addAssets(auth, id, dto); } @SharedLinkRoute() @Delete(':id/assets') removeSharedLinkAssets( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetIdsDto, ): Promise { - return this.service.removeAssets(authUser, id, dto); + return this.service.removeAssets(auth, id, dto); } } diff --git a/server/src/immich/controllers/tag.controller.ts b/server/src/immich/controllers/tag.controller.ts index 8558601315..b9d3636008 100644 --- a/server/src/immich/controllers/tag.controller.ts +++ b/server/src/immich/controllers/tag.controller.ts @@ -2,7 +2,7 @@ import { AssetIdsDto, AssetIdsResponseDto, AssetResponseDto, - AuthUserDto, + AuthDto, CreateTagDto, TagResponseDto, TagService, @@ -10,7 +10,7 @@ import { } from '@app/domain'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AuthUser, Authenticated } from '../app.guard'; +import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -22,54 +22,50 @@ export class TagController { constructor(private service: TagService) {} @Post() - createTag(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateTagDto): Promise { - return this.service.create(authUser, dto); + createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise { + return this.service.create(auth, dto); } @Get() - getAllTags(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getAll(authUser); + getAllTags(@Auth() auth: AuthDto): Promise { + return this.service.getAll(auth); } @Get(':id') - getTagById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getById(authUser, id); + getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getById(auth, id); } @Patch(':id') - updateTag( - @AuthUser() authUser: AuthUserDto, - @Param() { id }: UUIDParamDto, - @Body() dto: UpdateTagDto, - ): Promise { - return this.service.update(authUser, id, dto); + updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise { + return this.service.update(auth, id, dto); } @Delete(':id') - deleteTag(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.remove(authUser, id); + deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.remove(auth, id); } @Get(':id/assets') - getTagAssets(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getAssets(authUser, id); + getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getAssets(auth, id); } @Put(':id/assets') tagAssets( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetIdsDto, ): Promise { - return this.service.addAssets(authUser, id, dto); + return this.service.addAssets(auth, id, dto); } @Delete(':id/assets') untagAssets( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @Body() dto: AssetIdsDto, @Param() { id }: UUIDParamDto, ): Promise { - return this.service.removeAssets(authUser, id, dto); + return this.service.removeAssets(auth, id, dto); } } diff --git a/server/src/immich/controllers/user.controller.ts b/server/src/immich/controllers/user.controller.ts index 1772fb5481..64b6da8516 100644 --- a/server/src/immich/controllers/user.controller.ts +++ b/server/src/immich/controllers/user.controller.ts @@ -1,5 +1,5 @@ import { - AuthUserDto, + AuthDto, CreateUserDto as CreateDto, CreateProfileImageDto, CreateProfileImageResponseDto, @@ -23,7 +23,7 @@ import { UseInterceptors, } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; -import { AdminRoute, AuthUser, Authenticated } from '../app.guard'; +import { AdminRoute, Auth, Authenticated } from '../app.guard'; import { UseValidation, asStreamableFile } from '../app.utils'; import { FileUploadInterceptor, Route } from '../interceptors'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -36,8 +36,8 @@ export class UserController { constructor(private service: UserService) {} @Get() - getAllUsers(@AuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean): Promise { - return this.service.getAll(authUser, isAll); + getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise { + return this.service.getAll(auth, isAll); } @Get('info/:id') @@ -46,8 +46,8 @@ export class UserController { } @Get('me') - getMyUserInfo(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.getMe(authUser); + getMyUserInfo(@Auth() auth: AuthDto): Promise { + return this.service.getMe(auth); } @AdminRoute() @@ -58,26 +58,26 @@ export class UserController { @Delete('profile-image') @HttpCode(HttpStatus.NO_CONTENT) - deleteProfileImage(@AuthUser() authUser: AuthUserDto): Promise { - return this.service.deleteProfileImage(authUser); + deleteProfileImage(@Auth() auth: AuthDto): Promise { + return this.service.deleteProfileImage(auth); } @AdminRoute() @Delete(':id') - deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.delete(authUser, id); + deleteUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); } @AdminRoute() @Post(':id/restore') - restoreUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.restore(authUser, id); + restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.restore(auth, id); } // TODO: replace with @Put(':id') @Put() - updateUser(@AuthUser() authUser: AuthUserDto, @Body() updateUserDto: UpdateDto): Promise { - return this.service.update(authUser, updateUserDto); + updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateDto): Promise { + return this.service.update(auth, updateUserDto); } @UseInterceptors(FileUploadInterceptor) @@ -85,10 +85,10 @@ export class UserController { @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @Post('profile-image') createProfileImage( - @AuthUser() authUser: AuthUserDto, + @Auth() auth: AuthDto, @UploadedFile() fileInfo: Express.Multer.File, ): Promise { - return this.service.createProfileImage(authUser, fileInfo); + return this.service.createProfileImage(auth, fileInfo); } @Get('profile-image/:id') diff --git a/server/src/immich/interceptors/file.interceptor.ts b/server/src/immich/interceptors/file.interceptor.ts index 7388df457e..0fb59014d9 100644 --- a/server/src/immich/interceptors/file.interceptor.ts +++ b/server/src/immich/interceptors/file.interceptor.ts @@ -44,7 +44,7 @@ const callbackify = async (fn: (...args: any[]) => T, callback: Callback) const asRequest = (req: AuthRequest, file: Express.Multer.File) => { return { - authUser: req.user || null, + auth: req.user || null, fieldName: file.fieldname as UploadFieldName, file: mapToUploadFile(file as ImmichFile), }; diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts index 3eabaa86af..3ce0ccc496 100644 --- a/server/src/infra/repositories/communication.repository.ts +++ b/server/src/infra/repositories/communication.repository.ts @@ -19,10 +19,10 @@ export class CommunicationRepository implements OnGatewayConnection, OnGatewayDi async handleConnection(client: Socket) { try { this.logger.log(`Websocket Connect: ${client.id}`); - const user = await this.authService.validate(client.request.headers, {}); - await client.join(user.id); + const auth = await this.authService.validate(client.request.headers, {}); + await client.join(auth.user.id); for (const callback of this.onConnectCallbacks) { - await callback(user.id); + await callback(auth.user.id); } } catch (error: Error | any) { this.logger.error(`Websocket connection error: ${error}`, error?.stack); diff --git a/server/test/fixtures/activity.stub.ts b/server/test/fixtures/activity.stub.ts index 91c699cec3..7a3aae5ffb 100644 --- a/server/test/fixtures/activity.stub.ts +++ b/server/test/fixtures/activity.stub.ts @@ -9,7 +9,7 @@ export const activityStub = { id: 'activity-1', comment: 'comment', isLiked: false, - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, assetId: assetStub.image.id, asset: assetStub.image, @@ -22,7 +22,7 @@ export const activityStub = { id: 'activity-2', comment: null, isLiked: true, - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, assetId: assetStub.image.id, asset: assetStub.image, diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index fd4464d191..2fdc5b5dd4 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -8,7 +8,7 @@ export const albumStub = { id: 'album-1', albumName: 'Empty album', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [], albumThumbnailAsset: null, @@ -24,7 +24,7 @@ export const albumStub = { id: 'album-2', albumName: 'Empty album shared with user', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [], albumThumbnailAsset: null, @@ -40,7 +40,7 @@ export const albumStub = { id: 'album-3', albumName: 'Empty album shared with users', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [], albumThumbnailAsset: null, @@ -56,7 +56,7 @@ export const albumStub = { id: 'album-3', albumName: 'Empty album shared with admin', description: '', - ownerId: authStub.user1.id, + ownerId: authStub.user1.user.id, owner: userStub.user1, assets: [], albumThumbnailAsset: null, @@ -72,7 +72,7 @@ export const albumStub = { id: 'album-4', albumName: 'Album with one asset', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [assetStub.image], albumThumbnailAsset: null, @@ -88,7 +88,7 @@ export const albumStub = { id: 'album-4a', albumName: 'Album with two assets', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [assetStub.image, assetStub.withLocation], albumThumbnailAsset: assetStub.image, @@ -104,7 +104,7 @@ export const albumStub = { id: 'album-5', albumName: 'Empty album with invalid thumbnail', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [], albumThumbnailAsset: assetStub.image, @@ -120,7 +120,7 @@ export const albumStub = { id: 'album-5', albumName: 'Empty album with invalid thumbnail', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [], albumThumbnailAsset: null, @@ -136,7 +136,7 @@ export const albumStub = { id: 'album-6', albumName: 'Album with one asset and invalid thumbnail', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [assetStub.image], albumThumbnailAsset: assetStub.livePhotoMotionAsset, @@ -152,7 +152,7 @@ export const albumStub = { id: 'album-6', albumName: 'Album with one asset and invalid thumbnail', description: '', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, assets: [assetStub.image], albumThumbnailAsset: assetStub.image, diff --git a/server/test/fixtures/api-key.stub.ts b/server/test/fixtures/api-key.stub.ts index 36554ef688..582afcfb59 100644 --- a/server/test/fixtures/api-key.stub.ts +++ b/server/test/fixtures/api-key.stub.ts @@ -7,7 +7,7 @@ export const keyStub = { id: 'my-random-guid', name: 'My Key', key: 'my-api-key (hashed)', - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, } as APIKeyEntity), }; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 4c6288c408..a4a2208c22 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -403,7 +403,7 @@ export const assetStub = { livePhotoMotionAsset: Object.freeze({ id: 'live-photo-motion-asset', originalPath: fileStub.livePhotoMotion.originalPath, - ownerId: authStub.user1.id, + ownerId: authStub.user1.user.id, type: AssetType.VIDEO, isVisible: false, fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), @@ -418,7 +418,7 @@ export const assetStub = { livePhotoStillAsset: Object.freeze({ id: 'live-photo-still-asset', originalPath: fileStub.livePhotoStill.originalPath, - ownerId: authStub.user1.id, + ownerId: authStub.user1.user.id, type: AssetType.IMAGE, livePhotoVideoId: 'live-photo-motion-asset', isVisible: true, diff --git a/server/test/fixtures/audit.stub.ts b/server/test/fixtures/audit.stub.ts index c915ed8214..ab1ca98b9d 100644 --- a/server/test/fixtures/audit.stub.ts +++ b/server/test/fixtures/audit.stub.ts @@ -7,7 +7,7 @@ export const auditStub = { entityId: 'asset-created', action: DatabaseAction.CREATE, entityType: EntityType.ASSET, - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, createdAt: new Date(), }), update: Object.freeze({ @@ -15,7 +15,7 @@ export const auditStub = { entityId: 'asset-updated', action: DatabaseAction.UPDATE, entityType: EntityType.ASSET, - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, createdAt: new Date(), }), delete: Object.freeze({ @@ -23,7 +23,7 @@ export const auditStub = { entityId: 'asset-deleted', action: DatabaseAction.DELETE, entityType: EntityType.ASSET, - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, createdAt: new Date(), }), }; diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index d4b6368960..1a24d8cc17 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -1,4 +1,5 @@ -import { AuthUserDto } from '@app/domain'; +import { AuthDto } from '@app/domain'; +import { SharedLinkEntity, UserEntity, UserTokenEntity } from '../../src/infra/entities'; export const adminSignupStub = { name: 'Immich Admin', @@ -24,77 +25,84 @@ export const changePasswordStub = { }; export const authStub = { - admin: Object.freeze({ - id: 'admin_id', - email: 'admin@test.com', - isAdmin: true, - isPublicUser: false, - isAllowUpload: true, - externalPath: null, + admin: Object.freeze({ + user: { + id: 'admin_id', + email: 'admin@test.com', + isAdmin: true, + } as UserEntity, }), - user1: Object.freeze({ - id: 'user-id', - email: 'immich@test.com', - isAdmin: false, - isPublicUser: false, - isAllowUpload: true, - isAllowDownload: true, - isShowMetadata: true, - accessTokenId: 'token-id', - externalPath: null, + user1: Object.freeze({ + user: { + id: 'user-id', + email: 'immich@test.com', + isAdmin: false, + } as UserEntity, + userToken: { + id: 'token-id', + } as UserTokenEntity, }), - user2: Object.freeze({ - id: 'user-2', - email: 'user2@immich.app', - isAdmin: false, - isPublicUser: false, - isAllowUpload: true, - isAllowDownload: true, - isShowMetadata: true, - accessTokenId: 'token-id', - externalPath: null, + user2: Object.freeze({ + user: { + id: 'user-2', + email: 'user2@immich.app', + isAdmin: false, + } as UserEntity, + userToken: { + id: 'token-id', + } as UserTokenEntity, }), - external1: Object.freeze({ - id: 'user-id', - email: 'immich@test.com', - isAdmin: false, - isPublicUser: false, - isAllowUpload: true, - isAllowDownload: true, - isShowMetadata: true, - accessTokenId: 'token-id', - externalPath: '/data/user1', + external1: Object.freeze({ + user: { + id: 'user-id', + email: 'immich@test.com', + isAdmin: false, + externalPath: '/data/user1', + } as UserEntity, + userToken: { + id: 'token-id', + } as UserTokenEntity, }), - adminSharedLink: Object.freeze({ - id: 'admin_id', - email: 'admin@test.com', - isAdmin: true, - isAllowUpload: true, - isAllowDownload: true, - isPublicUser: true, - isShowMetadata: true, - sharedLinkId: '123', + adminSharedLink: Object.freeze({ + user: { + id: 'admin_id', + email: 'admin@test.com', + isAdmin: true, + } as UserEntity, + sharedLink: { + id: '123', + showExif: true, + allowDownload: true, + allowUpload: true, + key: Buffer.from('shared-link-key'), + } as SharedLinkEntity, }), - adminSharedLinkNoExif: Object.freeze({ - id: 'admin_id', - email: 'admin@test.com', - isAdmin: true, - isAllowUpload: true, - isAllowDownload: true, - isPublicUser: true, - isShowMetadata: false, - sharedLinkId: '123', + adminSharedLinkNoExif: Object.freeze({ + user: { + id: 'admin_id', + email: 'admin@test.com', + isAdmin: true, + } as UserEntity, + sharedLink: { + id: '123', + showExif: false, + allowDownload: true, + allowUpload: true, + key: Buffer.from('shared-link-key'), + } as SharedLinkEntity, }), - readonlySharedLink: Object.freeze({ - id: 'admin_id', - email: 'admin@test.com', - isAdmin: true, - isAllowUpload: false, - isAllowDownload: false, - isPublicUser: true, - isShowMetadata: true, - sharedLinkId: '123', - accessTokenId: 'token-id', + readonlySharedLink: Object.freeze({ + user: { + id: 'admin_id', + email: 'admin@test.com', + isAdmin: true, + } as UserEntity, + sharedLink: { + id: '123', + allowUpload: false, + allowDownload: false, + showExif: true, + } as SharedLinkEntity, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index a35b3dda7e..e4a487137e 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -106,7 +106,7 @@ const albumResponse: AlbumResponseDto = { export const sharedLinkStub = { individual: Object.freeze({ id: '123', - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, key: sharedLinkBytes, type: SharedLinkType.INDIVIDUAL, @@ -121,7 +121,7 @@ export const sharedLinkStub = { } as SharedLinkEntity), valid: Object.freeze({ id: '123', - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, key: sharedLinkBytes, type: SharedLinkType.ALBUM, @@ -138,7 +138,7 @@ export const sharedLinkStub = { } as SharedLinkEntity), expired: Object.freeze({ id: '123', - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, key: sharedLinkBytes, type: SharedLinkType.ALBUM, @@ -154,7 +154,7 @@ export const sharedLinkStub = { } as SharedLinkEntity), readonlyNoExif: Object.freeze({ id: '123', - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, key: sharedLinkBytes, type: SharedLinkType.ALBUM, @@ -169,7 +169,7 @@ export const sharedLinkStub = { albumId: 'album-123', album: { id: 'album-123', - ownerId: authStub.admin.id, + ownerId: authStub.admin.user.id, owner: userStub.admin, albumName: 'Test Album', description: '', @@ -260,7 +260,7 @@ export const sharedLinkStub = { }), passwordRequired: Object.freeze({ id: '123', - userId: authStub.admin.id, + userId: authStub.admin.user.id, user: userStub.admin, key: sharedLinkBytes, type: SharedLinkType.ALBUM, diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 9a03579af7..3837e67204 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -21,7 +21,7 @@ export const userDto = { export const userStub = { admin: Object.freeze({ - ...authStub.admin, + ...authStub.admin.user, password: 'admin_password', name: 'admin_name', storageLabel: 'admin', @@ -38,7 +38,7 @@ export const userStub = { avatarColor: UserAvatarColor.PRIMARY, }), user1: Object.freeze({ - ...authStub.user1, + ...authStub.user1.user, password: 'immich_password', name: 'immich_name', storageLabel: null, @@ -55,7 +55,7 @@ export const userStub = { avatarColor: UserAvatarColor.PRIMARY, }), user2: Object.freeze({ - ...authStub.user2, + ...authStub.user2.user, password: 'immich_password', name: 'immich_name', storageLabel: null, @@ -72,7 +72,7 @@ export const userStub = { avatarColor: UserAvatarColor.PRIMARY, }), storageLabel: Object.freeze({ - ...authStub.user1, + ...authStub.user1.user, password: 'immich_password', name: 'immich_name', storageLabel: 'label-1', @@ -89,7 +89,7 @@ export const userStub = { avatarColor: UserAvatarColor.PRIMARY, }), externalPath1: Object.freeze({ - ...authStub.user1, + ...authStub.user1.user, password: 'immich_password', name: 'immich_name', storageLabel: 'label-1', @@ -106,7 +106,7 @@ export const userStub = { avatarColor: UserAvatarColor.PRIMARY, }), externalPath2: Object.freeze({ - ...authStub.user1, + ...authStub.user1.user, password: 'immich_password', name: 'immich_name', storageLabel: 'label-1', @@ -123,7 +123,7 @@ export const userStub = { avatarColor: UserAvatarColor.PRIMARY, }), profilePath: Object.freeze({ - ...authStub.user1, + ...authStub.user1.user, password: 'immich_password', name: 'immich_name', storageLabel: 'label-1',