refactor(server): auth dto (#5593)

* refactor: AuthUserDto => AuthDto

* refactor: reorganize auth-dto

* refactor: AuthUser() => Auth()
This commit is contained in:
Jason Rasmussen 2023-12-09 23:34:12 -05:00 committed by GitHub
parent 8057c375ba
commit 33529d1d9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1033 additions and 1065 deletions

View File

@ -1,5 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common'; 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 { setDifference, setIsEqual, setUnion } from '../domain.util';
import { IAccessRepository } from '../repositories'; import { IAccessRepository } from '../repositories';
@ -64,20 +65,20 @@ export class AccessCore {
instance = null; instance = null;
} }
requireUploadAccess(authUser: AuthUserDto | null): AuthUserDto { requireUploadAccess(auth: AuthDto | null): AuthDto {
if (!authUser || (authUser.isPublicUser && !authUser.isAllowUpload)) { if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
return authUser; return auth;
} }
/** /**
* Check if user has access to all ids, for the given permission. * 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. * 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]; 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)) { if (!setIsEqual(new Set(ids), allowedIds)) {
throw new BadRequestException(`Not found or no ${permission} access`); throw new BadRequestException(`Not found or no ${permission} access`);
} }
@ -89,23 +90,21 @@ export class AccessCore {
* *
* @returns Set<string> * @returns Set<string>
*/ */
async checkAccess(authUser: AuthUserDto, permission: Permission, ids: Set<string> | string[]) { async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]) {
const idSet = Array.isArray(ids) ? new Set(ids) : ids; const idSet = Array.isArray(ids) ? new Set(ids) : ids;
if (idSet.size === 0) { if (idSet.size === 0) {
return new Set(); return new Set();
} }
const isSharedLink = authUser.isPublicUser ?? false; if (auth.sharedLink) {
return isSharedLink return this.checkAccessSharedLink(auth.sharedLink, permission, idSet);
? await this.checkAccessSharedLink(authUser, permission, idSet) }
: await this.checkAccessOther(authUser, permission, idSet);
return this.checkAccessOther(auth, permission, idSet);
} }
private async checkAccessSharedLink(authUser: AuthUserDto, permission: Permission, ids: Set<string>) { private async checkAccessSharedLink(sharedLink: SharedLinkEntity, permission: Permission, ids: Set<string>) {
const sharedLinkId = authUser.sharedLinkId; const sharedLinkId = sharedLink.id;
if (!sharedLinkId) {
return new Set();
}
switch (permission) { switch (permission) {
case Permission.ASSET_READ: case Permission.ASSET_READ:
@ -115,22 +114,22 @@ export class AccessCore {
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
case Permission.ASSET_DOWNLOAD: case Permission.ASSET_DOWNLOAD:
return !!authUser.isAllowDownload return !!sharedLink.allowDownload
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
: new Set(); : new Set();
case Permission.ASSET_UPLOAD: case Permission.ASSET_UPLOAD:
return authUser.isAllowUpload ? ids : new Set(); return sharedLink.allowUpload ? ids : new Set();
case Permission.ASSET_SHARE: case Permission.ASSET_SHARE:
// TODO: fix this to not use authUser.id for shared link access control // TODO: fix this to not use sharedLink.userId for access control
return await this.repository.asset.checkOwnerAccess(authUser.id, ids); return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids);
case Permission.ALBUM_READ: case Permission.ALBUM_READ:
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
case Permission.ALBUM_DOWNLOAD: case Permission.ALBUM_DOWNLOAD:
return !!authUser.isAllowDownload return !!sharedLink.allowDownload
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
: new Set(); : new Set();
@ -139,129 +138,129 @@ export class AccessCore {
} }
} }
private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set<string>) { private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>) {
switch (permission) { switch (permission) {
case Permission.ASSET_READ: { case Permission.ASSET_READ: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess( const isPartner = await this.repository.asset.checkPartnerAccess(
authUser.id, auth.user.id,
setDifference(ids, isOwner, isAlbum), setDifference(ids, isOwner, isAlbum),
); );
return setUnion(isOwner, isAlbum, isPartner); return setUnion(isOwner, isAlbum, isPartner);
} }
case Permission.ASSET_SHARE: { case Permission.ASSET_SHARE: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isPartner = await this.repository.asset.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner); return setUnion(isOwner, isPartner);
} }
case Permission.ASSET_VIEW: { case Permission.ASSET_VIEW: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess( const isPartner = await this.repository.asset.checkPartnerAccess(
authUser.id, auth.user.id,
setDifference(ids, isOwner, isAlbum), setDifference(ids, isOwner, isAlbum),
); );
return setUnion(isOwner, isAlbum, isPartner); return setUnion(isOwner, isAlbum, isPartner);
} }
case Permission.ASSET_DOWNLOAD: { case Permission.ASSET_DOWNLOAD: {
const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await this.repository.asset.checkPartnerAccess( const isPartner = await this.repository.asset.checkPartnerAccess(
authUser.id, auth.user.id,
setDifference(ids, isOwner, isAlbum), setDifference(ids, isOwner, isAlbum),
); );
return setUnion(isOwner, isAlbum, isPartner); return setUnion(isOwner, isAlbum, isPartner);
} }
case Permission.ASSET_UPDATE: 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: 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: 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: { case Permission.ALBUM_READ: {
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared); return setUnion(isOwner, isShared);
} }
case Permission.ALBUM_UPDATE: 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: 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: 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: { case Permission.ALBUM_DOWNLOAD: {
const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); const isShared = await this.repository.album.checkSharedAlbumAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isShared); return setUnion(isOwner, isShared);
} }
case Permission.ALBUM_REMOVE_ASSET: 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: 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: 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: 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: { case Permission.TIMELINE_READ: {
const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set<string>(); const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
const isPartner = await this.repository.timeline.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner); return setUnion(isOwner, isPartner);
} }
case Permission.TIMELINE_DOWNLOAD: 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: { case Permission.LIBRARY_READ: {
const isOwner = await this.repository.library.checkOwnerAccess(authUser.id, ids); const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids);
const isPartner = await this.repository.library.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); const isPartner = await this.repository.library.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner); return setUnion(isOwner, isPartner);
} }
case Permission.LIBRARY_UPDATE: 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: 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: 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: 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: 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: 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: 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: 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(); const allowedIds = new Set();
for (const id of ids) { for (const id of ids) {
const hasAccess = await this.hasOtherAccess(authUser, permission, id); const hasAccess = await this.hasOtherAccess(auth, permission, id);
if (hasAccess) { if (hasAccess) {
allowedIds.add(id); allowedIds.add(id);
} }
@ -270,17 +269,17 @@ export class AccessCore {
} }
// TODO: Migrate logic to checkAccessOther to evaluate permissions in bulk. // 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) { switch (permission) {
// uses album id // uses album id
case Permission.ACTIVITY_CREATE: 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 // uses activity id
case Permission.ACTIVITY_DELETE: case Permission.ACTIVITY_DELETE:
return ( return (
(await this.repository.activity.hasOwnerAccess(authUser.id, id)) || (await this.repository.activity.hasOwnerAccess(auth.user.id, id)) ||
(await this.repository.activity.hasAlbumOwnerAccess(authUser.id, id)) (await this.repository.activity.hasAlbumOwnerAccess(auth.user.id, id))
); );
default: default:

View File

@ -1,7 +1,7 @@
import { ActivityEntity } from '@app/infra/entities'; import { ActivityEntity } from '@app/infra/entities';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { IAccessRepository, IActivityRepository } from '../repositories'; import { IAccessRepository, IActivityRepository } from '../repositories';
import { import {
ActivityCreateDto, ActivityCreateDto,
@ -26,8 +26,8 @@ export class ActivityService {
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
} }
async getAll(authUser: AuthUserDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId); await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
const activities = await this.repository.search({ const activities = await this.repository.search({
userId: dto.userId, userId: dto.userId,
albumId: dto.albumId, albumId: dto.albumId,
@ -38,16 +38,16 @@ export class ActivityService {
return activities.map(mapActivity); return activities.map(mapActivity);
} }
async getStatistics(authUser: AuthUserDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, dto.albumId); await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId);
return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) };
} }
async create(authUser: AuthUserDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> { async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
await this.access.requirePermission(authUser, Permission.ACTIVITY_CREATE, dto.albumId); await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId);
const common = { const common = {
userId: authUser.id, userId: auth.user.id,
assetId: dto.assetId, assetId: dto.assetId,
albumId: dto.albumId, albumId: dto.albumId,
}; };
@ -77,8 +77,8 @@ export class ActivityService {
return { duplicate, value: mapActivity(activity) }; return { duplicate, value: mapActivity(activity) };
} }
async delete(authUser: AuthUserDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(authUser, Permission.ACTIVITY_DELETE, id); await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id);
await this.repository.delete(id); await this.repository.delete(id);
} }
} }

View File

@ -48,9 +48,9 @@ describe(AlbumService.name, () => {
notShared: 0, notShared: 0,
}); });
expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.id); expect(albumMock.getOwned).toHaveBeenCalledWith(authStub.admin.user.id);
expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.id); expect(albumMock.getShared).toHaveBeenCalledWith(authStub.admin.user.id);
expect(albumMock.getNotShared).toHaveBeenCalledWith(authStub.admin.id); expect(albumMock.getNotShared).toHaveBeenCalledWith(authStub.admin.user.id);
}); });
}); });
@ -188,7 +188,7 @@ describe(AlbumService.name, () => {
}); });
expect(albumMock.create).toHaveBeenCalledWith({ expect(albumMock.create).toHaveBeenCalledWith({
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
albumName: albumStub.empty.albumName, albumName: albumStub.empty.albumName,
description: albumStub.empty.description, description: albumStub.empty.description,
sharedUsers: [{ id: 'user-id' }], sharedUsers: [{ id: 'user-id' }],
@ -312,7 +312,7 @@ describe(AlbumService.name, () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin);
await expect( 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); ).rejects.toBeInstanceOf(BadRequestException);
expect(albumMock.update).not.toHaveBeenCalled(); expect(albumMock.update).not.toHaveBeenCalled();
}); });
@ -332,11 +332,11 @@ describe(AlbumService.name, () => {
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin)); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin));
albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin);
userMock.get.mockResolvedValue(userStub.user2); 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({ expect(albumMock.update).toHaveBeenCalledWith({
id: albumStub.sharedWithAdmin.id, id: albumStub.sharedWithAdmin.id,
updatedAt: expect.any(Date), 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); albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple);
await expect( 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); ).rejects.toBeInstanceOf(BadRequestException);
expect(albumMock.update).not.toHaveBeenCalled(); expect(albumMock.update).not.toHaveBeenCalled();
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
authStub.user1.id, authStub.user1.user.id,
new Set([albumStub.sharedWithMultiple.id]), new Set([albumStub.sharedWithMultiple.id]),
); );
}); });
@ -383,7 +383,7 @@ describe(AlbumService.name, () => {
it('should allow a shared user to remove themselves', async () => { it('should allow a shared user to remove themselves', async () => {
albumMock.getById.mockResolvedValue(albumStub.sharedWithUser); 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).toHaveBeenCalledTimes(1);
expect(albumMock.update).toHaveBeenCalledWith({ expect(albumMock.update).toHaveBeenCalledWith({
@ -409,7 +409,7 @@ describe(AlbumService.name, () => {
it('should not allow the owner to be removed', async () => { it('should not allow the owner to be removed', async () => {
albumMock.getById.mockResolvedValue(albumStub.empty); 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, BadRequestException,
); );
@ -444,7 +444,7 @@ describe(AlbumService.name, () => {
expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true }); expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true });
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.id, authStub.admin.user.id,
new Set([albumStub.oneAsset.id]), new Set([albumStub.oneAsset.id]),
); );
}); });
@ -465,7 +465,7 @@ describe(AlbumService.name, () => {
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId, authStub.adminSharedLink.sharedLink?.id,
new Set(['album-123']), new Set(['album-123']),
); );
}); });
@ -485,14 +485,20 @@ describe(AlbumService.name, () => {
await sut.get(authStub.user1, 'album-123', {}); await sut.get(authStub.user1, 'album-123', {});
expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); 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 () => { it('should throw an error for no access', async () => {
await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); 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.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-123']));
expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.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( expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId, authStub.adminSharedLink.sharedLink?.id,
new Set(['album-123']), new Set(['album-123']),
); );
}); });
@ -610,7 +616,7 @@ describe(AlbumService.name, () => {
updatedAt: expect.any(Date), updatedAt: expect.any(Date),
albumThumbnailAssetId: 'asset-1', 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 () => { it('should skip duplicate assets', async () => {
@ -635,8 +641,8 @@ describe(AlbumService.name, () => {
{ success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION }, { success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION },
]); ]);
expect(accessMock.asset.checkOwnerAccess).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.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 () => { it('should not allow unauthorized access to the album', async () => {
@ -729,7 +735,7 @@ describe(AlbumService.name, () => {
// // await expect( // // await expect(
// // sut.removeAssetsFromAlbum( // // sut.removeAssetsFromAlbum(
// // authUser, // // auth,
// // { // // {
// // ids: ['1'], // // ids: ['1'],
// // }, // // },
@ -755,6 +761,6 @@ describe(AlbumService.name, () => {
// albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
// albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); // albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse));
// await expect(sut.removeAssets(authUser, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException); // await expect(sut.removeAssets(auth, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException);
// }); // });
}); });

View File

@ -2,7 +2,7 @@ import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { setUnion } from '../domain.util'; import { setUnion } from '../domain.util';
import { import {
AlbumAssetCount, AlbumAssetCount,
@ -35,11 +35,11 @@ export class AlbumService {
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
} }
async getCount(authUser: AuthUserDto): Promise<AlbumCountResponseDto> { async getCount(auth: AuthDto): Promise<AlbumCountResponseDto> {
const [owned, shared, notShared] = await Promise.all([ const [owned, shared, notShared] = await Promise.all([
this.albumRepository.getOwned(authUser.id), this.albumRepository.getOwned(auth.user.id),
this.albumRepository.getShared(authUser.id), this.albumRepository.getShared(auth.user.id),
this.albumRepository.getNotShared(authUser.id), this.albumRepository.getNotShared(auth.user.id),
]); ]);
return { return {
@ -49,7 +49,7 @@ export class AlbumService {
}; };
} }
async getAll({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> { async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail(); const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail();
for (const albumId of invalidAlbumIds) { for (const albumId of invalidAlbumIds) {
const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId); const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
@ -98,8 +98,8 @@ export class AlbumService {
); );
} }
async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> { async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
await this.albumRepository.updateThumbnails(); await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets }); const album = await this.findOrFail(id, { withAssets });
@ -113,7 +113,7 @@ export class AlbumService {
}; };
} }
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> { async create(auth: AuthDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
for (const userId of dto.sharedWithUserIds || []) { for (const userId of dto.sharedWithUserIds || []) {
const exists = await this.userRepository.get(userId, {}); const exists = await this.userRepository.get(userId, {});
if (!exists) { if (!exists) {
@ -122,7 +122,7 @@ export class AlbumService {
} }
const album = await this.albumRepository.create({ const album = await this.albumRepository.create({
ownerId: authUser.id, ownerId: auth.user.id,
albumName: dto.albumName, albumName: dto.albumName,
description: dto.description, description: dto.description,
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value }) as UserEntity) ?? [], sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value }) as UserEntity) ?? [],
@ -133,8 +133,8 @@ export class AlbumService {
return mapAlbumWithAssets(album); return mapAlbumWithAssets(album);
} }
async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id); await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id);
const album = await this.findOrFail(id, { withAssets: true }); const album = await this.findOrFail(id, { withAssets: true });
@ -155,22 +155,22 @@ export class AlbumService {
return mapAlbumWithoutAssets(updatedAlbum); return mapAlbumWithoutAssets(updatedAlbum);
} }
async delete(authUser: AuthUserDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id); await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id);
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
await this.albumRepository.delete(album); await this.albumRepository.delete(album);
} }
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false }); 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 existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id)); 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[] = []; const results: BulkIdResponseDto[] = [];
for (const assetId of dto.ids) { for (const assetId of dto.ids) {
@ -202,14 +202,14 @@ export class AlbumService {
return results; return results;
} }
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false }); 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 existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
const canRemove = await this.access.checkAccess(authUser, Permission.ALBUM_REMOVE_ASSET, existingAssetIds); const canRemove = await this.access.checkAccess(auth, Permission.ALBUM_REMOVE_ASSET, existingAssetIds);
const canShare = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, existingAssetIds); const canShare = await this.access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds);
const allowedAssetIds = setUnion(canRemove, canShare); const allowedAssetIds = setUnion(canRemove, canShare);
const results: BulkIdResponseDto[] = []; const results: BulkIdResponseDto[] = [];
@ -241,8 +241,8 @@ export class AlbumService {
return results; return results;
} }
async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto): Promise<AlbumResponseDto> { async addUsers(auth: AuthDto, id: string, dto: AddUsersDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id); await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
@ -273,9 +273,9 @@ export class AlbumService {
.then(mapAlbumWithoutAssets); .then(mapAlbumWithoutAssets);
} }
async removeUser(authUser: AuthUserDto, id: string, userId: string | 'me'): Promise<void> { async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
if (userId === 'me') { if (userId === 'me') {
userId = authUser.id; userId = auth.user.id;
} }
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
@ -290,8 +290,8 @@ export class AlbumService {
} }
// non-admin can remove themselves // non-admin can remove themselves
if (authUser.id !== userId) { if (auth.user.id !== userId) {
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id); await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id);
} }
await this.albumRepository.update({ await this.albumRepository.update({

View File

@ -21,7 +21,7 @@ describe(APIKeyService.name, () => {
expect(keyMock.create).toHaveBeenCalledWith({ expect(keyMock.create).toHaveBeenCalledWith({
key: 'cmFuZG9tLWJ5dGVz (hashed)', key: 'cmFuZG9tLWJ5dGVz (hashed)',
name: 'Test Key', name: 'Test Key',
userId: authStub.admin.id, userId: authStub.admin.user.id,
}); });
expect(cryptoMock.randomBytes).toHaveBeenCalled(); expect(cryptoMock.randomBytes).toHaveBeenCalled();
expect(cryptoMock.hashSha256).toHaveBeenCalled(); expect(cryptoMock.hashSha256).toHaveBeenCalled();
@ -35,7 +35,7 @@ describe(APIKeyService.name, () => {
expect(keyMock.create).toHaveBeenCalledWith({ expect(keyMock.create).toHaveBeenCalledWith({
key: 'cmFuZG9tLWJ5dGVz (hashed)', key: 'cmFuZG9tLWJ5dGVz (hashed)',
name: 'API Key', name: 'API Key',
userId: authStub.admin.id, userId: authStub.admin.user.id,
}); });
expect(cryptoMock.randomBytes).toHaveBeenCalled(); expect(cryptoMock.randomBytes).toHaveBeenCalled();
expect(cryptoMock.hashSha256).toHaveBeenCalled(); expect(cryptoMock.hashSha256).toHaveBeenCalled();
@ -59,7 +59,7 @@ describe(APIKeyService.name, () => {
await sut.update(authStub.admin, 'random-guid', { name: 'New 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'); 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); 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 () => { it('should get a key by id', async () => {
@ -95,7 +95,7 @@ describe(APIKeyService.name, () => {
await sut.getById(authStub.admin, 'random-guid'); 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); await expect(sut.getAll(authStub.admin)).resolves.toHaveLength(1);
expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.id); expect(keyMock.getByUserId).toHaveBeenCalledWith(authStub.admin.user.id);
}); });
}); });
}); });

View File

@ -1,6 +1,6 @@
import { APIKeyEntity } from '@app/infra/entities'; import { APIKeyEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { ICryptoRepository, IKeyRepository } from '../repositories'; import { ICryptoRepository, IKeyRepository } from '../repositories';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from './api-key.dto';
@ -11,47 +11,47 @@ export class APIKeyService {
@Inject(IKeyRepository) private repository: IKeyRepository, @Inject(IKeyRepository) private repository: IKeyRepository,
) {} ) {}
async create(authUser: AuthUserDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, ''); const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
const entity = await this.repository.create({ const entity = await this.repository.create({
key: this.crypto.hashSha256(secret), key: this.crypto.hashSha256(secret),
name: dto.name || 'API Key', name: dto.name || 'API Key',
userId: authUser.id, userId: auth.user.id,
}); });
return { secret, apiKey: this.map(entity) }; return { secret, apiKey: this.map(entity) };
} }
async update(authUser: AuthUserDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> { async update(auth: AuthDto, id: string, dto: APIKeyCreateDto): Promise<APIKeyResponseDto> {
const exists = await this.repository.getById(authUser.id, id); const exists = await this.repository.getById(auth.user.id, id);
if (!exists) { if (!exists) {
throw new BadRequestException('API Key not found'); 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); return this.map(key);
} }
async delete(authUser: AuthUserDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
const exists = await this.repository.getById(authUser.id, id); const exists = await this.repository.getById(auth.user.id, id);
if (!exists) { if (!exists) {
throw new BadRequestException('API Key not found'); 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<APIKeyResponseDto> { async getById(auth: AuthDto, id: string): Promise<APIKeyResponseDto> {
const key = await this.repository.getById(authUser.id, id); const key = await this.repository.getById(auth.user.id, id);
if (!key) { if (!key) {
throw new BadRequestException('API Key not found'); throw new BadRequestException('API Key not found');
} }
return this.map(key); return this.map(key);
} }
async getAll(authUser: AuthUserDto): Promise<APIKeyResponseDto[]> { async getAll(auth: AuthDto): Promise<APIKeyResponseDto[]> {
const keys = await this.repository.getByUserId(authUser.id); const keys = await this.repository.getByUserId(auth.user.id);
return keys.map((key) => this.map(key)); return keys.map((key) => this.map(key));
} }

View File

@ -59,7 +59,7 @@ const statResponse: AssetStatsResponseDto = {
const uploadFile = { const uploadFile = {
nullAuth: { nullAuth: {
authUser: null, auth: null,
fieldName: UploadFieldName.ASSET_DATA, fieldName: UploadFieldName.ASSET_DATA,
file: { file: {
checksum: Buffer.from('checksum', 'utf8'), checksum: Buffer.from('checksum', 'utf8'),
@ -69,7 +69,7 @@ const uploadFile = {
}, },
filename: (fieldName: UploadFieldName, filename: string) => { filename: (fieldName: UploadFieldName, filename: string) => {
return { return {
authUser: authStub.admin, auth: authStub.admin,
fieldName, fieldName,
file: { file: {
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
@ -328,7 +328,7 @@ describe(AssetService.name, () => {
{ title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] }, { 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, size: TimeBucketSize.DAY,
}), }),
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); ).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' }), sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-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', { expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
@ -370,14 +370,14 @@ describe(AssetService.name, () => {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, isArchived: true,
userId: authStub.admin.id, userId: authStub.admin.user.id,
}), }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, isArchived: true,
userIds: [authStub.admin.id], userIds: [authStub.admin.user.id],
}); });
}); });
@ -388,13 +388,13 @@ describe(AssetService.name, () => {
sut.getTimeBucket(authStub.admin, { sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
userId: authStub.admin.id, userId: authStub.admin.user.id,
}), }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
userIds: [authStub.admin.id], userIds: [authStub.admin.user.id],
}); });
}); });
@ -405,7 +405,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, isArchived: true,
withPartners: true, withPartners: true,
userId: authStub.admin.id, userId: authStub.admin.user.id,
}), }),
).rejects.toThrowError(BadRequestException); ).rejects.toThrowError(BadRequestException);
@ -415,7 +415,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: undefined, isArchived: undefined,
withPartners: true, withPartners: true,
userId: authStub.admin.id, userId: authStub.admin.user.id,
}), }),
).rejects.toThrowError(BadRequestException); ).rejects.toThrowError(BadRequestException);
}); });
@ -427,7 +427,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket', timeBucket: 'bucket',
isFavorite: true, isFavorite: true,
withPartners: true, withPartners: true,
userId: authStub.admin.id, userId: authStub.admin.user.id,
}), }),
).rejects.toThrowError(BadRequestException); ).rejects.toThrowError(BadRequestException);
@ -437,7 +437,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket', timeBucket: 'bucket',
isFavorite: false, isFavorite: false,
withPartners: true, withPartners: true,
userId: authStub.admin.id, userId: authStub.admin.user.id,
}), }),
).rejects.toThrowError(BadRequestException); ).rejects.toThrowError(BadRequestException);
}); });
@ -449,7 +449,7 @@ describe(AssetService.name, () => {
timeBucket: 'bucket', timeBucket: 'bucket',
isTrashed: true, isTrashed: true,
withPartners: true, withPartners: true,
userId: authStub.admin.id, userId: authStub.admin.user.id,
}), }),
).rejects.toThrowError(BadRequestException); ).rejects.toThrowError(BadRequestException);
}); });
@ -459,9 +459,9 @@ describe(AssetService.name, () => {
it('should require the asset.download permission', async () => { it('should require the asset.download permission', async () => {
await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException); 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.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.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.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 () => { 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); 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'); expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1');
}); });
it('should return a list of archives (userId)', async () => { 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({ assetMock.getByUserId.mockResolvedValue({
items: [assetStub.image, assetStub.video], items: [assetStub.image, assetStub.video],
hasNextPage: false, 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, 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, isVisible: true,
}); });
}); });
it('should split archives by size', async () => { 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({ assetMock.getByUserId.mockResolvedValue({
items: [ items: [
@ -585,7 +585,7 @@ describe(AssetService.name, () => {
await expect( await expect(
sut.getDownloadInfo(authStub.admin, { sut.getDownloadInfo(authStub.admin, {
userId: authStub.admin.id, userId: authStub.admin.user.id,
archiveSize: 30_000, archiveSize: 30_000,
}), }),
).resolves.toEqual({ ).resolves.toEqual({
@ -624,25 +624,25 @@ describe(AssetService.name, () => {
it('should get the statistics for a user, excluding archived assets', async () => { it('should get the statistics for a user, excluding archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats); assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse); 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 () => { it('should get the statistics for a user for archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats); assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse); 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 () => { it('should get the statistics for a user for favorite assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats); assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse); 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 () => { it('should get the statistics for a user for all assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats); assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse); 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', 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', 'asset-1',
]); ]);
}); });

View File

@ -5,7 +5,7 @@ import { DateTime, Duration } from 'luxon';
import { extname } from 'path'; import { extname } from 'path';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { mimeTypes } from '../domain.constant'; import { mimeTypes } from '../domain.constant';
import { HumanReadableSize, usePagination } from '../domain.util'; import { HumanReadableSize, usePagination } from '../domain.util';
import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
@ -63,7 +63,7 @@ export enum UploadFieldName {
} }
export interface UploadRequest { export interface UploadRequest {
authUser: AuthUserDto | null; auth: AuthDto | null;
fieldName: UploadFieldName; fieldName: UploadFieldName;
file: UploadFile; file: UploadFile;
} }
@ -93,7 +93,7 @@ export class AssetService {
this.configCore = SystemConfigCore.create(configRepository); this.configCore = SystemConfigCore.create(configRepository);
} }
search(authUser: AuthUserDto, dto: AssetSearchDto) { search(auth: AuthDto, dto: AssetSearchDto) {
let checksum: Buffer | undefined = undefined; let checksum: Buffer | undefined = undefined;
if (dto.checksum) { if (dto.checksum) {
@ -109,7 +109,7 @@ export class AssetService {
...dto, ...dto,
order, order,
checksum, checksum,
ownerId: authUser.id, ownerId: auth.user.id,
}) })
.then((assets) => .then((assets) =>
assets.map((asset) => assets.map((asset) =>
@ -121,8 +121,8 @@ export class AssetService {
); );
} }
canUploadFile({ authUser, fieldName, file }: UploadRequest): true { canUploadFile({ auth, fieldName, file }: UploadRequest): true {
this.access.requireUploadAccess(authUser); this.access.requireUploadAccess(auth);
const filename = file.originalName; const filename = file.originalName;
@ -156,8 +156,8 @@ export class AssetService {
throw new BadRequestException(`Unsupported file type ${filename}`); throw new BadRequestException(`Unsupported file type ${filename}`);
} }
getUploadFilename({ authUser, fieldName, file }: UploadRequest): string { getUploadFilename({ auth, fieldName, file }: UploadRequest): string {
this.access.requireUploadAccess(authUser); this.access.requireUploadAccess(auth);
const originalExt = extname(file.originalName); const originalExt = extname(file.originalName);
@ -171,12 +171,12 @@ export class AssetService {
return sanitize(`${this.cryptoRepository.randomUUID()}${lookup[fieldName]}`); return sanitize(`${this.cryptoRepository.randomUUID()}${lookup[fieldName]}`);
} }
getUploadFolder({ authUser, fieldName }: UploadRequest): string { getUploadFolder({ auth, fieldName }: UploadRequest): string {
authUser = this.access.requireUploadAccess(authUser); 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) { if (fieldName === UploadFieldName.PROFILE_DATA) {
folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, authUser.id); folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, auth.user.id);
} }
this.storageRepository.mkdirSync(folder); this.storageRepository.mkdirSync(folder);
@ -184,13 +184,13 @@ export class AssetService {
return folder; return folder;
} }
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> { getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.assetRepository.getMapMarkers(authUser.id, options); return this.assetRepository.getMapMarkers(auth.user.id, options);
} }
async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> { async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
const currentYear = new Date().getFullYear(); 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) return _.chain(assets)
.filter((asset) => asset.localDateTime.getFullYear() < currentYear) .filter((asset) => asset.localDateTime.getFullYear() < currentYear)
@ -207,17 +207,17 @@ export class AssetService {
.value(); .value();
} }
private async timeBucketChecks(authUser: AuthUserDto, dto: TimeBucketDto) { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
if (dto.albumId) { if (dto.albumId) {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, [dto.albumId]); await this.access.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
} else { } else {
dto.userId = dto.userId || authUser.id; dto.userId = dto.userId || auth.user.id;
} }
if (dto.userId) { 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) { 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<TimeBucketResponseDto[]> { async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
await this.timeBucketChecks(authUser, dto); await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
return this.assetRepository.getTimeBuckets(timeBucketOptions); return this.assetRepository.getTimeBuckets(timeBucketOptions);
} }
async getTimeBucket( async getTimeBucket(
authUser: AuthUserDto, auth: AuthDto,
dto: TimeBucketAssetDto, dto: TimeBucketAssetDto,
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> { ): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
await this.timeBucketChecks(authUser, dto); await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); 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 })); return assets.map((asset) => mapAsset(asset, { withStack: true }));
} else { } else {
return assets.map((asset) => mapAsset(asset, { stripMetadata: true })); return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
} }
} }
async buildTimeBucketOptions(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketOptions> { async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
const { userId, ...options } = dto; const { userId, ...options } = dto;
let userIds: string[] | undefined = undefined; let userIds: string[] | undefined = undefined;
@ -263,7 +263,7 @@ export class AssetService {
userIds = [userId]; userIds = [userId];
if (dto.withPartners) { if (dto.withPartners) {
const partners = await this.partnerRepository.getAll(authUser.id); const partners = await this.partnerRepository.getAll(auth.user.id);
const partnersIds = partners const partnersIds = partners
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline) .filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline)
.map((partner) => partner.sharedById); .map((partner) => partner.sharedById);
@ -274,8 +274,8 @@ export class AssetService {
return { ...options, userIds }; return { ...options, userIds };
} }
async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> { async downloadFile(auth: AuthDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id); await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id);
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) { if (!asset) {
@ -289,12 +289,12 @@ export class AssetService {
return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath)); return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath));
} }
async getDownloadInfo(authUser: AuthUserDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> { async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
const archives: DownloadArchiveInfo[] = []; const archives: DownloadArchiveInfo[] = [];
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; 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) { for await (const assets of assetPagination) {
// motion part of live photos // motion part of live photos
const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter<string>((id): id is string => !!id); const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter<string>((id): id is string => !!id);
@ -323,8 +323,8 @@ export class AssetService {
}; };
} }
async downloadArchive(authUser: AuthUserDto, dto: AssetIdsDto): Promise<ImmichReadStream> { async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, dto.assetIds); await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds);
const zip = this.storageRepository.createZipStream(); const zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds); const assets = await this.assetRepository.getByIds(dto.assetIds);
@ -347,12 +347,12 @@ export class AssetService {
return { stream: zip.stream }; return { stream: zip.stream };
} }
private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadInfoDto): Promise<AsyncGenerator<AssetEntity[]>> { private async getDownloadAssets(auth: AuthDto, dto: DownloadInfoDto): Promise<AsyncGenerator<AssetEntity[]>> {
const PAGINATION_SIZE = 2500; const PAGINATION_SIZE = 2500;
if (dto.assetIds) { if (dto.assetIds) {
const assetIds = 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); const assets = await this.assetRepository.getByIds(assetIds);
return (async function* () { return (async function* () {
yield assets; yield assets;
@ -361,13 +361,13 @@ export class AssetService {
if (dto.albumId) { if (dto.albumId) {
const albumId = 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)); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
} }
if (dto.userId) { if (dto.userId) {
const userId = 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) => return usePagination(PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
); );
@ -376,22 +376,22 @@ export class AssetService {
throw new BadRequestException('assetIds, albumId, or userId is required'); throw new BadRequestException('assetIds, albumId, or userId is required');
} }
async getStatistics(authUser: AuthUserDto, dto: AssetStatsDto) { async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
const stats = await this.assetRepository.getStatistics(authUser.id, dto); const stats = await this.assetRepository.getStatistics(auth.user.id, dto);
return mapStats(stats); return mapStats(stats);
} }
async getRandom(authUser: AuthUserDto, count: number): Promise<AssetResponseDto[]> { async getRandom(auth: AuthDto, count: number): Promise<AssetResponseDto[]> {
const assets = await this.assetRepository.getRandom(authUser.id, count); const assets = await this.assetRepository.getRandom(auth.user.id, count);
return assets.map((a) => mapAsset(a)); return assets.map((a) => mapAsset(a));
} }
async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) {
return this.assetRepository.getAllByDeviceId(authUser.id, deviceId); return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId);
} }
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id); await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id);
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto; const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
@ -400,9 +400,9 @@ export class AssetService {
return mapAsset(asset); return mapAsset(asset);
} }
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto; 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) { if (removeParent) {
(options as Partial<AssetEntity>).stackParentId = null; (options as Partial<AssetEntity>).stackParentId = null;
@ -411,7 +411,7 @@ export class AssetService {
// All the unique parent's -> parent is set to null // All the unique parent's -> parent is set to null
ids.push(...new Set(assets.filter((a) => !!a.stackParentId).map((a) => a.stackParentId!))); ids.push(...new Set(assets.filter((a) => !!a.stackParentId).map((a) => a.stackParentId!)));
} else if (options.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 // Merge stacks
const assets = await this.assetRepository.getByIds(ids); const assets = await this.assetRepository.getByIds(ids);
const assetsWithChildren = assets.filter((a) => a.stack && a.stack.length > 0); const assetsWithChildren = assets.filter((a) => a.stack && a.stack.length > 0);
@ -430,7 +430,7 @@ export class AssetService {
} }
await this.assetRepository.updateAll(ids, options); 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() { async handleAssetDeletionCheck() {
@ -493,10 +493,10 @@ export class AssetService {
return true; return true;
} }
async deleteAll(authUser: AuthUserDto, dto: AssetBulkDeleteDto): Promise<void> { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto; const { ids, force } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_DELETE, ids); await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
if (force) { if (force) {
for (const id of ids) { for (const id of ids) {
@ -504,20 +504,20 @@ export class AssetService {
} }
} else { } else {
await this.assetRepository.softDeleteAll(ids); 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<void> { async handleTrashAction(auth: AuthDto, action: TrashAction): Promise<void> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => 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) { if (action == TrashAction.RESTORE_ALL) {
for await (const assets of assetPagination) { for await (const assets of assetPagination) {
const ids = assets.map((a) => a.id); const ids = assets.map((a) => a.id);
await this.assetRepository.restoreAll(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);
} }
return; return;
} }
@ -532,17 +532,17 @@ export class AssetService {
} }
} }
async restoreAll(authUser: AuthUserDto, dto: BulkIdsDto): Promise<void> { async restoreAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
const { ids } = dto; 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); 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<void> { async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise<void> {
const { oldParentId, newParentId } = dto; const { oldParentId, newParentId } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_READ, oldParentId); await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId);
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, newParentId); await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId);
const childIds: string[] = []; const childIds: string[] = [];
const oldParent = await this.assetRepository.getById(oldParentId); const oldParent = await this.assetRepository.getById(oldParentId);
@ -552,14 +552,14 @@ export class AssetService {
childIds.push(...(oldParent.stack?.map((a) => a.id) ?? [])); 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 }); await this.assetRepository.updateAll(childIds, { stackParentId: newParentId });
// Remove ParentId of new parent if this was previously a child of some other asset // Remove ParentId of new parent if this was previously a child of some other asset
return this.assetRepository.updateAll([newParentId], { stackParentId: null }); return this.assetRepository.updateAll([newParentId], { stackParentId: null });
} }
async run(authUser: AuthUserDto, dto: AssetJobsDto) { async run(auth: AuthDto, dto: AssetJobsDto) {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds); await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds);
for (const id of dto.assetIds) { for (const id of dto.assetIds) {
switch (dto.name) { switch (dto.name) {

View File

@ -65,7 +65,7 @@ describe(AuditService.name, () => {
expect(auditMock.getAfter).toHaveBeenCalledWith(date, { expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
action: DatabaseAction.DELETE, action: DatabaseAction.DELETE,
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
entityType: EntityType.ASSET, entityType: EntityType.ASSET,
}); });
}); });
@ -81,7 +81,7 @@ describe(AuditService.name, () => {
expect(auditMock.getAfter).toHaveBeenCalledWith(date, { expect(auditMock.getAfter).toHaveBeenCalledWith(date, {
action: DatabaseAction.DELETE, action: DatabaseAction.DELETE,
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
entityType: EntityType.ASSET, entityType: EntityType.ASSET,
}); });
}); });

View File

@ -3,7 +3,7 @@ import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { AUDIT_LOG_MAX_DURATION } from '../domain.constant'; import { AUDIT_LOG_MAX_DURATION } from '../domain.constant';
import { usePagination } from '../domain.util'; import { usePagination } from '../domain.util';
import { JOBS_ASSET_PAGINATION_SIZE } from '../job'; import { JOBS_ASSET_PAGINATION_SIZE } from '../job';
@ -48,9 +48,9 @@ export class AuditService {
return true; return true;
} }
async getDeletes(authUser: AuthUserDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> { async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
const userId = dto.userId || authUser.id; const userId = dto.userId || auth.user.id;
await this.access.requirePermission(authUser, Permission.TIMELINE_READ, userId); await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const audits = await this.repository.getAfter(dto.after, { const audits = await this.repository.getAfter(dto.after, {
ownerId: userId, ownerId: userId,

View File

@ -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 { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class AuthUserDto { export class AuthDto {
id!: string; user!: UserEntity;
email!: string;
isAdmin!: boolean; apiKey?: APIKeyEntity;
isPublicUser?: boolean; sharedLink?: SharedLinkEntity;
sharedLinkId?: string; userToken?: UserTokenEntity;
isAllowUpload?: boolean;
isAllowDownload?: boolean;
isShowMetadata?: boolean;
accessTokenId?: string;
externalPath?: string | null;
} }
export class LoginCredentialDto { export class LoginCredentialDto {

View File

@ -31,7 +31,7 @@ import {
IUserTokenRepository, IUserTokenRepository,
} from '../repositories'; } from '../repositories';
import { AuthType } from './auth.constant'; import { AuthType } from './auth.constant';
import { AuthUserDto, SignUpDto } from './auth.dto'; import { AuthDto, SignUpDto } from './auth.dto';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
// const token = Buffer.from('my-api-key', 'utf8').toString('base64'); // const token = Buffer.from('my-api-key', 'utf8').toString('base64');
@ -145,7 +145,7 @@ describe('AuthService', () => {
describe('changePassword', () => { describe('changePassword', () => {
it('should change the password', async () => { 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' }; const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue({ userMock.getByEmail.mockResolvedValue({
@ -153,23 +153,23 @@ describe('AuthService', () => {
password: 'hash-password', password: 'hash-password',
} as UserEntity); } 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'); expect(cryptoMock.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
}); });
it('should throw when auth user email is not found', async () => { 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' }; const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue(null); 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 () => { 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' }; const dto = { password: 'old-password', newPassword: 'new-password' };
cryptoMock.compareBcrypt.mockReturnValue(false); cryptoMock.compareBcrypt.mockReturnValue(false);
@ -179,11 +179,11 @@ describe('AuthService', () => {
password: 'hash-password', password: 'hash-password',
} as UserEntity); } 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 () => { 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' }; const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue({ userMock.getByEmail.mockResolvedValue({
@ -191,33 +191,33 @@ describe('AuthService', () => {
password: '', password: '',
} as UserEntity); } as UserEntity);
await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException); await expect(sut.changePassword(auth, dto)).rejects.toBeInstanceOf(BadRequestException);
}); });
}); });
describe('logout', () => { describe('logout', () => {
it('should return the end session endpoint', async () => { it('should return the end session endpoint', async () => {
configMock.load.mockResolvedValue(systemConfigStub.enabled); configMock.load.mockResolvedValue(systemConfigStub.enabled);
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, successful: true,
redirectUri: 'http://end-session-endpoint', redirectUri: 'http://end-session-endpoint',
}); });
}); });
it('should return the default redirect', async () => { 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, successful: true,
redirectUri: '/auth/login?autoLaunch=0', redirectUri: '/auth/login?autoLaunch=0',
}); });
}); });
it('should delete the access token', async () => { 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, successful: true,
redirectUri: '/auth/login?autoLaunch=0', 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 () => { 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, successful: true,
redirectUri: '/auth/login?autoLaunch=0', redirectUri: '/auth/login?autoLaunch=0',
}); });
@ -268,7 +268,10 @@ describe('AuthService', () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; 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); shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userStub.admin); userMock.get.mockResolvedValue(userStub.admin);
const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') }; 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); expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
}); });
@ -304,7 +310,10 @@ describe('AuthService', () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userStub.admin); userMock.get.mockResolvedValue(userStub.admin);
const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') }; 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); expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
}); });
}); });
@ -319,14 +328,20 @@ describe('AuthService', () => {
it('should return an auth dto', async () => { it('should return an auth dto', async () => {
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; 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 () => { it('should update when access time exceeds an hour', async () => {
userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken); userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken);
userTokenMock.save.mockResolvedValue(userTokenStub.userToken); userTokenMock.save.mockResolvedValue(userTokenStub.userToken);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; 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({ expect(userTokenMock.save.mock.calls[0][0]).toMatchObject({
id: 'not_active', id: 'not_active',
token: 'auth_token', token: 'auth_token',
@ -350,7 +365,7 @@ describe('AuthService', () => {
it('should return an auth dto', async () => { it('should return an auth dto', async () => {
keyMock.getKey.mockResolvedValue(keyStub.admin); keyMock.getKey.mockResolvedValue(keyStub.admin);
const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; 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)'); 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); 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).toHaveBeenCalledWith('not_active');
expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id'); expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id');
}); });
@ -399,7 +414,7 @@ describe('AuthService', () => {
await sut.logoutDevice(authStub.user1, 'token-1'); 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'); expect(userTokenMock.delete).toHaveBeenCalledWith('token-1');
}); });
}); });
@ -506,7 +521,7 @@ describe('AuthService', () => {
await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' }); 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 () => { it('should not link an already linked oauth.sub', async () => {
@ -528,7 +543,7 @@ describe('AuthService', () => {
await sut.unlink(authStub.user1); await sut.unlink(authStub.user1);
expect(userMock.update).toHaveBeenCalledWith(authStub.user1.id, { oauthId: '' }); expect(userMock.update).toHaveBeenCalledWith(authStub.user1.user.id, { oauthId: '' });
}); });
}); });
}); });

View File

@ -34,7 +34,7 @@ import {
} from './auth.constant'; } from './auth.constant';
import { import {
AuthDeviceResponseDto, AuthDeviceResponseDto,
AuthUserDto, AuthDto,
ChangePasswordDto, ChangePasswordDto,
LoginCredentialDto, LoginCredentialDto,
LoginResponseDto, LoginResponseDto,
@ -110,9 +110,9 @@ export class AuthService {
return this.createLoginResponse(user, AuthType.PASSWORD, details); return this.createLoginResponse(user, AuthType.PASSWORD, details);
} }
async logout(authUser: AuthUserDto, authType: AuthType): Promise<LogoutResponseDto> { async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
if (authUser.accessTokenId) { if (auth.userToken) {
await this.userTokenRepository.delete(authUser.accessTokenId); await this.userTokenRepository.delete(auth.userToken.id);
} }
return { 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 { password, newPassword } = dto;
const user = await this.userRepository.getByEmail(authUser.email, true); const user = await this.userRepository.getByEmail(auth.user.email, true);
if (!user) { if (!user) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
@ -133,7 +133,7 @@ export class AuthService {
throw new BadRequestException('Wrong password'); 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<UserResponseDto> { async adminSignUp(dto: SignUpDto): Promise<UserResponseDto> {
@ -154,7 +154,7 @@ export class AuthService {
return mapUser(admin); return mapUser(admin);
} }
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto> { async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
const shareKey = (headers['x-immich-share-key'] || params.key) as string; const shareKey = (headers['x-immich-share-key'] || params.key) as string;
const userToken = (headers['x-immich-user-token'] || const userToken = (headers['x-immich-user-token'] ||
params.userToken || params.userToken ||
@ -177,20 +177,20 @@ export class AuthService {
throw new UnauthorizedException('Authentication required'); throw new UnauthorizedException('Authentication required');
} }
async getDevices(authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> { async getDevices(auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
const userTokens = await this.userTokenRepository.getAll(authUser.id); const userTokens = await this.userTokenRepository.getAll(auth.user.id);
return userTokens.map((userToken) => mapUserToken(userToken, authUser.accessTokenId)); return userTokens.map((userToken) => mapUserToken(userToken, auth.userToken?.id));
} }
async logoutDevice(authUser: AuthUserDto, id: string): Promise<void> { async logoutDevice(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(authUser, Permission.AUTH_DEVICE_DELETE, id); await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id);
await this.userTokenRepository.delete(id); await this.userTokenRepository.delete(id);
} }
async logoutDevices(authUser: AuthUserDto): Promise<void> { async logoutDevices(auth: AuthDto): Promise<void> {
const devices = await this.userTokenRepository.getAll(authUser.id); const devices = await this.userTokenRepository.getAll(auth.user.id);
for (const device of devices) { for (const device of devices) {
if (device.id === authUser.accessTokenId) { if (device.id === auth.userToken?.id) {
continue; continue;
} }
await this.userTokenRepository.delete(device.id); await this.userTokenRepository.delete(device.id);
@ -284,19 +284,19 @@ export class AuthService {
return this.createLoginResponse(user, AuthType.OAUTH, loginDetails); return this.createLoginResponse(user, AuthType.OAUTH, loginDetails);
} }
async link(user: AuthUserDto, dto: OAuthCallbackDto): Promise<UserResponseDto> { async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
const config = await this.configCore.getConfig(); const config = await this.configCore.getConfig();
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url); const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
const duplicate = await this.userRepository.getByOAuthId(oauthId); 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}).`); 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.'); 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<UserResponseDto> { async unlink(auth: AuthDto): Promise<UserResponseDto> {
return mapUser(await this.userRepository.update(user.id, { oauthId: '' })); return mapUser(await this.userRepository.update(auth.user.id, { oauthId: '' }));
} }
private async getLogoutEndpoint(authType: AuthType): Promise<string> { private async getLogoutEndpoint(authType: AuthType): Promise<string> {
@ -371,45 +371,27 @@ export class AuthService {
return cookies[IMMICH_ACCESS_COOKIE] || null; return cookies[IMMICH_ACCESS_COOKIE] || null;
} }
private async validateSharedLink(key: string | string[]): Promise<AuthUserDto> { private async validateSharedLink(key: string | string[]): Promise<AuthDto> {
key = Array.isArray(key) ? key[0] : key; key = Array.isArray(key) ? key[0] : key;
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url'); const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
const link = await this.sharedLinkRepository.getByKey(bytes); const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
if (link) { if (sharedLink) {
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) { if (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date()) {
const user = link.user; const user = sharedLink.user;
if (user) { if (user) {
return { return { user, sharedLink };
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
isPublicUser: true,
sharedLinkId: link.id,
isAllowUpload: link.allowUpload,
isAllowDownload: link.allowDownload,
isShowMetadata: link.showExif,
};
} }
} }
} }
throw new UnauthorizedException('Invalid share key'); throw new UnauthorizedException('Invalid share key');
} }
private async validateApiKey(key: string): Promise<AuthUserDto> { private async validateApiKey(key: string): Promise<AuthDto> {
const hashedKey = this.cryptoRepository.hashSha256(key); const hashedKey = this.cryptoRepository.hashSha256(key);
const keyEntity = await this.keyRepository.getKey(hashedKey); const apiKey = await this.keyRepository.getKey(hashedKey);
if (keyEntity?.user) { if (apiKey?.user) {
const user = keyEntity.user; return { user: apiKey.user, apiKey };
return {
id: user.id,
email: user.email,
isAdmin: user.isAdmin,
isPublicUser: false,
isAllowUpload: true,
externalPath: user.externalPath,
};
} }
throw new UnauthorizedException('Invalid API key'); throw new UnauthorizedException('Invalid API key');
@ -422,26 +404,19 @@ export class AuthService {
return this.cryptoRepository.compareBcrypt(inputPassword, user.password); return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
} }
private async validateUserToken(tokenValue: string): Promise<AuthUserDto> { private async validateUserToken(tokenValue: string): Promise<AuthDto> {
const hashedToken = this.cryptoRepository.hashSha256(tokenValue); 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 now = DateTime.now();
const updatedAt = DateTime.fromJSDate(token.updatedAt); const updatedAt = DateTime.fromJSDate(userToken.updatedAt);
const diff = now.diff(updatedAt, ['hours']); const diff = now.diff(updatedAt, ['hours']);
if (diff.hours > 1) { if (diff.hours > 1) {
token = await this.userTokenRepository.save({ ...token, updatedAt: new Date() }); userToken = await this.userTokenRepository.save({ ...userToken, updatedAt: new Date() });
} }
return { return { user: userToken.user, userToken };
...token.user,
isPublicUser: false,
isAllowUpload: true,
isAllowDownload: true,
isShowMetadata: true,
accessTokenId: token.id,
};
} }
throw new UnauthorizedException('Invalid user token'); throw new UnauthorizedException('Invalid user token');

View File

@ -632,7 +632,7 @@ describe(LibraryService.name, () => {
await expect(sut.getCount(authStub.admin)).resolves.toBe(17); 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', () => { describe('update', () => {
it('can update library ', async () => { it('can update library ', async () => {
libraryMock.update.mockResolvedValue(libraryStub.uploadLibrary1); 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(libraryMock.update).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
id: authStub.admin.id, id: authStub.admin.user.id,
}), }),
); );
}); });

View File

@ -5,7 +5,7 @@ import { Stats } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { basename, parse } from 'path'; import { basename, parse } from 'path';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { mimeTypes } from '../domain.constant'; import { mimeTypes } from '../domain.constant';
import { usePagination, validateCronExpression } from '../domain.util'; import { usePagination, validateCronExpression } from '../domain.util';
import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; 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<LibraryStatsResponseDto> { async getStatistics(auth: AuthDto, id: string): Promise<LibraryStatsResponseDto> {
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, id); await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
return this.repository.getStatistics(id); return this.repository.getStatistics(id);
} }
async getCount(authUser: AuthUserDto): Promise<number> { async getCount(auth: AuthDto): Promise<number> {
return this.repository.getCountForUser(authUser.id); return this.repository.getCountForUser(auth.user.id);
} }
async getAllForUser(authUser: AuthUserDto): Promise<LibraryResponseDto[]> { async getAllForUser(auth: AuthDto): Promise<LibraryResponseDto[]> {
const libraries = await this.repository.getAllByUserId(authUser.id); const libraries = await this.repository.getAllByUserId(auth.user.id);
return libraries.map((library) => mapLibrary(library)); return libraries.map((library) => mapLibrary(library));
} }
async get(authUser: AuthUserDto, id: string): Promise<LibraryResponseDto> { async get(auth: AuthDto, id: string): Promise<LibraryResponseDto> {
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, id); await this.access.requirePermission(auth, Permission.LIBRARY_READ, id);
const library = await this.findOrFail(id); const library = await this.findOrFail(id);
return mapLibrary(library); return mapLibrary(library);
} }
@ -99,7 +99,7 @@ export class LibraryService {
return true; return true;
} }
async create(authUser: AuthUserDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> { async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
switch (dto.type) { switch (dto.type) {
case LibraryType.EXTERNAL: case LibraryType.EXTERNAL:
if (!dto.name) { if (!dto.name) {
@ -120,7 +120,7 @@ export class LibraryService {
} }
const library = await this.repository.create({ const library = await this.repository.create({
ownerId: authUser.id, ownerId: auth.user.id,
name: dto.name, name: dto.name,
type: dto.type, type: dto.type,
importPaths: dto.importPaths ?? [], importPaths: dto.importPaths ?? [],
@ -131,17 +131,17 @@ export class LibraryService {
return mapLibrary(library); return mapLibrary(library);
} }
async update(authUser: AuthUserDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id); await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
const library = await this.repository.update({ id, ...dto }); const library = await this.repository.update({ id, ...dto });
return mapLibrary(library); return mapLibrary(library);
} }
async delete(authUser: AuthUserDto, id: string) { async delete(auth: AuthDto, id: string) {
await this.access.requirePermission(authUser, Permission.LIBRARY_DELETE, id); await this.access.requirePermission(auth, Permission.LIBRARY_DELETE, id);
const library = await this.findOrFail(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) { if (library.type === LibraryType.UPLOAD && uploadCount <= 1) {
throw new BadRequestException('Cannot delete the last upload library'); throw new BadRequestException('Cannot delete the last upload library');
} }
@ -294,8 +294,8 @@ export class LibraryService {
return true; return true;
} }
async queueScan(authUser: AuthUserDto, id: string, dto: ScanLibraryDto) { async queueScan(auth: AuthDto, id: string, dto: ScanLibraryDto) {
await this.access.requirePermission(authUser, Permission.LIBRARY_UPDATE, id); await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id);
const library = await this.repository.get(id); const library = await this.repository.get(id);
if (!library || library.type !== LibraryType.EXTERNAL) { 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}`); 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({ await this.jobRepository.queue({
name: JobName.LIBRARY_REMOVE_OFFLINE, name: JobName.LIBRARY_REMOVE_OFFLINE,

View File

@ -60,13 +60,13 @@ describe(PartnerService.name, () => {
it("should return a list of partners with whom I've shared my library", async () => { it("should return a list of partners with whom I've shared my library", async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toEqual([responseDto.admin]); 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 () => { it('should return a list of partners who have shared their libraries with me', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toEqual([responseDto.admin]); 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.get.mockResolvedValue(null);
partnerMock.create.mockResolvedValue(partnerStub.adminToUser1); 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({ expect(partnerMock.create).toHaveBeenCalledWith({
sharedById: authStub.admin.id, sharedById: authStub.admin.user.id,
sharedWithId: authStub.user1.id, sharedWithId: authStub.user1.user.id,
}); });
}); });
it('should throw an error when the partner already exists', async () => { it('should throw an error when the partner already exists', async () => {
partnerMock.get.mockResolvedValue(partnerStub.adminToUser1); 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(); expect(partnerMock.create).not.toHaveBeenCalled();
}); });
@ -96,7 +96,7 @@ describe(PartnerService.name, () => {
it('should remove a partner', async () => { it('should remove a partner', async () => {
partnerMock.get.mockResolvedValue(partnerStub.adminToUser1); 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); 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 () => { it('should throw an error when the partner does not exist', async () => {
partnerMock.get.mockResolvedValue(null); 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(); expect(partnerMock.remove).not.toHaveBeenCalled();
}); });

View File

@ -1,7 +1,7 @@
import { PartnerEntity } from '@app/infra/entities'; import { PartnerEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories';
import { mapUser } from '../user'; import { mapUser } from '../user';
import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto'; import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto';
@ -16,8 +16,8 @@ export class PartnerService {
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
} }
async create(authUser: AuthUserDto, sharedWithId: string): Promise<PartnerResponseDto> { async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> {
const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const exists = await this.repository.get(partnerId); const exists = await this.repository.get(partnerId);
if (exists) { if (exists) {
throw new BadRequestException(`Partner already exists`); throw new BadRequestException(`Partner already exists`);
@ -27,8 +27,8 @@ export class PartnerService {
return this.map(partner, PartnerDirection.SharedBy); return this.map(partner, PartnerDirection.SharedBy);
} }
async remove(authUser: AuthUserDto, sharedWithId: string): Promise<void> { async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const partner = await this.repository.get(partnerId); const partner = await this.repository.get(partnerId);
if (!partner) { if (!partner) {
throw new BadRequestException('Partner not found'); throw new BadRequestException('Partner not found');
@ -37,18 +37,18 @@ export class PartnerService {
await this.repository.remove(partner); await this.repository.remove(partner);
} }
async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> { async getAll(auth: AuthDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> {
const partners = await this.repository.getAll(authUser.id); const partners = await this.repository.getAll(auth.user.id);
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
return partners return partners
.filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users .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)); .map((partner) => this.map(partner, direction));
} }
async update(authUser: AuthUserDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> { async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
await this.access.requirePermission(authUser, Permission.PARTNER_UPDATE, sharedById); await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById);
const partnerId: PartnerIds = { sharedById, sharedWithId: authUser.id }; const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
return this.map(entity, PartnerDirection.SharedWith); return this.map(entity, PartnerDirection.SharedWith);

View File

@ -2,7 +2,7 @@ import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; 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'; import { Optional, ValidateUUID, toBoolean } from '../domain.util';
export class PersonUpdateDto { 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 { return {
...mapFacesWithoutPerson(face), ...mapFacesWithoutPerson(face),
person: face.person?.ownerId === authUser.id ? mapPerson(face.person) : null, person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null,
}; };
} }

View File

@ -113,7 +113,7 @@ describe(PersonService.name, () => {
visible: 1, visible: 1,
people: [responseDto], people: [responseDto],
}); });
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, { expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
minimumFaceCount: 1, minimumFaceCount: 1,
withHidden: false, withHidden: false,
}); });
@ -125,7 +125,7 @@ describe(PersonService.name, () => {
visible: 1, visible: 1,
people: [responseDto], people: [responseDto],
}); });
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.id, { expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
minimumFaceCount: 1, minimumFaceCount: 1,
withHidden: false, 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, minimumFaceCount: 1,
withHidden: true, withHidden: true,
}); });
@ -157,14 +157,14 @@ describe(PersonService.name, () => {
it('should require person.read permission', async () => { it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.withName); personMock.getById.mockResolvedValue(personStub.withName);
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); 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 () => { it('should throw a bad request when person is not found', async () => {
personMock.getById.mockResolvedValue(null); personMock.getById.mockResolvedValue(null);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); 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 () => { it('should get a person by id', async () => {
@ -172,7 +172,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto); await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto);
expect(personMock.getById).toHaveBeenCalledWith('person-1'); 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); personMock.getById.mockResolvedValue(personStub.noName);
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(storageMock.createReadStream).not.toHaveBeenCalled(); 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 () => { 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'])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
expect(storageMock.createReadStream).not.toHaveBeenCalled(); 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 () => { 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'])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
expect(storageMock.createReadStream).not.toHaveBeenCalled(); 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 () => { it('should serve the thumbnail', async () => {
@ -205,7 +205,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await sut.getThumbnail(authStub.admin, 'person-1'); await sut.getThumbnail(authStub.admin, 'person-1');
expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg'); 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]); personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]);
await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
expect(personMock.getAssets).not.toHaveBeenCalled(); 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 () => { it("should return a person's assets", async () => {
@ -222,7 +222,7 @@ describe(PersonService.name, () => {
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await sut.getAssets(authStub.admin, 'person-1'); await sut.getAssets(authStub.admin, 'person-1');
expect(personMock.getAssets).toHaveBeenCalledWith('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, BadRequestException,
); );
expect(personMock.update).not.toHaveBeenCalled(); 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 () => { it('should throw an error when personId is invalid', async () => {
@ -243,7 +243,7 @@ describe(PersonService.name, () => {
BadRequestException, BadRequestException,
); );
expect(personMock.update).not.toHaveBeenCalled(); 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 () => { it("should update a person's name", async () => {
@ -256,7 +256,7 @@ describe(PersonService.name, () => {
expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.getById).toHaveBeenCalledWith('person-1');
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: '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 () => { 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.getById).toHaveBeenCalledWith('person-1');
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
expect(jobMock.queue).not.toHaveBeenCalled(); 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 () => { it('should update a person visibility', async () => {
@ -289,7 +289,7 @@ describe(PersonService.name, () => {
expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.getById).toHaveBeenCalledWith('person-1');
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false }); 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 () => { 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(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 () => { it('should throw an error when the face feature assetId is invalid', async () => {
@ -323,7 +323,7 @@ describe(PersonService.name, () => {
BadRequestException, BadRequestException,
); );
expect(personMock.update).not.toHaveBeenCalled(); 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' }] }), sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }),
).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]); ).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]);
expect(personMock.update).not.toHaveBeenCalled(); 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.reassignFaces).not.toHaveBeenCalled();
expect(personMock.delete).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 () => { it('should merge two people', async () => {
@ -784,7 +784,7 @@ describe(PersonService.name, () => {
name: JobName.PERSON_DELETE, name: JobName.PERSON_DELETE,
data: { id: personStub.mergePerson.id }, 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 () => { 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(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 () => { it('should handle invalid merge ids', async () => {
@ -811,7 +811,7 @@ describe(PersonService.name, () => {
expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled();
expect(personMock.delete).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 () => { it('should handle an error reassigning faces', async () => {
@ -826,7 +826,7 @@ describe(PersonService.name, () => {
]); ]);
expect(personMock.delete).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']));
}); });
}); });
@ -836,19 +836,19 @@ describe(PersonService.name, () => {
personMock.getStatistics.mockResolvedValue(statistics); personMock.getStatistics.mockResolvedValue(statistics);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 }); 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 () => { it('should require person.read permission', async () => {
personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getById.mockResolvedValue(personStub.primaryPerson);
await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); 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', () => { describe('mapFace', () => {
it('should map a face', () => { it('should map a face', () => {
expect(mapFaces(faceStub.face1, personStub.withName.owner)).toEqual({ expect(mapFaces(faceStub.face1, { user: personStub.withName.owner })).toEqual({
boundingBoxX1: 0, boundingBoxX1: 0,
boundingBoxX2: 1, boundingBoxX2: 1,
boundingBoxY1: 0, boundingBoxY1: 0,

View File

@ -3,7 +3,7 @@ import { PersonPathType } from '@app/infra/entities/move.entity';
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset'; import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { mimeTypes } from '../domain.constant'; import { mimeTypes } from '../domain.constant';
import { usePagination } from '../domain.util'; import { usePagination } from '../domain.util';
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; 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); this.storageCore = StorageCore.create(assetRepository, moveRepository, repository, storageRepository);
} }
async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
const { machineLearning } = await this.configCore.getConfig(); 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, minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden: dto.withHidden || false, withHidden: dto.withHidden || false,
}); });
@ -83,12 +83,12 @@ export class PersonService {
}; };
} }
createPerson(authUser: AuthUserDto): Promise<PersonResponseDto> { createPerson(auth: AuthDto): Promise<PersonResponseDto> {
return this.repository.create({ ownerId: authUser.id }); return this.repository.create({ ownerId: auth.user.id });
} }
async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> { async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
const person = await this.findOrFail(personId); const person = await this.findOrFail(personId);
const result: PersonResponseDto[] = []; const result: PersonResponseDto[] = [];
const changeFeaturePhoto: string[] = []; const changeFeaturePhoto: string[] = [];
@ -96,7 +96,7 @@ export class PersonService {
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
for (const face of faces) { 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) { if (person.faceAssetId === null) {
changeFeaturePhoto.push(person.id); changeFeaturePhoto.push(person.id);
} }
@ -116,10 +116,10 @@ export class PersonService {
return result; return result;
} }
async reassignFacesById(authUser: AuthUserDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> { async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId); 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 face = await this.repository.getFaceById(dto.id);
const person = await this.findOrFail(personId); const person = await this.findOrFail(personId);
@ -134,10 +134,10 @@ export class PersonService {
return await this.findOrFail(personId).then(mapPerson); return await this.findOrFail(personId).then(mapPerson);
} }
async getFacesById(authUser: AuthUserDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> { async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
await this.access.requirePermission(authUser, Permission.ASSET_READ, dto.id); await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id);
const faces = await this.repository.getFaces(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[]) { async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
@ -163,18 +163,18 @@ export class PersonService {
} }
} }
async getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> { async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id); await this.access.requirePermission(auth, Permission.PERSON_READ, id);
return this.findOrFail(id).then(mapPerson); return this.findOrFail(id).then(mapPerson);
} }
async getStatistics(authUser: AuthUserDto, id: string): Promise<PersonStatisticsResponseDto> { async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id); await this.access.requirePermission(auth, Permission.PERSON_READ, id);
return this.repository.getStatistics(id); return this.repository.getStatistics(id);
} }
async getThumbnail(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> { async getThumbnail(auth: AuthDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id); await this.access.requirePermission(auth, Permission.PERSON_READ, id);
const person = await this.repository.getById(id); const person = await this.repository.getById(id);
if (!person || !person.thumbnailPath) { if (!person || !person.thumbnailPath) {
throw new NotFoundException(); throw new NotFoundException();
@ -183,14 +183,14 @@ export class PersonService {
return this.storageRepository.createReadStream(person.thumbnailPath, mimeTypes.lookup(person.thumbnailPath)); return this.storageRepository.createReadStream(person.thumbnailPath, mimeTypes.lookup(person.thumbnailPath));
} }
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> { async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, id); await this.access.requirePermission(auth, Permission.PERSON_READ, id);
const assets = await this.repository.getAssets(id); const assets = await this.repository.getAssets(id);
return assets.map((asset) => mapAsset(asset)); return assets.map((asset) => mapAsset(asset));
} }
async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id); await this.access.requirePermission(auth, Permission.PERSON_WRITE, id);
let person = await this.findOrFail(id); let person = await this.findOrFail(id);
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
@ -200,7 +200,7 @@ export class PersonService {
} }
if (assetId) { 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 }]); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]);
if (!face) { if (!face) {
throw new BadRequestException('Invalid assetId for feature face'); throw new BadRequestException('Invalid assetId for feature face');
@ -213,11 +213,11 @@ export class PersonService {
return mapPerson(person); return mapPerson(person);
} }
async updatePeople(authUser: AuthUserDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> { async updatePeople(auth: AuthDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
const results: BulkIdResponseDto[] = []; const results: BulkIdResponseDto[] = [];
for (const person of dto.people) { for (const person of dto.people) {
try { try {
await this.update(authUser, person.id, { await this.update(auth, person.id, {
isHidden: person.isHidden, isHidden: person.isHidden,
name: person.name, name: person.name,
birthDate: person.birthDate, birthDate: person.birthDate,
@ -438,15 +438,15 @@ export class PersonService {
return true; return true;
} }
async mergePerson(authUser: AuthUserDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> { async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
const mergeIds = dto.ids; 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 primaryPerson = await this.findOrFail(id);
const primaryName = primaryPerson.name || primaryPerson.id; const primaryName = primaryPerson.name || primaryPerson.id;
const results: BulkIdResponseDto[] = []; 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) { for (const mergeId of mergeIds) {
const hasAccess = allowedIds.has(mergeId); const hasAccess = allowedIds.has(mergeId);

View File

@ -49,11 +49,11 @@ describe(SearchService.name, () => {
await sut.searchPerson(authStub.user1, { name, withHidden: false }); 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 }); 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); const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse); 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(); expect(smartInfoMock.searchCLIP).not.toHaveBeenCalled();
}); });
@ -132,7 +132,11 @@ describe(SearchService.name, () => {
const result = await sut.search(authStub.user1, dto); const result = await sut.search(authStub.user1, dto);
expect(result).toEqual(expectedResponse); 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(); expect(assetMock.searchMetadata).not.toHaveBeenCalled();
}); });

View File

@ -1,7 +1,7 @@
import { AssetEntity } from '@app/infra/entities'; import { AssetEntity } from '@app/infra/entities';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { AssetResponseDto, mapAsset } from '../asset'; import { AssetResponseDto, mapAsset } from '../asset';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { PersonResponseDto } from '../person'; import { PersonResponseDto } from '../person';
import { import {
IAssetRepository, IAssetRepository,
@ -31,16 +31,16 @@ export class SearchService {
this.configCore = SystemConfigCore.create(configRepository); this.configCore = SystemConfigCore.create(configRepository);
} }
async searchPerson(authUser: AuthUserDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> { async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(authUser.id, dto.name, { withHidden: dto.withHidden }); return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
} }
async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetResponseDto>[]> { async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
await this.configCore.requireFeature(FeatureFlag.SEARCH); await this.configCore.requireFeature(FeatureFlag.SEARCH);
const options = { maxFields: 12, minAssetsPerField: 5 }; const options = { maxFields: 12, minAssetsPerField: 5 };
const results = await Promise.all([ const results = await Promise.all([
this.assetRepository.getAssetIdByCity(authUser.id, options), this.assetRepository.getAssetIdByCity(auth.user.id, options),
this.assetRepository.getAssetIdByTag(authUser.id, options), this.assetRepository.getAssetIdByTag(auth.user.id, options),
]); ]);
const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data))); const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
const assets = await this.assetRepository.getByIds(Array.from(assetIds)); const assets = await this.assetRepository.getByIds(Array.from(assetIds));
@ -52,7 +52,7 @@ export class SearchService {
})); }));
} }
async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> { async search(auth: AuthDto, dto: SearchDto): Promise<SearchResponseDto> {
const { machineLearning } = await this.configCore.getConfig(); const { machineLearning } = await this.configCore.getConfig();
const query = dto.q || dto.query; const query = dto.q || dto.query;
if (!query) { if (!query) {
@ -73,10 +73,10 @@ export class SearchService {
{ text: query }, { text: query },
machineLearning.clip, 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; break;
case SearchStrategy.TEXT: 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: default:
break; break;
} }

View File

@ -41,7 +41,7 @@ describe(SharedLinkService.name, () => {
sharedLinkResponseStub.expired, sharedLinkResponseStub.expired,
sharedLinkResponseStub.valid, 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; const authDto = authStub.adminSharedLink;
shareMock.get.mockResolvedValue(sharedLinkStub.valid); shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.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 () => { it('should not return metadata', async () => {
const authDto = authStub.adminSharedLinkNoExif; const authDto = authStub.adminSharedLinkNoExif;
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); 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 () => { it('should throw an error for an password protected shared link', async () => {
const authDto = authStub.adminSharedLink; const authDto = authStub.adminSharedLink;
shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired); shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException); 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 () => { it('should throw an error for an invalid shared link', async () => {
shareMock.get.mockResolvedValue(null); shareMock.get.mockResolvedValue(null);
await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); 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(); expect(shareMock.update).not.toHaveBeenCalled();
}); });
it('should get a shared link by id', async () => { it('should get a shared link by id', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid); shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.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 }); await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id });
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(
authStub.admin.id, authStub.admin.user.id,
new Set([albumStub.oneAsset.id]), new Set([albumStub.oneAsset.id]),
); );
expect(shareMock.create).toHaveBeenCalledWith({ expect(shareMock.create).toHaveBeenCalledWith({
type: SharedLinkType.ALBUM, type: SharedLinkType.ALBUM,
userId: authStub.admin.id, userId: authStub.admin.user.id,
albumId: albumStub.oneAsset.id, albumId: albumStub.oneAsset.id,
allowDownload: true, allowDownload: true,
allowUpload: true, allowUpload: true,
@ -149,10 +149,13 @@ describe(SharedLinkService.name, () => {
allowUpload: true, 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({ expect(shareMock.create).toHaveBeenCalledWith({
type: SharedLinkType.INDIVIDUAL, type: SharedLinkType.INDIVIDUAL,
userId: authStub.admin.id, userId: authStub.admin.user.id,
albumId: null, albumId: null,
allowDownload: true, allowDownload: true,
allowUpload: true, allowUpload: true,
@ -169,7 +172,7 @@ describe(SharedLinkService.name, () => {
it('should throw an error for an invalid shared link', async () => { it('should throw an error for an invalid shared link', async () => {
shareMock.get.mockResolvedValue(null); shareMock.get.mockResolvedValue(null);
await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException); 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(); expect(shareMock.update).not.toHaveBeenCalled();
}); });
@ -177,10 +180,10 @@ describe(SharedLinkService.name, () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid); shareMock.get.mockResolvedValue(sharedLinkStub.valid);
shareMock.update.mockResolvedValue(sharedLinkStub.valid); shareMock.update.mockResolvedValue(sharedLinkStub.valid);
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false }); 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({ expect(shareMock.update).toHaveBeenCalledWith({
id: sharedLinkStub.valid.id, id: sharedLinkStub.valid.id,
userId: authStub.user1.id, userId: authStub.user1.user.id,
allowDownload: false, allowDownload: false,
}); });
}); });
@ -190,14 +193,14 @@ describe(SharedLinkService.name, () => {
it('should throw an error for an invalid shared link', async () => { it('should throw an error for an invalid shared link', async () => {
shareMock.get.mockResolvedValue(null); shareMock.get.mockResolvedValue(null);
await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); 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(); expect(shareMock.update).not.toHaveBeenCalled();
}); });
it('should remove a key', async () => { it('should remove a key', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid); shareMock.get.mockResolvedValue(sharedLinkStub.valid);
await sut.remove(authStub.user1, sharedLinkStub.valid.id); 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); expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
}); });
}); });

View File

@ -2,7 +2,7 @@ import { AssetEntity, SharedLinkEntity, SharedLinkType } from '@app/infra/entiti
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { AccessCore, Permission } from '../access'; import { AccessCore, Permission } from '../access';
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset'; import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto } from '../asset';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories'; import { IAccessRepository, ICryptoRepository, ISharedLinkRepository } from '../repositories';
import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto'; import { SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata } from './shared-link-response.dto';
import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from './shared-link.dto'; import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from './shared-link.dto';
@ -19,42 +19,36 @@ export class SharedLinkService {
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
} }
getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> { getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.repository.getAll(authUser.id).then((links) => links.map(mapSharedLink)); return this.repository.getAll(auth.user.id).then((links) => links.map(mapSharedLink));
} }
async getMine(authUser: AuthUserDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> { async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
const { sharedLinkId: id, isPublicUser, isShowMetadata: isShowExif } = authUser; if (!auth.sharedLink) {
if (!isPublicUser || !id) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
const sharedLink = await this.findOrFail(authUser, id); const sharedLink = await this.findOrFail(auth, auth.sharedLink.id);
const response = this.map(sharedLink, { withExif: sharedLink.showExif });
let newToken;
if (sharedLink.password) { if (sharedLink.password) {
newToken = this.validateAndRefreshToken(sharedLink, dto); response.token = this.validateAndRefreshToken(sharedLink, dto);
} }
return { return response;
...this.map(sharedLink, { withExif: isShowExif ?? true }),
token: newToken,
};
} }
async get(authUser: AuthUserDto, id: string): Promise<SharedLinkResponseDto> { async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> {
const sharedLink = await this.findOrFail(authUser, id); const sharedLink = await this.findOrFail(auth, id);
return this.map(sharedLink, { withExif: true }); return this.map(sharedLink, { withExif: true });
} }
async create(authUser: AuthUserDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> { async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
switch (dto.type) { switch (dto.type) {
case SharedLinkType.ALBUM: case SharedLinkType.ALBUM:
if (!dto.albumId) { if (!dto.albumId) {
throw new BadRequestException('Invalid 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; break;
case SharedLinkType.INDIVIDUAL: case SharedLinkType.INDIVIDUAL:
@ -62,14 +56,14 @@ export class SharedLinkService {
throw new BadRequestException('Invalid assetIds'); 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; break;
} }
const sharedLink = await this.repository.create({ const sharedLink = await this.repository.create({
key: this.cryptoRepository.randomBytes(50), key: this.cryptoRepository.randomBytes(50),
userId: authUser.id, userId: auth.user.id,
type: dto.type, type: dto.type,
albumId: dto.albumId || null, albumId: dto.albumId || null,
assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity), assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity),
@ -84,11 +78,11 @@ export class SharedLinkService {
return this.map(sharedLink, { withExif: true }); return this.map(sharedLink, { withExif: true });
} }
async update(authUser: AuthUserDto, id: string, dto: SharedLinkEditDto) { async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
await this.findOrFail(authUser, id); await this.findOrFail(auth, id);
const sharedLink = await this.repository.update({ const sharedLink = await this.repository.update({
id, id,
userId: authUser.id, userId: auth.user.id,
description: dto.description, description: dto.description,
password: dto.password, password: dto.password,
expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt, expiresAt: dto.changeExpiryTime && !dto.expiresAt ? null : dto.expiresAt,
@ -99,21 +93,21 @@ export class SharedLinkService {
return this.map(sharedLink, { withExif: true }); return this.map(sharedLink, { withExif: true });
} }
async remove(authUser: AuthUserDto, id: string): Promise<void> { async remove(auth: AuthDto, id: string): Promise<void> {
const sharedLink = await this.findOrFail(authUser, id); const sharedLink = await this.findOrFail(auth, id);
await this.repository.remove(sharedLink); await this.repository.remove(sharedLink);
} }
private async findOrFail(authUser: AuthUserDto, id: string) { private async findOrFail(auth: AuthDto, id: string) {
const sharedLink = await this.repository.get(authUser.id, id); const sharedLink = await this.repository.get(auth.user.id, id);
if (!sharedLink) { if (!sharedLink) {
throw new BadRequestException('Shared link not found'); throw new BadRequestException('Shared link not found');
} }
return sharedLink; return sharedLink;
} }
async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
const sharedLink = await this.findOrFail(authUser, id); const sharedLink = await this.findOrFail(auth, id);
if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { if (sharedLink.type !== SharedLinkType.INDIVIDUAL) {
throw new BadRequestException('Invalid shared link type'); 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 existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); 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[] = []; const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) { for (const assetId of dto.assetIds) {
@ -146,8 +140,8 @@ export class SharedLinkService {
return results; return results;
} }
async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
const sharedLink = await this.findOrFail(authUser, id); const sharedLink = await this.findOrFail(auth, id);
if (sharedLink.type !== SharedLinkType.INDIVIDUAL) { if (sharedLink.type !== SharedLinkType.INDIVIDUAL) {
throw new BadRequestException('Invalid shared link type'); throw new BadRequestException('Invalid shared link type');

View File

@ -23,7 +23,7 @@ describe(TagService.name, () => {
it('should return all tags for a user', async () => { it('should return all tags for a user', async () => {
tagMock.getAll.mockResolvedValue([tagStub.tag1]); tagMock.getAll.mockResolvedValue([tagStub.tag1]);
await expect(sut.getAll(authStub.admin)).resolves.toEqual([tagResponseStub.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 () => { it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null); tagMock.getById.mockResolvedValue(null);
await expect(sut.getById(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); 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 () => { it('should return a tag for a user', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.getById.mockResolvedValue(tagStub.tag1);
await expect(sut.getById(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.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( await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).rejects.toBeInstanceOf(
BadRequestException, 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(); expect(tagMock.create).not.toHaveBeenCalled();
}); });
@ -57,7 +57,7 @@ describe(TagService.name, () => {
tagResponseStub.tag1, tagResponseStub.tag1,
); );
expect(tagMock.create).toHaveBeenCalledWith({ expect(tagMock.create).toHaveBeenCalledWith({
userId: authStub.admin.id, userId: authStub.admin.user.id,
name: 'tag-1', name: 'tag-1',
type: TagType.CUSTOM, type: TagType.CUSTOM,
}); });
@ -68,7 +68,7 @@ describe(TagService.name, () => {
it('should throw an error for an invalid id', async () => { it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null); tagMock.getById.mockResolvedValue(null);
await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).rejects.toBeInstanceOf(BadRequestException); 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(); expect(tagMock.remove).not.toHaveBeenCalled();
}); });
@ -76,7 +76,7 @@ describe(TagService.name, () => {
tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.getById.mockResolvedValue(tagStub.tag1);
tagMock.update.mockResolvedValue(tagStub.tag1); tagMock.update.mockResolvedValue(tagStub.tag1);
await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).resolves.toEqual(tagResponseStub.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' }); 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 () => { it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null); tagMock.getById.mockResolvedValue(null);
await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); 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(); expect(tagMock.remove).not.toHaveBeenCalled();
}); });
it('should remove a tag', async () => { it('should remove a tag', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.getById.mockResolvedValue(tagStub.tag1);
await sut.remove(authStub.admin, 'tag-1'); 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); expect(tagMock.remove).toHaveBeenCalledWith(tagStub.tag1);
}); });
}); });
@ -101,7 +101,7 @@ describe(TagService.name, () => {
it('should throw an error for an invalid id', async () => { it('should throw an error for an invalid id', async () => {
tagMock.getById.mockResolvedValue(null); tagMock.getById.mockResolvedValue(null);
await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); 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(); expect(tagMock.remove).not.toHaveBeenCalled();
}); });
@ -109,8 +109,8 @@ describe(TagService.name, () => {
tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.getById.mockResolvedValue(tagStub.tag1);
tagMock.getAssets.mockResolvedValue([assetStub.image]); tagMock.getAssets.mockResolvedValue([assetStub.image]);
await sut.getAssets(authStub.admin, 'tag-1'); await sut.getAssets(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.getAssets).toHaveBeenCalledWith(authStub.admin.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( await expect(sut.addAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException, 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(); expect(tagMock.addAssets).not.toHaveBeenCalled();
}); });
it('should reject duplicate asset ids and accept new ones', async () => { it('should reject duplicate asset ids and accept new ones', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.getById.mockResolvedValue(tagStub.tag1);
when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true); when(tagMock.hasAsset).calledWith(authStub.admin.user.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-2').mockResolvedValue(false);
await expect( await expect(
sut.addAssets(authStub.admin, 'tag-1', { sut.addAssets(authStub.admin, 'tag-1', {
@ -139,9 +139,9 @@ describe(TagService.name, () => {
{ assetId: 'asset-2', success: true }, { 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.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( await expect(sut.removeAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException, 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(); expect(tagMock.removeAssets).not.toHaveBeenCalled();
}); });
it('should accept accept ids that are tagged and reject the rest', async () => { it('should accept accept ids that are tagged and reject the rest', async () => {
tagMock.getById.mockResolvedValue(tagStub.tag1); tagMock.getById.mockResolvedValue(tagStub.tag1);
when(tagMock.hasAsset).calledWith(authStub.admin.id, 'tag-1', 'asset-1').mockResolvedValue(true); when(tagMock.hasAsset).calledWith(authStub.admin.user.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-2').mockResolvedValue(false);
await expect( await expect(
sut.removeAssets(authStub.admin, 'tag-1', { sut.removeAssets(authStub.admin, 'tag-1', {
@ -170,9 +170,9 @@ describe(TagService.name, () => {
{ assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, { 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.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']);
}); });
}); });
}); });

View File

@ -1,6 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto, AssetResponseDto, mapAsset } from '../asset'; import { AssetIdErrorReason, AssetIdsDto, AssetIdsResponseDto, AssetResponseDto, mapAsset } from '../asset';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { ITagRepository } from '../repositories'; import { ITagRepository } from '../repositories';
import { TagResponseDto, mapTag } from './tag-response.dto'; import { TagResponseDto, mapTag } from './tag-response.dto';
import { CreateTagDto, UpdateTagDto } from './tag.dto'; import { CreateTagDto, UpdateTagDto } from './tag.dto';
@ -9,23 +9,23 @@ import { CreateTagDto, UpdateTagDto } from './tag.dto';
export class TagService { export class TagService {
constructor(@Inject(ITagRepository) private repository: ITagRepository) {} constructor(@Inject(ITagRepository) private repository: ITagRepository) {}
getAll(authUser: AuthUserDto) { getAll(auth: AuthDto) {
return this.repository.getAll(authUser.id).then((tags) => tags.map(mapTag)); return this.repository.getAll(auth.user.id).then((tags) => tags.map(mapTag));
} }
async getById(authUser: AuthUserDto, id: string): Promise<TagResponseDto> { async getById(auth: AuthDto, id: string): Promise<TagResponseDto> {
const tag = await this.findOrFail(authUser, id); const tag = await this.findOrFail(auth, id);
return mapTag(tag); return mapTag(tag);
} }
async create(authUser: AuthUserDto, dto: CreateTagDto) { async create(auth: AuthDto, dto: CreateTagDto) {
const duplicate = await this.repository.hasName(authUser.id, dto.name); const duplicate = await this.repository.hasName(auth.user.id, dto.name);
if (duplicate) { if (duplicate) {
throw new BadRequestException(`A tag with that name already exists`); throw new BadRequestException(`A tag with that name already exists`);
} }
const tag = await this.repository.create({ const tag = await this.repository.create({
userId: authUser.id, userId: auth.user.id,
name: dto.name, name: dto.name,
type: dto.type, type: dto.type,
}); });
@ -33,29 +33,29 @@ export class TagService {
return mapTag(tag); return mapTag(tag);
} }
async update(authUser: AuthUserDto, id: string, dto: UpdateTagDto): Promise<TagResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateTagDto): Promise<TagResponseDto> {
await this.findOrFail(authUser, id); await this.findOrFail(auth, id);
const tag = await this.repository.update({ id, name: dto.name }); const tag = await this.repository.update({ id, name: dto.name });
return mapTag(tag); return mapTag(tag);
} }
async remove(authUser: AuthUserDto, id: string): Promise<void> { async remove(auth: AuthDto, id: string): Promise<void> {
const tag = await this.findOrFail(authUser, id); const tag = await this.findOrFail(auth, id);
await this.repository.remove(tag); await this.repository.remove(tag);
} }
async getAssets(authUser: AuthUserDto, id: string): Promise<AssetResponseDto[]> { async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
await this.findOrFail(authUser, id); await this.findOrFail(auth, id);
const assets = await this.repository.getAssets(authUser.id, id); const assets = await this.repository.getAssets(auth.user.id, id);
return assets.map((asset) => mapAsset(asset)); return assets.map((asset) => mapAsset(asset));
} }
async addAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
await this.findOrFail(authUser, id); await this.findOrFail(auth, id);
const results: AssetIdsResponseDto[] = []; const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) { 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) { if (hasAsset) {
results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE });
} else { } else {
@ -64,7 +64,7 @@ export class TagService {
} }
await this.repository.addAssets( await this.repository.addAssets(
authUser.id, auth.user.id,
id, id,
results.filter((result) => result.success).map((result) => result.assetId), results.filter((result) => result.success).map((result) => result.assetId),
); );
@ -72,12 +72,12 @@ export class TagService {
return results; return results;
} }
async removeAssets(authUser: AuthUserDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise<AssetIdsResponseDto[]> {
await this.findOrFail(authUser, id); await this.findOrFail(auth, id);
const results: AssetIdsResponseDto[] = []; const results: AssetIdsResponseDto[] = [];
for (const assetId of dto.assetIds) { 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) { if (!hasAsset) {
results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
} else { } else {
@ -86,7 +86,7 @@ export class TagService {
} }
await this.repository.removeAssets( await this.repository.removeAssets(
authUser.id, auth.user.id,
id, id,
results.filter((result) => result.success).map((result) => result.assetId), results.filter((result) => result.success).map((result) => result.assetId),
); );
@ -94,8 +94,8 @@ export class TagService {
return results; return results;
} }
private async findOrFail(authUser: AuthUserDto, id: string) { private async findOrFail(auth: AuthDto, id: string) {
const tag = await this.repository.getById(authUser.id, id); const tag = await this.repository.getById(auth.user.id, id);
if (!tag) { if (!tag) {
throw new BadRequestException('Tag not found'); throw new BadRequestException('Tag not found');
} }

View File

@ -2,8 +2,8 @@ import { LibraryType, UserEntity } from '@app/infra/entities';
import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { BadRequestException, ForbiddenException } from '@nestjs/common';
import path from 'path'; import path from 'path';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
import { AuthUserDto } from '../auth';
import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories'; import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
import { UserResponseDto } from './response-dto';
const SALT_ROUNDS = 10; const SALT_ROUNDS = 10;
@ -32,17 +32,18 @@ export class UserCore {
instance = null; instance = null;
} }
async updateUser(authUser: AuthUserDto, id: string, dto: Partial<UserEntity>): Promise<UserEntity> { // TODO: move auth related checks to the service layer
if (!authUser.isAdmin && authUser.id !== id) { async updateUser(user: UserEntity | UserResponseDto, id: string, dto: Partial<UserEntity>): Promise<UserEntity> {
if (!user.isAdmin && user.id !== id) {
throw new ForbiddenException('You are not allowed to update this user'); throw new ForbiddenException('You are not allowed to update this user');
} }
if (!authUser.isAdmin) { if (!user.isAdmin) {
// Users can never update the isAdmin property. // Users can never update the isAdmin property.
delete dto.isAdmin; delete dto.isAdmin;
delete dto.storageLabel; delete dto.storageLabel;
delete dto.externalPath; delete dto.externalPath;
} else if (dto.isAdmin && authUser.id !== id) { } else if (dto.isAdmin && user.id !== id) {
// Admin cannot create another admin. // Admin cannot create another admin.
throw new BadRequestException('The server already has an admin'); throw new BadRequestException('The server already has an admin');
} }

View File

@ -60,10 +60,10 @@ describe(UserService.name, () => {
sut = new UserService(albumMock, assetMock, cryptoRepositoryMock, jobMock, libraryMock, storageMock, userMock); 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.user.id, {}).mockResolvedValue(userStub.admin);
when(userMock.get).calledWith(authStub.admin.id, { withDeleted: true }).mockResolvedValue(userStub.admin); when(userMock.get).calledWith(authStub.admin.user.id, { withDeleted: true }).mockResolvedValue(userStub.admin);
when(userMock.get).calledWith(authStub.user1.id, {}).mockResolvedValue(userStub.user1); when(userMock.get).calledWith(authStub.user1.user.id, {}).mockResolvedValue(userStub.user1);
when(userMock.get).calledWith(authStub.user1.id, { withDeleted: true }).mockResolvedValue(userStub.user1); when(userMock.get).calledWith(authStub.user1.user.id, { withDeleted: true }).mockResolvedValue(userStub.user1);
}); });
describe('getAll', () => { describe('getAll', () => {
@ -71,8 +71,8 @@ describe(UserService.name, () => {
userMock.getList.mockResolvedValue([userStub.admin]); userMock.getList.mockResolvedValue([userStub.admin]);
await expect(sut.getAll(authStub.admin, false)).resolves.toEqual([ await expect(sut.getAll(authStub.admin, false)).resolves.toEqual([
expect.objectContaining({ expect.objectContaining({
id: authStub.admin.id, id: authStub.admin.user.id,
email: authStub.admin.email, email: authStub.admin.user.email,
}), }),
]); ]);
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true }); expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
@ -82,14 +82,14 @@ describe(UserService.name, () => {
describe('get', () => { describe('get', () => {
it('should get a user by id', async () => { it('should get a user by id', async () => {
userMock.get.mockResolvedValue(userStub.admin); userMock.get.mockResolvedValue(userStub.admin);
await sut.get(authStub.admin.id); await sut.get(authStub.admin.user.id);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false }); expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
}); });
it('should throw an error if a user is not found', async () => { it('should throw an error if a user is not found', async () => {
userMock.get.mockResolvedValue(null); userMock.get.mockResolvedValue(null);
await expect(sut.get(authStub.admin.id)).rejects.toBeInstanceOf(NotFoundException); await expect(sut.get(authStub.admin.user.id)).rejects.toBeInstanceOf(NotFoundException);
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false }); 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 () => { it("should get the auth user's info", async () => {
userMock.get.mockResolvedValue(userStub.admin); userMock.get.mockResolvedValue(userStub.admin);
await sut.getMe(authStub.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 () => { it('should throw an error if a user is not found', async () => {
userMock.get.mockResolvedValue(null); userMock.get.mockResolvedValue(null);
await expect(sut.getMe(authStub.admin)).rejects.toBeInstanceOf(BadRequestException); 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.getByStorageLabel.mockResolvedValue(null);
userMock.update.mockResolvedValue(userStub.user1); 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.getByEmail).toHaveBeenCalledWith(update.email);
expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel); expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel);
@ -127,13 +127,16 @@ describe(UserService.name, () => {
it('should not set an empty string for storage label', async () => { it('should not set an empty string for storage label', async () => {
userMock.update.mockResolvedValue(userStub.user1); userMock.update.mockResolvedValue(userStub.user1);
await sut.update(userStub.admin, { id: userStub.user1.id, storageLabel: '' }); await sut.update(authStub.admin, { id: userStub.user1.id, storageLabel: '' });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.id, storageLabel: null }); 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 () => { it('should omit a storage label set by non-admin users', async () => {
userMock.update.mockResolvedValue(userStub.user1); 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 }); expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.id });
}); });
@ -145,10 +148,13 @@ describe(UserService.name, () => {
id: 'not_immich_auth_user_id', id: 'not_immich_auth_user_id',
}); });
const result = sut.update(userStub.user1, { const result = sut.update(
id: 'not_immich_auth_user_id', { user: userStub.user1 },
password: 'I take over your account now', {
}); id: 'not_immich_auth_user_id',
password: 'I take over your account now',
},
);
await expect(result).rejects.toBeInstanceOf(ForbiddenException); await expect(result).rejects.toBeInstanceOf(ForbiddenException);
}); });
@ -158,7 +164,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.update.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, { expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: 'user-id', id: 'user-id',
@ -172,7 +178,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.getByEmail.mockResolvedValue(userStub.admin); 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(); expect(userMock.update).not.toHaveBeenCalled();
}); });
@ -183,7 +189,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.getByStorageLabel.mockResolvedValue(userStub.admin); 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(); expect(userMock.update).not.toHaveBeenCalled();
}); });
@ -195,7 +201,7 @@ describe(UserService.name, () => {
}; };
when(userMock.update).calledWith(userStub.user1.id, update).mockResolvedValueOnce(userStub.user1); 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, { expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: 'user-id', id: 'user-id',
shouldChangePassword: true, shouldChangePassword: true,
@ -205,7 +211,7 @@ describe(UserService.name, () => {
it('update user information should throw error if user not found', async () => { it('update user information should throw error if user not found', async () => {
when(userMock.get).calledWith(userStub.user1.id, {}).mockResolvedValueOnce(null); 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, id: userStub.user1.id,
shouldChangePassword: true, shouldChangePassword: true,
}); });
@ -218,7 +224,7 @@ describe(UserService.name, () => {
when(userMock.update).calledWith(userStub.admin.id, dto).mockResolvedValueOnce(userStub.admin); 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); 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); 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(); 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 () => { it('should restore an user', async () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.restore.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 () => { 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(); expect(userMock.delete).not.toHaveBeenCalled();
}); });
@ -276,7 +277,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.delete.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.get).toHaveBeenCalledWith(userStub.user1.id, {});
expect(userMock.delete).toHaveBeenCalledWith(userStub.user1); expect(userMock.delete).toHaveBeenCalledWith(userStub.user1);
}); });
@ -323,7 +324,7 @@ describe(UserService.name, () => {
const file = { path: '/profile/path' } as Express.Multer.File; const file = { path: '/profile/path' } as Express.Multer.File;
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); 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 () => { 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.get.mockResolvedValue(userStub.profilePath);
userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error')); 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 () => { it('should delete the previous profile image', async () => {
@ -340,7 +341,7 @@ describe(UserService.name, () => {
const files = [userStub.profilePath.profileImagePath]; const files = [userStub.profilePath.profileImagePath];
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); 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 } }]]); 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.get.mockResolvedValue(userStub.admin);
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); 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(); expect(jobMock.queue).not.toHaveBeenCalled();
}); });
}); });
@ -358,7 +359,7 @@ describe(UserService.name, () => {
it('should send an http error has no profile image', async () => { it('should send an http error has no profile image', async () => {
userMock.get.mockResolvedValue(userStub.admin); 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(); expect(jobMock.queue).not.toHaveBeenCalled();
}); });
@ -366,7 +367,7 @@ describe(UserService.name, () => {
userMock.get.mockResolvedValue(userStub.profilePath); userMock.get.mockResolvedValue(userStub.profilePath);
const files = [userStub.profilePath.profileImagePath]; 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 } }]]); await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
}); });
}); });

View File

@ -1,7 +1,7 @@
import { UserEntity } from '@app/infra/entities'; import { UserEntity } from '@app/infra/entities';
import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { AuthUserDto } from '../auth'; import { AuthDto } from '../auth';
import { IEntityJob, JobName } from '../job'; import { IEntityJob, JobName } from '../job';
import { import {
IAlbumRepository, IAlbumRepository,
@ -36,7 +36,7 @@ export class UserService {
this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository); this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
} }
async getAll(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> { async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: !isAll }); const users = await this.userRepository.getList({ withDeleted: !isAll });
return users.map(mapUser); return users.map(mapUser);
} }
@ -50,24 +50,20 @@ export class UserService {
return mapUser(user); return mapUser(user);
} }
getMe(authUser: AuthUserDto): Promise<UserResponseDto> { getMe(auth: AuthDto): Promise<UserResponseDto> {
return this.findOrFail(authUser.id, {}).then(mapUser); return this.findOrFail(auth.user.id, {}).then(mapUser);
} }
create(createUserDto: CreateUserDto): Promise<UserResponseDto> { create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
return this.userCore.createUser(createUserDto).then(mapUser); return this.userCore.createUser(createUserDto).then(mapUser);
} }
async update(authUser: AuthUserDto, dto: UpdateUserDto): Promise<UserResponseDto> { async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> {
await this.findOrFail(dto.id, {}); 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<UserResponseDto> { async delete(auth: AuthDto, id: string): Promise<UserResponseDto> {
if (!authUser.isAdmin) {
throw new ForbiddenException('Unauthorized');
}
const user = await this.findOrFail(id, {}); const user = await this.findOrFail(id, {});
if (user.isAdmin) { if (user.isAdmin) {
throw new ForbiddenException('Cannot delete admin user'); throw new ForbiddenException('Cannot delete admin user');
@ -78,35 +74,28 @@ export class UserService {
return this.userRepository.delete(user).then(mapUser); return this.userRepository.delete(user).then(mapUser);
} }
async restore(authUser: AuthUserDto, id: string): Promise<UserResponseDto> { async restore(auth: AuthDto, id: string): Promise<UserResponseDto> {
if (!authUser.isAdmin) {
throw new ForbiddenException('Unauthorized');
}
let user = await this.findOrFail(id, { withDeleted: true }); let user = await this.findOrFail(id, { withDeleted: true });
user = await this.userRepository.restore(user); user = await this.userRepository.restore(user);
await this.albumRepository.restoreAll(id); await this.albumRepository.restoreAll(id);
return mapUser(user); return mapUser(user);
} }
async createProfileImage( async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
authUser: AuthUserDto, const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false });
fileInfo: Express.Multer.File, const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path });
): Promise<CreateProfileImageResponseDto> {
const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false });
const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path });
if (oldpath !== '') { if (oldpath !== '') {
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } });
} }
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath); return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
} }
async deleteProfileImage(authUser: AuthUserDto): Promise<void> { async deleteProfileImage(auth: AuthDto): Promise<void> {
const user = await this.findOrFail(authUser.id, { withDeleted: false }); const user = await this.findOrFail(auth.user.id, { withDeleted: false });
if (user.profileImagePath === '') { if (user.profileImagePath === '') {
throw new BadRequestException("Can't delete a missing profile Image"); 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] } }); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } });
} }

View File

@ -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 = { export const CLI_USER: AuthDto = {
id: 'cli', user: {
email: 'cli@immich.app', id: 'cli',
isAdmin: true, email: 'cli@immich.app',
isPublicUser: false, isAdmin: true,
isAllowUpload: true, } as UserEntity,
}; };

View File

@ -1,4 +1,4 @@
import { AssetResponseDto, AuthUserDto } from '@app/domain'; import { AssetResponseDto, AuthDto } from '@app/domain';
import { import {
Body, Body,
Controller, Controller,
@ -16,7 +16,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Response as Res } from 'express'; 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 { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors'; import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors';
import FileNotEmptyValidator from '../validation/file-not-empty-validator'; import FileNotEmptyValidator from '../validation/file-not-empty-validator';
@ -55,7 +55,7 @@ export class AssetController {
type: CreateAssetDto, type: CreateAssetDto,
}) })
async uploadFile( async uploadFile(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles, @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles,
@Body(new ValidationPipe({ transform: true })) dto: CreateAssetDto, @Body(new ValidationPipe({ transform: true })) dto: CreateAssetDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@ -73,7 +73,7 @@ export class AssetController {
sidecarFile = mapToUploadFile(_sidecarFile); 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) { if (responseDto.duplicate) {
res.status(HttpStatus.OK); res.status(HttpStatus.OK);
} }
@ -89,12 +89,12 @@ export class AssetController {
}, },
}) })
async serveFile( async serveFile(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Response() res: Res, @Response() res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto, @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
) { ) {
await this.assetService.serveFile(authUser, id, query, res); await this.assetService.serveFile(auth, id, query, res);
} }
@SharedLinkRoute() @SharedLinkRoute()
@ -106,27 +106,27 @@ export class AssetController {
}, },
}) })
async getAssetThumbnail( async getAssetThumbnail(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Response() res: Res, @Response() res: Res,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto, @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') @Get('/curated-objects')
getCuratedObjects(@AuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> { getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(authUser); return this.assetService.getCuratedObject(auth);
} }
@Get('/curated-locations') @Get('/curated-locations')
getCuratedLocations(@AuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> { getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(authUser); return this.assetService.getCuratedLocation(auth);
} }
@Get('/search-terms') @Get('/search-terms')
getAssetSearchTerms(@AuthUser() authUser: AuthUserDto): Promise<string[]> { getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
return this.assetService.getAssetSearchTerm(authUser); return this.assetService.getAssetSearchTerm(auth);
} }
/** /**
@ -140,10 +140,10 @@ export class AssetController {
schema: { type: 'string' }, schema: { type: 'string' },
}) })
getAllAssets( getAllAssets(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto, @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
): Promise<AssetResponseDto[]> { ): Promise<AssetResponseDto[]> {
return this.assetService.getAllAssets(authUser, dto); return this.assetService.getAllAssets(auth, dto);
} }
/** /**
@ -151,8 +151,8 @@ export class AssetController {
*/ */
@Get('/:deviceId') @Get('/:deviceId')
@ApiOperation({ deprecated: true, summary: 'Use /asset/device/:deviceId instead - Remove in 1.92 release' }) @ApiOperation({ deprecated: true, summary: 'Use /asset/device/:deviceId instead - Remove in 1.92 release' })
getUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { getUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.assetService.getUserAssetsByDeviceId(authUser, deviceId); return this.assetService.getUserAssetsByDeviceId(auth, deviceId);
} }
/** /**
@ -160,8 +160,8 @@ export class AssetController {
*/ */
@SharedLinkRoute() @SharedLinkRoute()
@Get('/assetById/:id') @Get('/assetById/:id')
getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> { getAssetById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.assetService.getAssetById(authUser, id) as Promise<AssetResponseDto>; return this.assetService.getAssetById(auth, id) as Promise<AssetResponseDto>;
} }
/** /**
@ -170,10 +170,10 @@ export class AssetController {
@Post('/exist') @Post('/exist')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
checkExistingAssets( checkExistingAssets(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Body(ValidationPipe) dto: CheckExistingAssetsDto, @Body(ValidationPipe) dto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> { ): Promise<CheckExistingAssetsResponseDto> {
return this.assetService.checkExistingAssets(authUser, dto); return this.assetService.checkExistingAssets(auth, dto);
} }
/** /**
@ -182,9 +182,9 @@ export class AssetController {
@Post('/bulk-upload-check') @Post('/bulk-upload-check')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
checkBulkUpload( checkBulkUpload(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto, @Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> { ): Promise<AssetBulkUploadCheckResponseDto> {
return this.assetService.bulkUploadCheck(authUser, dto); return this.assetService.bulkUploadCheck(auth, dto);
} }
} }

View File

@ -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 { AssetEntity } from '@app/infra/entities';
import { parse } from 'node:path'; import { parse } from 'node:path';
import { IAssetRepository } from './asset-repository'; import { IAssetRepository } from './asset-repository';
@ -11,14 +11,14 @@ export class AssetCore {
) {} ) {}
async create( async create(
authUser: AuthUserDto, auth: AuthDto,
dto: CreateAssetDto & { libraryId: string }, dto: CreateAssetDto & { libraryId: string },
file: UploadFile, file: UploadFile,
livePhotoAssetId?: string, livePhotoAssetId?: string,
sidecarPath?: string, sidecarPath?: string,
): Promise<AssetEntity> { ): Promise<AssetEntity> {
const asset = await this.repository.create({ const asset = await this.repository.create({
ownerId: authUser.id, ownerId: auth.user.id,
libraryId: dto.libraryId, libraryId: dto.libraryId,
checksum: file.checksum, checksum: file.checksum,

View File

@ -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])); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image); assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id); 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 () => { it('should allow shared link access', async () => {
@ -243,7 +246,7 @@ describe('AssetService', () => {
assetRepositoryMock.getById.mockResolvedValue(assetStub.image); assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.adminSharedLink, assetStub.image.id); await sut.getAssetById(authStub.adminSharedLink, assetStub.image.id);
expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith( expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId, authStub.adminSharedLink.sharedLink?.id,
new Set([assetStub.image.id]), new Set([assetStub.image.id]),
); );
}); });
@ -253,7 +256,7 @@ describe('AssetService', () => {
assetRepositoryMock.getById.mockResolvedValue(assetStub.image); assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id); await sut.getAssetById(authStub.admin, assetStub.image.id);
expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith( expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(
authStub.admin.id, authStub.admin.user.id,
new Set([assetStub.image.id]), new Set([assetStub.image.id]),
); );
}); });
@ -262,7 +265,10 @@ describe('AssetService', () => {
accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id])); accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetRepositoryMock.getById.mockResolvedValue(assetStub.image); assetRepositoryMock.getById.mockResolvedValue(assetStub.image);
await sut.getAssetById(authStub.admin, assetStub.image.id); 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 () => { it('should throw an error for no access', async () => {

View File

@ -1,7 +1,7 @@
import { import {
AccessCore, AccessCore,
AssetResponseDto, AssetResponseDto,
AuthUserDto, AuthDto,
getLivePhotoMotionFilename, getLivePhotoMotionFilename,
IAccessRepository, IAccessRepository,
IJobRepository, IJobRepository,
@ -65,7 +65,7 @@ export class AssetService {
} }
public async uploadFile( public async uploadFile(
authUser: AuthUserDto, auth: AuthDto,
dto: CreateAssetDto, dto: CreateAssetDto,
file: UploadFile, file: UploadFile,
livePhotoFile?: UploadFile, livePhotoFile?: UploadFile,
@ -81,15 +81,15 @@ export class AssetService {
let livePhotoAsset: AssetEntity | null = null; let livePhotoAsset: AssetEntity | null = null;
try { try {
const libraryId = await this.getLibraryId(authUser, dto.libraryId); const libraryId = await this.getLibraryId(auth, dto.libraryId);
await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId); await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
if (livePhotoFile) { if (livePhotoFile) {
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId }; 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( const asset = await this.assetCore.create(
authUser, auth,
{ ...dto, libraryId }, { ...dto, libraryId },
file, file,
livePhotoAsset?.id, livePhotoAsset?.id,
@ -107,7 +107,7 @@ export class AssetService {
// handle duplicates with a success response // handle duplicates with a success response
if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) { if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) {
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum); 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 }; return { id: duplicate.id, duplicate: true };
} }
@ -116,33 +116,29 @@ export class AssetService {
} }
} }
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { public async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) {
return this._assetRepository.getAllByDeviceId(authUser.id, deviceId); return this._assetRepository.getAllByDeviceId(auth.user.id, deviceId);
} }
public async getAllAssets(authUser: AuthUserDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> { public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || authUser.id; const userId = dto.userId || auth.user.id;
await this.access.requirePermission(authUser, Permission.TIMELINE_READ, userId); await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const assets = await this._assetRepository.getAllByUserId(userId, dto); const assets = await this._assetRepository.getAllByUserId(userId, dto);
return assets.map((asset) => mapAsset(asset)); return assets.map((asset) => mapAsset(asset));
} }
public async getAssetById( public async getAssetById(auth: AuthDto, assetId: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
authUser: AuthUserDto, await this.access.requirePermission(auth, Permission.ASSET_READ, assetId);
assetId: string,
): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
const includeMetadata = this.getExifPermission(authUser);
const asset = await this._assetRepository.getById(assetId); const asset = await this._assetRepository.getById(assetId);
if (includeMetadata) { if (!auth.sharedLink || auth.sharedLink?.showExif) {
const data = mapAsset(asset, { withStack: true }); const data = mapAsset(asset, { withStack: true });
if (data.ownerId !== authUser.id) { if (data.ownerId !== auth.user.id) {
data.people = []; data.people = [];
} }
if (authUser.isPublicUser) { if (auth.sharedLink) {
delete data.owner; delete data.owner;
} }
@ -152,8 +148,8 @@ export class AssetService {
} }
} }
async serveThumbnail(authUser: AuthUserDto, assetId: string, query: GetAssetThumbnailDto, res: Res) { async serveThumbnail(auth: AuthDto, assetId: string, query: GetAssetThumbnailDto, res: Res) {
await this.access.requirePermission(authUser, Permission.ASSET_VIEW, assetId); await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
const asset = await this._assetRepository.get(assetId); const asset = await this._assetRepository.get(assetId);
if (!asset) { 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 // 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); const asset = await this._assetRepository.getById(assetId);
if (!asset) { if (!asset) {
throw new NotFoundException('Asset does not exist'); throw new NotFoundException('Asset does not exist');
} }
const allowOriginalFile = !!(!authUser.isPublicUser || authUser.isAllowDownload); const allowOriginalFile = !!(!auth.sharedLink || auth.sharedLink?.allowDownload);
const filepath = const filepath =
asset.type === AssetType.IMAGE asset.type === AssetType.IMAGE
@ -191,10 +187,10 @@ export class AssetService {
await this.sendFile(res, filepath); await this.sendFile(res, filepath);
} }
async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> { async getAssetSearchTerm(auth: AuthDto): Promise<string[]> {
const possibleSearchTerm = new Set<string>(); const possibleSearchTerm = new Set<string>();
const rows = await this._assetRepository.getSearchPropertiesByUserId(authUser.id); const rows = await this._assetRepository.getSearchPropertiesByUserId(auth.user.id);
rows.forEach((row: SearchPropertiesDto) => { rows.forEach((row: SearchPropertiesDto) => {
// tags // tags
row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); 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 != ''); return Array.from(possibleSearchTerm).filter((x) => x != null && x != '');
} }
async getCuratedLocation(authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> { async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
return this._assetRepository.getLocationsByUserId(authUser.id); return this._assetRepository.getLocationsByUserId(auth.user.id);
} }
async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> { async getCuratedObject(auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
return this._assetRepository.getDetectedObjectsByUserId(authUser.id); return this._assetRepository.getDetectedObjectsByUserId(auth.user.id);
} }
async checkExistingAssets( async checkExistingAssets(
authUser: AuthUserDto, auth: AuthDto,
checkExistingAssetsDto: CheckExistingAssetsDto, checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> { ): Promise<CheckExistingAssetsResponseDto> {
return { 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<AssetBulkUploadCheckResponseDto> { async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
// support base64 and hex checksums // support base64 and hex checksums
for (const asset of dto.assets) { for (const asset of dto.assets) {
if (asset.checksum.length === 28) { 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 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<string, string> = {}; const checksumMap: Record<string, string> = {};
for (const { id, checksum } of results) { 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) { private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
switch (format) { switch (format) {
case GetAssetThumbnailFormatEnum.WEBP: 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) { if (libraryId) {
return libraryId; return libraryId;
} }
let library = await this.libraryRepository.getDefaultUploadLibrary(authUser.id); let library = await this.libraryRepository.getDefaultUploadLibrary(auth.user.id);
if (!library) { if (!library) {
library = await this.libraryRepository.create({ library = await this.libraryRepository.create({
ownerId: authUser.id, ownerId: auth.user.id,
name: 'Default Library', name: 'Default Library',
assets: [], assets: [],
type: LibraryType.UPLOAD, type: LibraryType.UPLOAD,

View File

@ -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 { import {
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,
@ -50,8 +50,8 @@ export const SharedLinkRoute = () =>
applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false })); 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 AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value);
export const AuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => { export const Auth = createParamDecorator((data, ctx: ExecutionContext): AuthDto => {
return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user; return ctx.switchToHttp().getRequest<{ user: AuthDto }>().user;
}); });
export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => { export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => {
@ -67,7 +67,7 @@ export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext
}); });
export interface AuthRequest extends Request { export interface AuthRequest extends Request {
user?: AuthUserDto; user?: AuthDto;
} }
@Injectable() @Injectable()
@ -93,12 +93,12 @@ export class AppGuard implements CanActivate {
const req = context.switchToHttp().getRequest<AuthRequest>(); const req = context.switchToHttp().getRequest<AuthRequest>();
const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>); const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>);
if (authDto.isPublicUser && !isSharedRoute) { if (authDto.sharedLink && !isSharedRoute) {
this.logger.warn(`Denied access to non-shared route: ${req.path}`); this.logger.warn(`Denied access to non-shared route: ${req.path}`);
return false; return false;
} }
if (isAdminRoute && !authDto.isAdmin) { if (isAdminRoute && !authDto.user.isAdmin) {
this.logger.warn(`Denied access to admin only route: ${req.path}`); this.logger.warn(`Denied access to admin only route: ${req.path}`);
return false; return false;
} }

View File

@ -1,4 +1,4 @@
import { AuthUserDto } from '@app/domain'; import { AuthDto } from '@app/domain';
import { import {
ActivityDto, ActivityDto,
ActivitySearchDto, ActivitySearchDto,
@ -10,7 +10,7 @@ import {
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Response } from 'express'; import { Response } from 'express';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -22,22 +22,22 @@ export class ActivityController {
constructor(private service: ActivityService) {} constructor(private service: ActivityService) {}
@Get() @Get()
getActivities(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivitySearchDto): Promise<ResponseDto[]> { getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ResponseDto[]> {
return this.service.getAll(authUser, dto); return this.service.getAll(auth, dto);
} }
@Get('statistics') @Get('statistics')
getActivityStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: ActivityDto): Promise<StatsResponseDto> { getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<StatsResponseDto> {
return this.service.getStatistics(authUser, dto); return this.service.getStatistics(auth, dto);
} }
@Post() @Post()
async createActivity( async createActivity(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Body() dto: CreateDto, @Body() dto: CreateDto,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
): Promise<ResponseDto> { ): Promise<ResponseDto> {
const { duplicate, value } = await this.service.create(authUser, dto); const { duplicate, value } = await this.service.create(auth, dto);
if (duplicate) { if (duplicate) {
res.status(HttpStatus.OK); res.status(HttpStatus.OK);
} }
@ -46,7 +46,7 @@ export class ActivityController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteActivity(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id); return this.service.delete(auth, id);
} }
} }

View File

@ -4,7 +4,7 @@ import {
AlbumInfoDto, AlbumInfoDto,
AlbumResponseDto, AlbumResponseDto,
AlbumService, AlbumService,
AuthUserDto, AuthDto,
BulkIdResponseDto, BulkIdResponseDto,
BulkIdsDto, BulkIdsDto,
CreateAlbumDto as CreateDto, CreateAlbumDto as CreateDto,
@ -14,7 +14,7 @@ import {
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe'; 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 { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -26,78 +26,78 @@ export class AlbumController {
constructor(private service: AlbumService) {} constructor(private service: AlbumService) {}
@Get('count') @Get('count')
getAlbumCount(@AuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> { getAlbumCount(@Auth() auth: AuthDto): Promise<AlbumCountResponseDto> {
return this.service.getCount(authUser); return this.service.getCount(auth);
} }
@Get() @Get()
getAllAlbums(@AuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> { getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(authUser, query); return this.service.getAll(auth, query);
} }
@Post() @Post()
createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise<AlbumResponseDto> { createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise<AlbumResponseDto> {
return this.service.create(authUser, dto); return this.service.create(auth, dto);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Get(':id') @Get(':id')
getAlbumInfo( getAlbumInfo(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Query() dto: AlbumInfoDto, @Query() dto: AlbumInfoDto,
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
return this.service.get(authUser, id, dto); return this.service.get(auth, id, dto);
} }
@Patch(':id') @Patch(':id')
updateAlbumInfo( updateAlbumInfo(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: UpdateDto, @Body() dto: UpdateDto,
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
return this.service.update(authUser, id, dto); return this.service.update(auth, id, dto);
} }
@Delete(':id') @Delete(':id')
deleteAlbum(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(authUser, id); return this.service.delete(auth, id);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Put(':id/assets') @Put(':id/assets')
addAssetsToAlbum( addAssetsToAlbum(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: BulkIdsDto, @Body() dto: BulkIdsDto,
): Promise<BulkIdResponseDto[]> { ): Promise<BulkIdResponseDto[]> {
return this.service.addAssets(authUser, id, dto); return this.service.addAssets(auth, id, dto);
} }
@Delete(':id/assets') @Delete(':id/assets')
removeAssetFromAlbum( removeAssetFromAlbum(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Body() dto: BulkIdsDto, @Body() dto: BulkIdsDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
): Promise<BulkIdResponseDto[]> { ): Promise<BulkIdResponseDto[]> {
return this.service.removeAssets(authUser, id, dto); return this.service.removeAssets(auth, id, dto);
} }
@Put(':id/users') @Put(':id/users')
addUsersToAlbum( addUsersToAlbum(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: AddUsersDto, @Body() dto: AddUsersDto,
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
return this.service.addUsers(authUser, id, dto); return this.service.addUsers(auth, id, dto);
} }
@Delete(':id/user/:userId') @Delete(':id/user/:userId')
removeUserFromAlbum( removeUserFromAlbum(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string, @Param('userId', new ParseMeUUIDPipe({ version: '4' })) userId: string,
) { ) {
return this.service.removeUser(authUser, id, userId); return this.service.removeUser(auth, id, userId);
} }
} }

View File

@ -4,11 +4,11 @@ import {
APIKeyResponseDto, APIKeyResponseDto,
APIKeyService, APIKeyService,
APIKeyUpdateDto, APIKeyUpdateDto,
AuthUserDto, AuthDto,
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -20,31 +20,31 @@ export class APIKeyController {
constructor(private service: APIKeyService) {} constructor(private service: APIKeyService) {}
@Post() @Post()
createApiKey(@AuthUser() authUser: AuthUserDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(authUser, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
getApiKeys(@AuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> { getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(authUser); return this.service.getAll(auth);
} }
@Get(':id') @Get(':id')
getApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> { getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(authUser, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
updateApiKey( updateApiKey(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: APIKeyUpdateDto, @Body() dto: APIKeyUpdateDto,
): Promise<APIKeyResponseDto> { ): Promise<APIKeyResponseDto> {
return this.service.update(authUser, id, dto); return this.service.update(auth, id, dto);
} }
@Delete(':id') @Delete(':id')
deleteApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id); return this.service.delete(auth, id);
} }
} }

View File

@ -8,7 +8,7 @@ import {
AssetService, AssetService,
AssetStatsDto, AssetStatsDto,
AssetStatsResponseDto, AssetStatsResponseDto,
AuthUserDto, AuthDto,
BulkIdsDto, BulkIdsDto,
DownloadInfoDto, DownloadInfoDto,
DownloadResponseDto, DownloadResponseDto,
@ -39,7 +39,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { DeviceIdDto } from '../api-v1/asset/dto/device-id.dto'; 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 { UseValidation, asStreamableFile } from '../app.utils';
import { Route } from '../interceptors'; import { Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -52,8 +52,8 @@ export class AssetsController {
constructor(private service: AssetService) {} constructor(private service: AssetService) {}
@Get() @Get()
searchAssets(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> { searchAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
return this.service.search(authUser, dto); return this.service.search(auth, dto);
} }
} }
@ -65,115 +65,111 @@ export class AssetController {
constructor(private service: AssetService) {} constructor(private service: AssetService) {}
@Get('map-marker') @Get('map-marker')
getMapMarkers(@AuthUser() authUser: AuthUserDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> { getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
return this.service.getMapMarkers(authUser, options); return this.service.getMapMarkers(auth, options);
} }
@Get('memory-lane') @Get('memory-lane')
getMemoryLane(@AuthUser() authUser: AuthUserDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> { getMemoryLane(@Auth() auth: AuthDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(authUser, dto); return this.service.getMemoryLane(auth, dto);
} }
@Get('random') @Get('random')
getRandom(@AuthUser() authUser: AuthUserDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> { getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
return this.service.getRandom(authUser, dto.count ?? 1); return this.service.getRandom(auth, dto.count ?? 1);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Post('download/info') @Post('download/info')
getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> { getDownloadInfo(@Auth() auth: AuthDto, @Body() dto: DownloadInfoDto): Promise<DownloadResponseDto> {
return this.service.getDownloadInfo(authUser, dto); return this.service.getDownloadInfo(auth, dto);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Post('download/archive') @Post('download/archive')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
downloadArchive(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> { downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise<StreamableFile> {
return this.service.downloadArchive(authUser, dto).then(asStreamableFile); return this.service.downloadArchive(auth, dto).then(asStreamableFile);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Post('download/:id') @Post('download/:id')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { downloadFile(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.downloadFile(authUser, id).then(asStreamableFile); return this.service.downloadFile(auth, id).then(asStreamableFile);
} }
/** /**
* Get all asset of a device that are in the database, ID only. * Get all asset of a device that are in the database, ID only.
*/ */
@Get('/device/:deviceId') @Get('/device/:deviceId')
getAllUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(authUser, deviceId); return this.service.getUserAssetsByDeviceId(auth, deviceId);
} }
@Get('statistics') @Get('statistics')
getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> { getAssetStatistics(@Auth() auth: AuthDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(authUser, dto); return this.service.getStatistics(auth, dto);
} }
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Get('time-buckets') @Get('time-buckets')
getTimeBuckets(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> { getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
return this.service.getTimeBuckets(authUser, dto); return this.service.getTimeBuckets(auth, dto);
} }
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Get('time-bucket') @Get('time-bucket')
getTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> { getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>; return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
} }
@Post('jobs') @Post('jobs')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
runAssetJobs(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetJobsDto): Promise<void> { runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
return this.service.run(authUser, dto); return this.service.run(auth, dto);
} }
@Put() @Put()
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> { updateAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkUpdateDto): Promise<void> {
return this.service.updateAll(authUser, dto); return this.service.updateAll(auth, dto);
} }
@Delete() @Delete()
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkDeleteDto): Promise<void> { deleteAssets(@Auth() auth: AuthDto, @Body() dto: AssetBulkDeleteDto): Promise<void> {
return this.service.deleteAll(authUser, dto); return this.service.deleteAll(auth, dto);
} }
@Post('restore') @Post('restore')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
restoreAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: BulkIdsDto): Promise<void> { restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.restoreAll(authUser, dto); return this.service.restoreAll(auth, dto);
} }
@Post('trash/empty') @Post('trash/empty')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
emptyTrash(@AuthUser() authUser: AuthUserDto): Promise<void> { emptyTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.handleTrashAction(authUser, TrashAction.EMPTY_ALL); return this.service.handleTrashAction(auth, TrashAction.EMPTY_ALL);
} }
@Post('trash/restore') @Post('trash/restore')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
restoreTrash(@AuthUser() authUser: AuthUserDto): Promise<void> { restoreTrash(@Auth() auth: AuthDto): Promise<void> {
return this.service.handleTrashAction(authUser, TrashAction.RESTORE_ALL); return this.service.handleTrashAction(auth, TrashAction.RESTORE_ALL);
} }
@Put('stack/parent') @Put('stack/parent')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
updateStackParent(@AuthUser() authUser: AuthUserDto, @Body() dto: UpdateStackParentDto): Promise<void> { updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise<void> {
return this.service.updateStackParent(authUser, dto); return this.service.updateStackParent(auth, dto);
} }
@Put(':id') @Put(':id')
updateAsset( updateAsset(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<AssetResponseDto> {
@AuthUser() authUser: AuthUserDto, return this.service.update(auth, id, dto);
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateDto,
): Promise<AssetResponseDto> {
return this.service.update(authUser, id, dto);
} }
} }

View File

@ -2,7 +2,7 @@ import {
AuditDeletesDto, AuditDeletesDto,
AuditDeletesResponseDto, AuditDeletesResponseDto,
AuditService, AuditService,
AuthUserDto, AuthDto,
FileChecksumDto, FileChecksumDto,
FileChecksumResponseDto, FileChecksumResponseDto,
FileReportDto, FileReportDto,
@ -10,7 +10,7 @@ import {
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Get, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AdminRoute, AuthUser, Authenticated } from '../app.guard'; import { AdminRoute, Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
@ApiTags('Audit') @ApiTags('Audit')
@ -21,8 +21,8 @@ export class AuditController {
constructor(private service: AuditService) {} constructor(private service: AuditService) {}
@Get('deletes') @Get('deletes')
getAuditDeletes(@AuthUser() authUser: AuthUserDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> { getAuditDeletes(@Auth() auth: AuthDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
return this.service.getDeletes(authUser, dto); return this.service.getDeletes(auth, dto);
} }
@AdminRoute() @AdminRoute()

View File

@ -1,7 +1,7 @@
import { import {
AuthDeviceResponseDto, AuthDeviceResponseDto,
AuthDto,
AuthService, AuthService,
AuthUserDto,
ChangePasswordDto, ChangePasswordDto,
IMMICH_ACCESS_COOKIE, IMMICH_ACCESS_COOKIE,
IMMICH_AUTH_TYPE_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 { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; 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 { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -47,20 +47,20 @@ export class AuthController {
} }
@Get('devices') @Get('devices')
getAuthDevices(@AuthUser() authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> { getAuthDevices(@Auth() auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
return this.service.getDevices(authUser); return this.service.getDevices(auth);
} }
@Delete('devices') @Delete('devices')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
logoutAuthDevices(@AuthUser() authUser: AuthUserDto): Promise<void> { logoutAuthDevices(@Auth() auth: AuthDto): Promise<void> {
return this.service.logoutDevices(authUser); return this.service.logoutDevices(auth);
} }
@Delete('devices/:id') @Delete('devices/:id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
logoutAuthDevice(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { logoutAuthDevice(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.logoutDevice(authUser, id); return this.service.logoutDevice(auth, id);
} }
@Post('validateToken') @Post('validateToken')
@ -71,8 +71,8 @@ export class AuthController {
@Post('change-password') @Post('change-password')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> { changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.service.changePassword(authUser, dto).then(mapUser); return this.service.changePassword(auth, dto).then(mapUser);
} }
@Post('logout') @Post('logout')
@ -80,11 +80,11 @@ export class AuthController {
logout( logout(
@Req() req: Request, @Req() req: Request,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
): Promise<LogoutResponseDto> { ): Promise<LogoutResponseDto> {
res.clearCookie(IMMICH_ACCESS_COOKIE); res.clearCookie(IMMICH_ACCESS_COOKIE);
res.clearCookie(IMMICH_AUTH_TYPE_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]);
} }
} }

View File

@ -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 { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -13,16 +13,16 @@ export class FaceController {
constructor(private service: PersonService) {} constructor(private service: PersonService) {}
@Get() @Get()
getFaces(@AuthUser() authUser: AuthUserDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> { getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
return this.service.getFacesById(authUser, dto); return this.service.getFacesById(auth, dto);
} }
@Put(':id') @Put(':id')
reassignFacesById( reassignFacesById(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: FaceDto, @Body() dto: FaceDto,
): Promise<PersonResponseDto> { ): Promise<PersonResponseDto> {
return this.service.reassignFacesById(authUser, id, dto); return this.service.reassignFacesById(auth, id, dto);
} }
} }

View File

@ -1,5 +1,5 @@
import { import {
AuthUserDto, AuthDto,
CreateLibraryDto as CreateDto, CreateLibraryDto as CreateDto,
LibraryService, LibraryService,
LibraryStatsResponseDto, LibraryStatsResponseDto,
@ -9,7 +9,7 @@ import {
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -21,49 +21,42 @@ export class LibraryController {
constructor(private service: LibraryService) {} constructor(private service: LibraryService) {}
@Get() @Get()
getLibraries(@AuthUser() authUser: AuthUserDto): Promise<ResponseDto[]> { getLibraries(@Auth() auth: AuthDto): Promise<ResponseDto[]> {
return this.service.getAllForUser(authUser); return this.service.getAllForUser(auth);
} }
@Post() @Post()
createLibrary(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise<ResponseDto> { createLibrary(@Auth() auth: AuthDto, @Body() dto: CreateDto): Promise<ResponseDto> {
return this.service.create(authUser, dto); return this.service.create(auth, dto);
} }
@Put(':id') @Put(':id')
updateLibrary( updateLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto): Promise<ResponseDto> {
@AuthUser() authUser: AuthUserDto, return this.service.update(auth, id, dto);
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateDto,
): Promise<ResponseDto> {
return this.service.update(authUser, id, dto);
} }
@Get(':id') @Get(':id')
getLibraryInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> { getLibraryInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<ResponseDto> {
return this.service.get(authUser, id); return this.service.get(auth, id);
} }
@Delete(':id') @Delete(':id')
deleteLibrary(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id); return this.service.delete(auth, id);
} }
@Get(':id/statistics') @Get(':id/statistics')
getLibraryStatistics( getLibraryStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
@AuthUser() authUser: AuthUserDto, return this.service.getStatistics(auth, id);
@Param() { id }: UUIDParamDto,
): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(authUser, id);
} }
@Post(':id/scan') @Post(':id/scan')
scanLibrary(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) { scanLibrary(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
return this.service.queueScan(authUser, id, dto); return this.service.queueScan(auth, id, dto);
} }
@Post(':id/removeOffline') @Post(':id/removeOffline')
removeOfflineFiles(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { removeOfflineFiles(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.queueRemoveOffline(authUser, id); return this.service.queueRemoveOffline(auth, id);
} }
} }

View File

@ -1,6 +1,6 @@
import { import {
AuthDto,
AuthService, AuthService,
AuthUserDto,
LoginDetails, LoginDetails,
LoginResponseDto, LoginResponseDto,
OAuthAuthorizeResponseDto, OAuthAuthorizeResponseDto,
@ -12,7 +12,7 @@ import {
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; 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 { UseValidation } from '../app.utils';
@ApiTags('OAuth') @ApiTags('OAuth')
@ -58,12 +58,12 @@ export class OAuthController {
} }
@Post('link') @Post('link')
linkOAuthAccount(@AuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> { linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(authUser, dto); return this.service.link(auth, dto);
} }
@Post('unlink') @Post('unlink')
unlinkOAuthAccount(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> { unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.unlink(authUser); return this.service.unlink(auth);
} }
} }

View File

@ -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 { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partner.dto';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -15,29 +15,26 @@ export class PartnerController {
@Get() @Get()
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
getPartners( getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> {
@AuthUser() authUser: AuthUserDto, return this.service.getAll(auth, direction);
@Query('direction') direction: PartnerDirection,
): Promise<PartnerResponseDto[]> {
return this.service.getAll(authUser, direction);
} }
@Post(':id') @Post(':id')
createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> { createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(authUser, id); return this.service.create(auth, id);
} }
@Put(':id') @Put(':id')
updatePartner( updatePartner(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: UpdatePartnerDto, @Body() dto: UpdatePartnerDto,
): Promise<PartnerResponseDto> { ): Promise<PartnerResponseDto> {
return this.service.update(authUser, id, dto); return this.service.update(auth, id, dto);
} }
@Delete(':id') @Delete(':id')
removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id); return this.service.remove(auth, id);
} }
} }

View File

@ -1,7 +1,7 @@
import { import {
AssetFaceUpdateDto, AssetFaceUpdateDto,
AssetResponseDto, AssetResponseDto,
AuthUserDto, AuthDto,
BulkIdResponseDto, BulkIdResponseDto,
ImmichReadStream, ImmichReadStream,
MergePersonDto, MergePersonDto,
@ -15,7 +15,7 @@ import {
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common'; import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -31,49 +31,46 @@ export class PersonController {
constructor(private service: PersonService) {} constructor(private service: PersonService) {}
@Get() @Get()
getAllPeople(@AuthUser() authUser: AuthUserDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> { getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(authUser, withHidden); return this.service.getAll(auth, withHidden);
} }
@Post() @Post()
createPerson(@AuthUser() authUser: AuthUserDto): Promise<PersonResponseDto> { createPerson(@Auth() auth: AuthDto): Promise<PersonResponseDto> {
return this.service.createPerson(authUser); return this.service.createPerson(auth);
} }
@Put(':id/reassign') @Put(':id/reassign')
reassignFaces( reassignFaces(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: AssetFaceUpdateDto, @Body() dto: AssetFaceUpdateDto,
): Promise<PersonResponseDto[]> { ): Promise<PersonResponseDto[]> {
return this.service.reassignFaces(authUser, id, dto); return this.service.reassignFaces(auth, id, dto);
} }
@Put() @Put()
updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> { updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updatePeople(authUser, dto); return this.service.updatePeople(auth, dto);
} }
@Get(':id') @Get(':id')
getPerson(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> { getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(authUser, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
updatePerson( updatePerson(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: PersonUpdateDto, @Body() dto: PersonUpdateDto,
): Promise<PersonResponseDto> { ): Promise<PersonResponseDto> {
return this.service.update(authUser, id, dto); return this.service.update(auth, id, dto);
} }
@Get(':id/statistics') @Get(':id/statistics')
getPersonStatistics( getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
@AuthUser() authUser: AuthUserDto, return this.service.getStatistics(auth, id);
@Param() { id }: UUIDParamDto,
): Promise<PersonStatisticsResponseDto> {
return this.service.getStatistics(authUser, id);
} }
@Get(':id/thumbnail') @Get(':id/thumbnail')
@ -82,21 +79,21 @@ export class PersonController {
'image/jpeg': { schema: { type: 'string', format: 'binary' } }, 'image/jpeg': { schema: { type: 'string', format: 'binary' } },
}, },
}) })
getPersonThumbnail(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { getPersonThumbnail(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.getThumbnail(authUser, id).then(asStreamableFile); return this.service.getThumbnail(auth, id).then(asStreamableFile);
} }
@Get(':id/assets') @Get(':id/assets')
getPersonAssets(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> { getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(authUser, id); return this.service.getAssets(auth, id);
} }
@Post(':id/merge') @Post(':id/merge')
mergePerson( mergePerson(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: MergePersonDto, @Body() dto: MergePersonDto,
): Promise<BulkIdResponseDto[]> { ): Promise<BulkIdResponseDto[]> {
return this.service.mergePerson(authUser, id, dto); return this.service.mergePerson(auth, id, dto);
} }
} }

View File

@ -1,5 +1,5 @@
import { import {
AuthUserDto, AuthDto,
PersonResponseDto, PersonResponseDto,
SearchDto, SearchDto,
SearchExploreResponseDto, SearchExploreResponseDto,
@ -9,7 +9,7 @@ import {
} from '@app/domain'; } from '@app/domain';
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
@ApiTags('Search') @ApiTags('Search')
@ -20,17 +20,17 @@ export class SearchController {
constructor(private service: SearchService) {} constructor(private service: SearchService) {}
@Get() @Get()
search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise<SearchResponseDto> { search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
return this.service.search(authUser, dto); return this.service.search(auth, dto);
} }
@Get('explore') @Get('explore')
getExploreData(@AuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> { getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
return this.service.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>; return this.service.getExploreData(auth) as Promise<SearchExploreResponseDto[]>;
} }
@Get('person') @Get('person')
searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> { searchPerson(@Auth() auth: AuthDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.service.searchPerson(authUser, dto); return this.service.searchPerson(auth, dto);
} }
} }

View File

@ -1,7 +1,7 @@
import { import {
AssetIdsDto, AssetIdsDto,
AssetIdsResponseDto, AssetIdsResponseDto,
AuthUserDto, AuthDto,
IMMICH_SHARED_LINK_ACCESS_COOKIE, IMMICH_SHARED_LINK_ACCESS_COOKIE,
SharedLinkCreateDto, SharedLinkCreateDto,
SharedLinkEditDto, SharedLinkEditDto,
@ -12,7 +12,7 @@ import {
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; 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 { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -24,14 +24,14 @@ export class SharedLinkController {
constructor(private readonly service: SharedLinkService) {} constructor(private readonly service: SharedLinkService) {}
@Get() @Get()
getAllSharedLinks(@AuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> { getAllSharedLinks(@Auth() auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(authUser); return this.service.getAll(auth);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Get('me') @Get('me')
async getMySharedLink( async getMySharedLink(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Query() dto: SharedLinkPasswordDto, @Query() dto: SharedLinkPasswordDto,
@Req() req: Request, @Req() req: Request,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@ -40,58 +40,58 @@ export class SharedLinkController {
if (sharedLinkToken) { if (sharedLinkToken) {
dto.token = sharedLinkToken; dto.token = sharedLinkToken;
} }
const sharedLinkResponse = await this.service.getMine(authUser, dto); const response = await this.service.getMine(auth, dto);
if (sharedLinkResponse.token) { if (response.token) {
res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, sharedLinkResponse.token, { res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24), expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
httpOnly: true, httpOnly: true,
sameSite: 'lax', sameSite: 'lax',
}); });
} }
return sharedLinkResponse; return response;
} }
@Get(':id') @Get(':id')
getSharedLinkById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> { getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
return this.service.get(authUser, id); return this.service.get(auth, id);
} }
@Post() @Post()
createSharedLink(@AuthUser() authUser: AuthUserDto, @Body() dto: SharedLinkCreateDto) { createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) {
return this.service.create(authUser, dto); return this.service.create(auth, dto);
} }
@Patch(':id') @Patch(':id')
updateSharedLink( updateSharedLink(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: SharedLinkEditDto, @Body() dto: SharedLinkEditDto,
): Promise<SharedLinkResponseDto> { ): Promise<SharedLinkResponseDto> {
return this.service.update(authUser, id, dto); return this.service.update(auth, id, dto);
} }
@Delete(':id') @Delete(':id')
removeSharedLink(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id); return this.service.remove(auth, id);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Put(':id/assets') @Put(':id/assets')
addSharedLinkAssets( addSharedLinkAssets(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto, @Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> { ): Promise<AssetIdsResponseDto[]> {
return this.service.addAssets(authUser, id, dto); return this.service.addAssets(auth, id, dto);
} }
@SharedLinkRoute() @SharedLinkRoute()
@Delete(':id/assets') @Delete(':id/assets')
removeSharedLinkAssets( removeSharedLinkAssets(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto, @Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> { ): Promise<AssetIdsResponseDto[]> {
return this.service.removeAssets(authUser, id, dto); return this.service.removeAssets(auth, id, dto);
} }
} }

View File

@ -2,7 +2,7 @@ import {
AssetIdsDto, AssetIdsDto,
AssetIdsResponseDto, AssetIdsResponseDto,
AssetResponseDto, AssetResponseDto,
AuthUserDto, AuthDto,
CreateTagDto, CreateTagDto,
TagResponseDto, TagResponseDto,
TagService, TagService,
@ -10,7 +10,7 @@ import {
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { Auth, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -22,54 +22,50 @@ export class TagController {
constructor(private service: TagService) {} constructor(private service: TagService) {}
@Post() @Post()
createTag(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> { createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise<TagResponseDto> {
return this.service.create(authUser, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
getAllTags(@AuthUser() authUser: AuthUserDto): Promise<TagResponseDto[]> { getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> {
return this.service.getAll(authUser); return this.service.getAll(auth);
} }
@Get(':id') @Get(':id')
getTagById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> { getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.getById(authUser, id); return this.service.getById(auth, id);
} }
@Patch(':id') @Patch(':id')
updateTag( updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise<TagResponseDto> {
@AuthUser() authUser: AuthUserDto, return this.service.update(auth, id, dto);
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateTagDto,
): Promise<TagResponseDto> {
return this.service.update(authUser, id, dto);
} }
@Delete(':id') @Delete(':id')
deleteTag(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id); return this.service.remove(auth, id);
} }
@Get(':id/assets') @Get(':id/assets')
getTagAssets(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> { getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto[]> {
return this.service.getAssets(authUser, id); return this.service.getAssets(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
tagAssets( tagAssets(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: AssetIdsDto, @Body() dto: AssetIdsDto,
): Promise<AssetIdsResponseDto[]> { ): Promise<AssetIdsResponseDto[]> {
return this.service.addAssets(authUser, id, dto); return this.service.addAssets(auth, id, dto);
} }
@Delete(':id/assets') @Delete(':id/assets')
untagAssets( untagAssets(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@Body() dto: AssetIdsDto, @Body() dto: AssetIdsDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
): Promise<AssetIdsResponseDto[]> { ): Promise<AssetIdsResponseDto[]> {
return this.service.removeAssets(authUser, id, dto); return this.service.removeAssets(auth, id, dto);
} }
} }

View File

@ -1,5 +1,5 @@
import { import {
AuthUserDto, AuthDto,
CreateUserDto as CreateDto, CreateUserDto as CreateDto,
CreateProfileImageDto, CreateProfileImageDto,
CreateProfileImageResponseDto, CreateProfileImageResponseDto,
@ -23,7 +23,7 @@ import {
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; 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 { UseValidation, asStreamableFile } from '../app.utils';
import { FileUploadInterceptor, Route } from '../interceptors'; import { FileUploadInterceptor, Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto'; import { UUIDParamDto } from './dto/uuid-param.dto';
@ -36,8 +36,8 @@ export class UserController {
constructor(private service: UserService) {} constructor(private service: UserService) {}
@Get() @Get()
getAllUsers(@AuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> { getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
return this.service.getAll(authUser, isAll); return this.service.getAll(auth, isAll);
} }
@Get('info/:id') @Get('info/:id')
@ -46,8 +46,8 @@ export class UserController {
} }
@Get('me') @Get('me')
getMyUserInfo(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> { getMyUserInfo(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.getMe(authUser); return this.service.getMe(auth);
} }
@AdminRoute() @AdminRoute()
@ -58,26 +58,26 @@ export class UserController {
@Delete('profile-image') @Delete('profile-image')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteProfileImage(@AuthUser() authUser: AuthUserDto): Promise<void> { deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteProfileImage(authUser); return this.service.deleteProfileImage(auth);
} }
@AdminRoute() @AdminRoute()
@Delete(':id') @Delete(':id')
deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> { deleteUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.delete(authUser, id); return this.service.delete(auth, id);
} }
@AdminRoute() @AdminRoute()
@Post(':id/restore') @Post(':id/restore')
restoreUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> { restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.restore(authUser, id); return this.service.restore(auth, id);
} }
// TODO: replace with @Put(':id') // TODO: replace with @Put(':id')
@Put() @Put()
updateUser(@AuthUser() authUser: AuthUserDto, @Body() updateUserDto: UpdateDto): Promise<UserResponseDto> { updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateDto): Promise<UserResponseDto> {
return this.service.update(authUser, updateUserDto); return this.service.update(auth, updateUserDto);
} }
@UseInterceptors(FileUploadInterceptor) @UseInterceptors(FileUploadInterceptor)
@ -85,10 +85,10 @@ export class UserController {
@ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto })
@Post('profile-image') @Post('profile-image')
createProfileImage( createProfileImage(
@AuthUser() authUser: AuthUserDto, @Auth() auth: AuthDto,
@UploadedFile() fileInfo: Express.Multer.File, @UploadedFile() fileInfo: Express.Multer.File,
): Promise<CreateProfileImageResponseDto> { ): Promise<CreateProfileImageResponseDto> {
return this.service.createProfileImage(authUser, fileInfo); return this.service.createProfileImage(auth, fileInfo);
} }
@Get('profile-image/:id') @Get('profile-image/:id')

View File

@ -44,7 +44,7 @@ const callbackify = async <T>(fn: (...args: any[]) => T, callback: Callback<T>)
const asRequest = (req: AuthRequest, file: Express.Multer.File) => { const asRequest = (req: AuthRequest, file: Express.Multer.File) => {
return { return {
authUser: req.user || null, auth: req.user || null,
fieldName: file.fieldname as UploadFieldName, fieldName: file.fieldname as UploadFieldName,
file: mapToUploadFile(file as ImmichFile), file: mapToUploadFile(file as ImmichFile),
}; };

View File

@ -19,10 +19,10 @@ export class CommunicationRepository implements OnGatewayConnection, OnGatewayDi
async handleConnection(client: Socket) { async handleConnection(client: Socket) {
try { try {
this.logger.log(`Websocket Connect: ${client.id}`); this.logger.log(`Websocket Connect: ${client.id}`);
const user = await this.authService.validate(client.request.headers, {}); const auth = await this.authService.validate(client.request.headers, {});
await client.join(user.id); await client.join(auth.user.id);
for (const callback of this.onConnectCallbacks) { for (const callback of this.onConnectCallbacks) {
await callback(user.id); await callback(auth.user.id);
} }
} catch (error: Error | any) { } catch (error: Error | any) {
this.logger.error(`Websocket connection error: ${error}`, error?.stack); this.logger.error(`Websocket connection error: ${error}`, error?.stack);

View File

@ -9,7 +9,7 @@ export const activityStub = {
id: 'activity-1', id: 'activity-1',
comment: 'comment', comment: 'comment',
isLiked: false, isLiked: false,
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,
@ -22,7 +22,7 @@ export const activityStub = {
id: 'activity-2', id: 'activity-2',
comment: null, comment: null,
isLiked: true, isLiked: true,
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
assetId: assetStub.image.id, assetId: assetStub.image.id,
asset: assetStub.image, asset: assetStub.image,

View File

@ -8,7 +8,7 @@ export const albumStub = {
id: 'album-1', id: 'album-1',
albumName: 'Empty album', albumName: 'Empty album',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
albumThumbnailAsset: null, albumThumbnailAsset: null,
@ -24,7 +24,7 @@ export const albumStub = {
id: 'album-2', id: 'album-2',
albumName: 'Empty album shared with user', albumName: 'Empty album shared with user',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
albumThumbnailAsset: null, albumThumbnailAsset: null,
@ -40,7 +40,7 @@ export const albumStub = {
id: 'album-3', id: 'album-3',
albumName: 'Empty album shared with users', albumName: 'Empty album shared with users',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
albumThumbnailAsset: null, albumThumbnailAsset: null,
@ -56,7 +56,7 @@ export const albumStub = {
id: 'album-3', id: 'album-3',
albumName: 'Empty album shared with admin', albumName: 'Empty album shared with admin',
description: '', description: '',
ownerId: authStub.user1.id, ownerId: authStub.user1.user.id,
owner: userStub.user1, owner: userStub.user1,
assets: [], assets: [],
albumThumbnailAsset: null, albumThumbnailAsset: null,
@ -72,7 +72,7 @@ export const albumStub = {
id: 'album-4', id: 'album-4',
albumName: 'Album with one asset', albumName: 'Album with one asset',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image], assets: [assetStub.image],
albumThumbnailAsset: null, albumThumbnailAsset: null,
@ -88,7 +88,7 @@ export const albumStub = {
id: 'album-4a', id: 'album-4a',
albumName: 'Album with two assets', albumName: 'Album with two assets',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image, assetStub.withLocation], assets: [assetStub.image, assetStub.withLocation],
albumThumbnailAsset: assetStub.image, albumThumbnailAsset: assetStub.image,
@ -104,7 +104,7 @@ export const albumStub = {
id: 'album-5', id: 'album-5',
albumName: 'Empty album with invalid thumbnail', albumName: 'Empty album with invalid thumbnail',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
albumThumbnailAsset: assetStub.image, albumThumbnailAsset: assetStub.image,
@ -120,7 +120,7 @@ export const albumStub = {
id: 'album-5', id: 'album-5',
albumName: 'Empty album with invalid thumbnail', albumName: 'Empty album with invalid thumbnail',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
albumThumbnailAsset: null, albumThumbnailAsset: null,
@ -136,7 +136,7 @@ export const albumStub = {
id: 'album-6', id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail', albumName: 'Album with one asset and invalid thumbnail',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image], assets: [assetStub.image],
albumThumbnailAsset: assetStub.livePhotoMotionAsset, albumThumbnailAsset: assetStub.livePhotoMotionAsset,
@ -152,7 +152,7 @@ export const albumStub = {
id: 'album-6', id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail', albumName: 'Album with one asset and invalid thumbnail',
description: '', description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image], assets: [assetStub.image],
albumThumbnailAsset: assetStub.image, albumThumbnailAsset: assetStub.image,

View File

@ -7,7 +7,7 @@ export const keyStub = {
id: 'my-random-guid', id: 'my-random-guid',
name: 'My Key', name: 'My Key',
key: 'my-api-key (hashed)', key: 'my-api-key (hashed)',
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
} as APIKeyEntity), } as APIKeyEntity),
}; };

View File

@ -403,7 +403,7 @@ export const assetStub = {
livePhotoMotionAsset: Object.freeze({ livePhotoMotionAsset: Object.freeze({
id: 'live-photo-motion-asset', id: 'live-photo-motion-asset',
originalPath: fileStub.livePhotoMotion.originalPath, originalPath: fileStub.livePhotoMotion.originalPath,
ownerId: authStub.user1.id, ownerId: authStub.user1.user.id,
type: AssetType.VIDEO, type: AssetType.VIDEO,
isVisible: false, isVisible: false,
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
@ -418,7 +418,7 @@ export const assetStub = {
livePhotoStillAsset: Object.freeze({ livePhotoStillAsset: Object.freeze({
id: 'live-photo-still-asset', id: 'live-photo-still-asset',
originalPath: fileStub.livePhotoStill.originalPath, originalPath: fileStub.livePhotoStill.originalPath,
ownerId: authStub.user1.id, ownerId: authStub.user1.user.id,
type: AssetType.IMAGE, type: AssetType.IMAGE,
livePhotoVideoId: 'live-photo-motion-asset', livePhotoVideoId: 'live-photo-motion-asset',
isVisible: true, isVisible: true,

View File

@ -7,7 +7,7 @@ export const auditStub = {
entityId: 'asset-created', entityId: 'asset-created',
action: DatabaseAction.CREATE, action: DatabaseAction.CREATE,
entityType: EntityType.ASSET, entityType: EntityType.ASSET,
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
createdAt: new Date(), createdAt: new Date(),
}), }),
update: Object.freeze<AuditEntity>({ update: Object.freeze<AuditEntity>({
@ -15,7 +15,7 @@ export const auditStub = {
entityId: 'asset-updated', entityId: 'asset-updated',
action: DatabaseAction.UPDATE, action: DatabaseAction.UPDATE,
entityType: EntityType.ASSET, entityType: EntityType.ASSET,
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
createdAt: new Date(), createdAt: new Date(),
}), }),
delete: Object.freeze<AuditEntity>({ delete: Object.freeze<AuditEntity>({
@ -23,7 +23,7 @@ export const auditStub = {
entityId: 'asset-deleted', entityId: 'asset-deleted',
action: DatabaseAction.DELETE, action: DatabaseAction.DELETE,
entityType: EntityType.ASSET, entityType: EntityType.ASSET,
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
createdAt: new Date(), createdAt: new Date(),
}), }),
}; };

View File

@ -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 = { export const adminSignupStub = {
name: 'Immich Admin', name: 'Immich Admin',
@ -24,77 +25,84 @@ export const changePasswordStub = {
}; };
export const authStub = { export const authStub = {
admin: Object.freeze<AuthUserDto>({ admin: Object.freeze<AuthDto>({
id: 'admin_id', user: {
email: 'admin@test.com', id: 'admin_id',
isAdmin: true, email: 'admin@test.com',
isPublicUser: false, isAdmin: true,
isAllowUpload: true, } as UserEntity,
externalPath: null,
}), }),
user1: Object.freeze<AuthUserDto>({ user1: Object.freeze<AuthDto>({
id: 'user-id', user: {
email: 'immich@test.com', id: 'user-id',
isAdmin: false, email: 'immich@test.com',
isPublicUser: false, isAdmin: false,
isAllowUpload: true, } as UserEntity,
isAllowDownload: true, userToken: {
isShowMetadata: true, id: 'token-id',
accessTokenId: 'token-id', } as UserTokenEntity,
externalPath: null,
}), }),
user2: Object.freeze<AuthUserDto>({ user2: Object.freeze<AuthDto>({
id: 'user-2', user: {
email: 'user2@immich.app', id: 'user-2',
isAdmin: false, email: 'user2@immich.app',
isPublicUser: false, isAdmin: false,
isAllowUpload: true, } as UserEntity,
isAllowDownload: true, userToken: {
isShowMetadata: true, id: 'token-id',
accessTokenId: 'token-id', } as UserTokenEntity,
externalPath: null,
}), }),
external1: Object.freeze<AuthUserDto>({ external1: Object.freeze<AuthDto>({
id: 'user-id', user: {
email: 'immich@test.com', id: 'user-id',
isAdmin: false, email: 'immich@test.com',
isPublicUser: false, isAdmin: false,
isAllowUpload: true, externalPath: '/data/user1',
isAllowDownload: true, } as UserEntity,
isShowMetadata: true, userToken: {
accessTokenId: 'token-id', id: 'token-id',
externalPath: '/data/user1', } as UserTokenEntity,
}), }),
adminSharedLink: Object.freeze<AuthUserDto>({ adminSharedLink: Object.freeze<AuthDto>({
id: 'admin_id', user: {
email: 'admin@test.com', id: 'admin_id',
isAdmin: true, email: 'admin@test.com',
isAllowUpload: true, isAdmin: true,
isAllowDownload: true, } as UserEntity,
isPublicUser: true, sharedLink: {
isShowMetadata: true, id: '123',
sharedLinkId: '123', showExif: true,
allowDownload: true,
allowUpload: true,
key: Buffer.from('shared-link-key'),
} as SharedLinkEntity,
}), }),
adminSharedLinkNoExif: Object.freeze<AuthUserDto>({ adminSharedLinkNoExif: Object.freeze<AuthDto>({
id: 'admin_id', user: {
email: 'admin@test.com', id: 'admin_id',
isAdmin: true, email: 'admin@test.com',
isAllowUpload: true, isAdmin: true,
isAllowDownload: true, } as UserEntity,
isPublicUser: true, sharedLink: {
isShowMetadata: false, id: '123',
sharedLinkId: '123', showExif: false,
allowDownload: true,
allowUpload: true,
key: Buffer.from('shared-link-key'),
} as SharedLinkEntity,
}), }),
readonlySharedLink: Object.freeze<AuthUserDto>({ readonlySharedLink: Object.freeze<AuthDto>({
id: 'admin_id', user: {
email: 'admin@test.com', id: 'admin_id',
isAdmin: true, email: 'admin@test.com',
isAllowUpload: false, isAdmin: true,
isAllowDownload: false, } as UserEntity,
isPublicUser: true, sharedLink: {
isShowMetadata: true, id: '123',
sharedLinkId: '123', allowUpload: false,
accessTokenId: 'token-id', allowDownload: false,
showExif: true,
} as SharedLinkEntity,
}), }),
}; };

View File

@ -106,7 +106,7 @@ const albumResponse: AlbumResponseDto = {
export const sharedLinkStub = { export const sharedLinkStub = {
individual: Object.freeze({ individual: Object.freeze({
id: '123', id: '123',
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
key: sharedLinkBytes, key: sharedLinkBytes,
type: SharedLinkType.INDIVIDUAL, type: SharedLinkType.INDIVIDUAL,
@ -121,7 +121,7 @@ export const sharedLinkStub = {
} as SharedLinkEntity), } as SharedLinkEntity),
valid: Object.freeze({ valid: Object.freeze({
id: '123', id: '123',
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
key: sharedLinkBytes, key: sharedLinkBytes,
type: SharedLinkType.ALBUM, type: SharedLinkType.ALBUM,
@ -138,7 +138,7 @@ export const sharedLinkStub = {
} as SharedLinkEntity), } as SharedLinkEntity),
expired: Object.freeze({ expired: Object.freeze({
id: '123', id: '123',
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
key: sharedLinkBytes, key: sharedLinkBytes,
type: SharedLinkType.ALBUM, type: SharedLinkType.ALBUM,
@ -154,7 +154,7 @@ export const sharedLinkStub = {
} as SharedLinkEntity), } as SharedLinkEntity),
readonlyNoExif: Object.freeze<SharedLinkEntity>({ readonlyNoExif: Object.freeze<SharedLinkEntity>({
id: '123', id: '123',
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
key: sharedLinkBytes, key: sharedLinkBytes,
type: SharedLinkType.ALBUM, type: SharedLinkType.ALBUM,
@ -169,7 +169,7 @@ export const sharedLinkStub = {
albumId: 'album-123', albumId: 'album-123',
album: { album: {
id: 'album-123', id: 'album-123',
ownerId: authStub.admin.id, ownerId: authStub.admin.user.id,
owner: userStub.admin, owner: userStub.admin,
albumName: 'Test Album', albumName: 'Test Album',
description: '', description: '',
@ -260,7 +260,7 @@ export const sharedLinkStub = {
}), }),
passwordRequired: Object.freeze<SharedLinkEntity>({ passwordRequired: Object.freeze<SharedLinkEntity>({
id: '123', id: '123',
userId: authStub.admin.id, userId: authStub.admin.user.id,
user: userStub.admin, user: userStub.admin,
key: sharedLinkBytes, key: sharedLinkBytes,
type: SharedLinkType.ALBUM, type: SharedLinkType.ALBUM,

View File

@ -21,7 +21,7 @@ export const userDto = {
export const userStub = { export const userStub = {
admin: Object.freeze<UserEntity>({ admin: Object.freeze<UserEntity>({
...authStub.admin, ...authStub.admin.user,
password: 'admin_password', password: 'admin_password',
name: 'admin_name', name: 'admin_name',
storageLabel: 'admin', storageLabel: 'admin',
@ -38,7 +38,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY, avatarColor: UserAvatarColor.PRIMARY,
}), }),
user1: Object.freeze<UserEntity>({ user1: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1.user,
password: 'immich_password', password: 'immich_password',
name: 'immich_name', name: 'immich_name',
storageLabel: null, storageLabel: null,
@ -55,7 +55,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY, avatarColor: UserAvatarColor.PRIMARY,
}), }),
user2: Object.freeze<UserEntity>({ user2: Object.freeze<UserEntity>({
...authStub.user2, ...authStub.user2.user,
password: 'immich_password', password: 'immich_password',
name: 'immich_name', name: 'immich_name',
storageLabel: null, storageLabel: null,
@ -72,7 +72,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY, avatarColor: UserAvatarColor.PRIMARY,
}), }),
storageLabel: Object.freeze<UserEntity>({ storageLabel: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1.user,
password: 'immich_password', password: 'immich_password',
name: 'immich_name', name: 'immich_name',
storageLabel: 'label-1', storageLabel: 'label-1',
@ -89,7 +89,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY, avatarColor: UserAvatarColor.PRIMARY,
}), }),
externalPath1: Object.freeze<UserEntity>({ externalPath1: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1.user,
password: 'immich_password', password: 'immich_password',
name: 'immich_name', name: 'immich_name',
storageLabel: 'label-1', storageLabel: 'label-1',
@ -106,7 +106,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY, avatarColor: UserAvatarColor.PRIMARY,
}), }),
externalPath2: Object.freeze<UserEntity>({ externalPath2: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1.user,
password: 'immich_password', password: 'immich_password',
name: 'immich_name', name: 'immich_name',
storageLabel: 'label-1', storageLabel: 'label-1',
@ -123,7 +123,7 @@ export const userStub = {
avatarColor: UserAvatarColor.PRIMARY, avatarColor: UserAvatarColor.PRIMARY,
}), }),
profilePath: Object.freeze<UserEntity>({ profilePath: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1.user,
password: 'immich_password', password: 'immich_password',
name: 'immich_name', name: 'immich_name',
storageLabel: 'label-1', storageLabel: 'label-1',