Pārlūkot izejas kodu

refactor(server): use cascades for keys and tokens (#2544)

Jason Rasmussen 2 gadi atpakaļ
vecāks
revīzija
50a792a81a

+ 5 - 0
server/libs/domain/src/api-key/response-dto/api-key-response.dto.ts → server/libs/domain/src/api-key/api-key-response.dto.ts

@@ -1,5 +1,10 @@
 import { APIKeyEntity } from '@app/infra/entities';
 import { APIKeyEntity } from '@app/infra/entities';
 
 
+export class APIKeyCreateResponseDto {
+  secret!: string;
+  apiKey!: APIKeyResponseDto;
+}
+
 export class APIKeyResponseDto {
 export class APIKeyResponseDto {
   id!: string;
   id!: string;
   name!: string;
   name!: string;

+ 6 - 0
server/libs/domain/src/api-key/dto/api-key-create.dto.ts → server/libs/domain/src/api-key/api-key.dto.ts

@@ -6,3 +6,9 @@ export class APIKeyCreateDto {
   @IsOptional()
   @IsOptional()
   name?: string;
   name?: string;
 }
 }
+
+export class APIKeyUpdateDto {
+  @IsString()
+  @IsNotEmpty()
+  name!: string;
+}

+ 0 - 1
server/libs/domain/src/api-key/api-key.repository.ts

@@ -6,7 +6,6 @@ export interface IKeyRepository {
   create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
   create(dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
   update(userId: string, id: string, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
   update(userId: string, id: string, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
   delete(userId: string, id: string): Promise<void>;
   delete(userId: string, id: string): Promise<void>;
-  deleteAll(userId: string): Promise<void>;
   /**
   /**
    * Includes the hashed `key` for verification
    * Includes the hashed `key` for verification
    * @param id
    * @param id

+ 2 - 3
server/libs/domain/src/api-key/api-key.service.ts

@@ -1,10 +1,9 @@
 import { BadRequestException, Inject, Injectable } from '@nestjs/common';
 import { BadRequestException, Inject, Injectable } from '@nestjs/common';
 import { AuthUserDto } from '../auth';
 import { AuthUserDto } from '../auth';
 import { ICryptoRepository } from '../crypto';
 import { ICryptoRepository } from '../crypto';
+import { APIKeyCreateResponseDto, APIKeyResponseDto, mapKey } from './api-key-response.dto';
+import { APIKeyCreateDto } from './api-key.dto';
 import { IKeyRepository } from './api-key.repository';
 import { IKeyRepository } from './api-key.repository';
-import { APIKeyCreateDto } from './dto/api-key-create.dto';
-import { APIKeyCreateResponseDto } from './response-dto/api-key-create-response.dto';
-import { APIKeyResponseDto, mapKey } from './response-dto/api-key-response.dto';
 
 
 @Injectable()
 @Injectable()
 export class APIKeyService {
 export class APIKeyService {

+ 0 - 7
server/libs/domain/src/api-key/dto/api-key-update.dto.ts

@@ -1,7 +0,0 @@
-import { IsNotEmpty, IsString } from 'class-validator';
-
-export class APIKeyUpdateDto {
-  @IsString()
-  @IsNotEmpty()
-  name!: string;
-}

+ 0 - 2
server/libs/domain/src/api-key/dto/index.ts

@@ -1,2 +0,0 @@
-export * from './api-key-create.dto';
-export * from './api-key-update.dto';

+ 2 - 2
server/libs/domain/src/api-key/index.ts

@@ -1,4 +1,4 @@
+export * from './api-key-response.dto';
+export * from './api-key.dto';
 export * from './api-key.repository';
 export * from './api-key.repository';
 export * from './api-key.service';
 export * from './api-key.service';
-export * from './dto';
-export * from './response-dto';

+ 0 - 6
server/libs/domain/src/api-key/response-dto/api-key-create-response.dto.ts

@@ -1,6 +0,0 @@
-import { APIKeyResponseDto } from './api-key-response.dto';
-
-export class APIKeyCreateResponseDto {
-  secret!: string;
-  apiKey!: APIKeyResponseDto;
-}

+ 0 - 2
server/libs/domain/src/api-key/response-dto/index.ts

@@ -1,2 +0,0 @@
-export * from './api-key-create-response.dto';
-export * from './api-key-response.dto';

+ 0 - 1
server/libs/domain/src/user-token/user-token.repository.ts

@@ -6,7 +6,6 @@ export interface IUserTokenRepository {
   create(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
   create(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
   save(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
   save(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
   delete(userId: string, id: string): Promise<void>;
   delete(userId: string, id: string): Promise<void>;
-  deleteAll(userId: string): Promise<void>;
   getByToken(token: string): Promise<UserTokenEntity | null>;
   getByToken(token: string): Promise<UserTokenEntity | null>;
   getAll(userId: string): Promise<UserTokenEntity[]>;
   getAll(userId: string): Promise<UserTokenEntity[]>;
 }
 }

+ 65 - 86
server/libs/domain/src/user/user.service.spec.ts

@@ -6,19 +6,15 @@ import {
   newAssetRepositoryMock,
   newAssetRepositoryMock,
   newCryptoRepositoryMock,
   newCryptoRepositoryMock,
   newJobRepositoryMock,
   newJobRepositoryMock,
-  newKeyRepositoryMock,
   newStorageRepositoryMock,
   newStorageRepositoryMock,
   newUserRepositoryMock,
   newUserRepositoryMock,
-  newUserTokenRepositoryMock,
 } from '../../test';
 } from '../../test';
 import { IAlbumRepository } from '../album';
 import { IAlbumRepository } from '../album';
-import { IKeyRepository } from '../api-key';
 import { IAssetRepository } from '../asset';
 import { IAssetRepository } from '../asset';
 import { AuthUserDto } from '../auth';
 import { AuthUserDto } from '../auth';
 import { ICryptoRepository } from '../crypto';
 import { ICryptoRepository } from '../crypto';
 import { IJobRepository, JobName } from '../job';
 import { IJobRepository, JobName } from '../job';
 import { IStorageRepository } from '../storage';
 import { IStorageRepository } from '../storage';
-import { IUserTokenRepository } from '../user-token';
 import { UpdateUserDto } from './dto/update-user.dto';
 import { UpdateUserDto } from './dto/update-user.dto';
 import { IUserRepository } from './user.repository';
 import { IUserRepository } from './user.repository';
 import { UserService } from './user.service';
 import { UserService } from './user.service';
@@ -109,52 +105,37 @@ const adminUserResponse = Object.freeze({
 
 
 describe(UserService.name, () => {
 describe(UserService.name, () => {
   let sut: UserService;
   let sut: UserService;
-  let userRepositoryMock: jest.Mocked<IUserRepository>;
+  let userMock: jest.Mocked<IUserRepository>;
   let cryptoRepositoryMock: jest.Mocked<ICryptoRepository>;
   let cryptoRepositoryMock: jest.Mocked<ICryptoRepository>;
 
 
   let albumMock: jest.Mocked<IAlbumRepository>;
   let albumMock: jest.Mocked<IAlbumRepository>;
   let assetMock: jest.Mocked<IAssetRepository>;
   let assetMock: jest.Mocked<IAssetRepository>;
   let jobMock: jest.Mocked<IJobRepository>;
   let jobMock: jest.Mocked<IJobRepository>;
-  let keyMock: jest.Mocked<IKeyRepository>;
   let storageMock: jest.Mocked<IStorageRepository>;
   let storageMock: jest.Mocked<IStorageRepository>;
-  let tokenMock: jest.Mocked<IUserTokenRepository>;
 
 
   beforeEach(async () => {
   beforeEach(async () => {
-    userRepositoryMock = newUserRepositoryMock();
     cryptoRepositoryMock = newCryptoRepositoryMock();
     cryptoRepositoryMock = newCryptoRepositoryMock();
-
     albumMock = newAlbumRepositoryMock();
     albumMock = newAlbumRepositoryMock();
     assetMock = newAssetRepositoryMock();
     assetMock = newAssetRepositoryMock();
     jobMock = newJobRepositoryMock();
     jobMock = newJobRepositoryMock();
-    keyMock = newKeyRepositoryMock();
     storageMock = newStorageRepositoryMock();
     storageMock = newStorageRepositoryMock();
-    tokenMock = newUserTokenRepositoryMock();
-    userRepositoryMock = newUserRepositoryMock();
-
-    sut = new UserService(
-      userRepositoryMock,
-      cryptoRepositoryMock,
-      albumMock,
-      assetMock,
-      jobMock,
-      keyMock,
-      storageMock,
-      tokenMock,
-    );
-
-    when(userRepositoryMock.get).calledWith(adminUser.id).mockResolvedValue(adminUser);
-    when(userRepositoryMock.get).calledWith(adminUser.id, undefined).mockResolvedValue(adminUser);
-    when(userRepositoryMock.get).calledWith(immichUser.id).mockResolvedValue(immichUser);
-    when(userRepositoryMock.get).calledWith(immichUser.id, undefined).mockResolvedValue(immichUser);
+    userMock = newUserRepositoryMock();
+
+    sut = new UserService(userMock, cryptoRepositoryMock, albumMock, assetMock, jobMock, storageMock);
+
+    when(userMock.get).calledWith(adminUser.id).mockResolvedValue(adminUser);
+    when(userMock.get).calledWith(adminUser.id, undefined).mockResolvedValue(adminUser);
+    when(userMock.get).calledWith(immichUser.id).mockResolvedValue(immichUser);
+    when(userMock.get).calledWith(immichUser.id, undefined).mockResolvedValue(immichUser);
   });
   });
 
 
   describe('getAllUsers', () => {
   describe('getAllUsers', () => {
     it('should get all users', async () => {
     it('should get all users', async () => {
-      userRepositoryMock.getList.mockResolvedValue([adminUser]);
+      userMock.getList.mockResolvedValue([adminUser]);
 
 
       const response = await sut.getAllUsers(adminUserAuth, false);
       const response = await sut.getAllUsers(adminUserAuth, false);
 
 
-      expect(userRepositoryMock.getList).toHaveBeenCalledWith({ withDeleted: true });
+      expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
       expect(response).toEqual([
       expect(response).toEqual([
         {
         {
           id: adminUserAuth.id,
           id: adminUserAuth.id,
@@ -176,49 +157,49 @@ describe(UserService.name, () => {
 
 
   describe('getUserById', () => {
   describe('getUserById', () => {
     it('should get a user by id', async () => {
     it('should get a user by id', async () => {
-      userRepositoryMock.get.mockResolvedValue(adminUser);
+      userMock.get.mockResolvedValue(adminUser);
 
 
       const response = await sut.getUserById(adminUser.id);
       const response = await sut.getUserById(adminUser.id);
 
 
-      expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, false);
+      expect(userMock.get).toHaveBeenCalledWith(adminUser.id, false);
       expect(response).toEqual(adminUserResponse);
       expect(response).toEqual(adminUserResponse);
     });
     });
 
 
     it('should throw an error if a user is not found', async () => {
     it('should throw an error if a user is not found', async () => {
-      userRepositoryMock.get.mockResolvedValue(null);
+      userMock.get.mockResolvedValue(null);
 
 
       await expect(sut.getUserById(adminUser.id)).rejects.toBeInstanceOf(NotFoundException);
       await expect(sut.getUserById(adminUser.id)).rejects.toBeInstanceOf(NotFoundException);
 
 
-      expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, false);
+      expect(userMock.get).toHaveBeenCalledWith(adminUser.id, false);
     });
     });
   });
   });
 
 
   describe('getUserInfo', () => {
   describe('getUserInfo', () => {
     it("should get the auth user's info", async () => {
     it("should get the auth user's info", async () => {
-      userRepositoryMock.get.mockResolvedValue(adminUser);
+      userMock.get.mockResolvedValue(adminUser);
 
 
       const response = await sut.getUserInfo(adminUser);
       const response = await sut.getUserInfo(adminUser);
 
 
-      expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, undefined);
+      expect(userMock.get).toHaveBeenCalledWith(adminUser.id, undefined);
       expect(response).toEqual(adminUserResponse);
       expect(response).toEqual(adminUserResponse);
     });
     });
 
 
     it('should throw an error if a user is not found', async () => {
     it('should throw an error if a user is not found', async () => {
-      userRepositoryMock.get.mockResolvedValue(null);
+      userMock.get.mockResolvedValue(null);
 
 
       await expect(sut.getUserInfo(adminUser)).rejects.toBeInstanceOf(BadRequestException);
       await expect(sut.getUserInfo(adminUser)).rejects.toBeInstanceOf(BadRequestException);
 
 
-      expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, undefined);
+      expect(userMock.get).toHaveBeenCalledWith(adminUser.id, undefined);
     });
     });
   });
   });
 
 
   describe('getUserCount', () => {
   describe('getUserCount', () => {
     it('should get the user count', async () => {
     it('should get the user count', async () => {
-      userRepositoryMock.getList.mockResolvedValue([adminUser]);
+      userMock.getList.mockResolvedValue([adminUser]);
 
 
       const response = await sut.getUserCount({});
       const response = await sut.getUserCount({});
 
 
-      expect(userRepositoryMock.getList).toHaveBeenCalled();
+      expect(userMock.getList).toHaveBeenCalled();
       expect(response).toEqual({ userCount: 1 });
       expect(response).toEqual({ userCount: 1 });
     });
     });
   });
   });
@@ -230,30 +211,30 @@ describe(UserService.name, () => {
         shouldChangePassword: true,
         shouldChangePassword: true,
       };
       };
 
 
-      when(userRepositoryMock.update).calledWith(update.id, update).mockResolvedValueOnce(updatedImmichUser);
+      when(userMock.update).calledWith(update.id, update).mockResolvedValueOnce(updatedImmichUser);
 
 
       const updatedUser = await sut.updateUser(immichUserAuth, update);
       const updatedUser = await sut.updateUser(immichUserAuth, update);
       expect(updatedUser.shouldChangePassword).toEqual(true);
       expect(updatedUser.shouldChangePassword).toEqual(true);
     });
     });
 
 
     it('should not set an empty string for storage label', async () => {
     it('should not set an empty string for storage label', async () => {
-      userRepositoryMock.update.mockResolvedValue(updatedImmichUser);
+      userMock.update.mockResolvedValue(updatedImmichUser);
 
 
       await sut.updateUser(adminUserAuth, { id: immichUser.id, storageLabel: '' });
       await sut.updateUser(adminUserAuth, { id: immichUser.id, storageLabel: '' });
 
 
-      expect(userRepositoryMock.update).toHaveBeenCalledWith(immichUser.id, { id: immichUser.id, storageLabel: null });
+      expect(userMock.update).toHaveBeenCalledWith(immichUser.id, { id: immichUser.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 () => {
-      userRepositoryMock.update.mockResolvedValue(updatedImmichUser);
+      userMock.update.mockResolvedValue(updatedImmichUser);
 
 
       await sut.updateUser(immichUserAuth, { id: immichUser.id, storageLabel: 'admin' });
       await sut.updateUser(immichUserAuth, { id: immichUser.id, storageLabel: 'admin' });
 
 
-      expect(userRepositoryMock.update).toHaveBeenCalledWith(immichUser.id, { id: immichUser.id });
+      expect(userMock.update).toHaveBeenCalledWith(immichUser.id, { id: immichUser.id });
     });
     });
 
 
     it('user can only update its information', async () => {
     it('user can only update its information', async () => {
-      when(userRepositoryMock.get)
+      when(userMock.get)
         .calledWith('not_immich_auth_user_id', undefined)
         .calledWith('not_immich_auth_user_id', undefined)
         .mockResolvedValueOnce({
         .mockResolvedValueOnce({
           ...immichUser,
           ...immichUser,
@@ -270,12 +251,12 @@ describe(UserService.name, () => {
     it('should let a user change their email', async () => {
     it('should let a user change their email', async () => {
       const dto = { id: immichUser.id, email: 'updated@test.com' };
       const dto = { id: immichUser.id, email: 'updated@test.com' };
 
 
-      userRepositoryMock.get.mockResolvedValue(immichUser);
-      userRepositoryMock.update.mockResolvedValue(immichUser);
+      userMock.get.mockResolvedValue(immichUser);
+      userMock.update.mockResolvedValue(immichUser);
 
 
       await sut.updateUser(immichUser, dto);
       await sut.updateUser(immichUser, dto);
 
 
-      expect(userRepositoryMock.update).toHaveBeenCalledWith(immichUser.id, {
+      expect(userMock.update).toHaveBeenCalledWith(immichUser.id, {
         id: 'user-id',
         id: 'user-id',
         email: 'updated@test.com',
         email: 'updated@test.com',
       });
       });
@@ -284,23 +265,23 @@ describe(UserService.name, () => {
     it('should not let a user change their email to one already in use', async () => {
     it('should not let a user change their email to one already in use', async () => {
       const dto = { id: immichUser.id, email: 'updated@test.com' };
       const dto = { id: immichUser.id, email: 'updated@test.com' };
 
 
-      userRepositoryMock.get.mockResolvedValue(immichUser);
-      userRepositoryMock.getByEmail.mockResolvedValue(adminUser);
+      userMock.get.mockResolvedValue(immichUser);
+      userMock.getByEmail.mockResolvedValue(adminUser);
 
 
       await expect(sut.updateUser(immichUser, dto)).rejects.toBeInstanceOf(BadRequestException);
       await expect(sut.updateUser(immichUser, dto)).rejects.toBeInstanceOf(BadRequestException);
 
 
-      expect(userRepositoryMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
     });
     });
 
 
     it('should not let the admin change the storage label to one already in use', async () => {
     it('should not let the admin change the storage label to one already in use', async () => {
       const dto = { id: immichUser.id, storageLabel: 'admin' };
       const dto = { id: immichUser.id, storageLabel: 'admin' };
 
 
-      userRepositoryMock.get.mockResolvedValue(immichUser);
-      userRepositoryMock.getByStorageLabel.mockResolvedValue(adminUser);
+      userMock.get.mockResolvedValue(immichUser);
+      userMock.getByStorageLabel.mockResolvedValue(adminUser);
 
 
       await expect(sut.updateUser(adminUser, dto)).rejects.toBeInstanceOf(BadRequestException);
       await expect(sut.updateUser(adminUser, dto)).rejects.toBeInstanceOf(BadRequestException);
 
 
-      expect(userRepositoryMock.update).not.toHaveBeenCalled();
+      expect(userMock.update).not.toHaveBeenCalled();
     });
     });
 
 
     it('admin can update any user information', async () => {
     it('admin can update any user information', async () => {
@@ -309,7 +290,7 @@ describe(UserService.name, () => {
         shouldChangePassword: true,
         shouldChangePassword: true,
       };
       };
 
 
-      when(userRepositoryMock.update).calledWith(immichUser.id, update).mockResolvedValueOnce(updatedImmichUser);
+      when(userMock.update).calledWith(immichUser.id, update).mockResolvedValueOnce(updatedImmichUser);
 
 
       const result = await sut.updateUser(adminUserAuth, update);
       const result = await sut.updateUser(adminUserAuth, update);
 
 
@@ -319,7 +300,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(userRepositoryMock.get).calledWith(immichUser.id, undefined).mockResolvedValueOnce(null);
+      when(userMock.get).calledWith(immichUser.id, undefined).mockResolvedValueOnce(null);
 
 
       const result = sut.updateUser(adminUser, {
       const result = sut.updateUser(adminUser, {
         id: immichUser.id,
         id: immichUser.id,
@@ -332,18 +313,18 @@ describe(UserService.name, () => {
     it('should let the admin update himself', async () => {
     it('should let the admin update himself', async () => {
       const dto = { id: adminUser.id, shouldChangePassword: true, isAdmin: true };
       const dto = { id: adminUser.id, shouldChangePassword: true, isAdmin: true };
 
 
-      when(userRepositoryMock.get).calledWith(adminUser.id).mockResolvedValueOnce(null);
-      when(userRepositoryMock.update).calledWith(adminUser.id, dto).mockResolvedValueOnce(adminUser);
+      when(userMock.get).calledWith(adminUser.id).mockResolvedValueOnce(null);
+      when(userMock.update).calledWith(adminUser.id, dto).mockResolvedValueOnce(adminUser);
 
 
       await sut.updateUser(adminUser, dto);
       await sut.updateUser(adminUser, dto);
 
 
-      expect(userRepositoryMock.update).toHaveBeenCalledWith(adminUser.id, dto);
+      expect(userMock.update).toHaveBeenCalledWith(adminUser.id, dto);
     });
     });
 
 
     it('should not let the another user become an admin', async () => {
     it('should not let the another user become an admin', async () => {
       const dto = { id: immichUser.id, shouldChangePassword: true, isAdmin: true };
       const dto = { id: immichUser.id, shouldChangePassword: true, isAdmin: true };
 
 
-      when(userRepositoryMock.get).calledWith(immichUser.id).mockResolvedValueOnce(immichUser);
+      when(userMock.get).calledWith(immichUser.id).mockResolvedValueOnce(immichUser);
 
 
       await expect(sut.updateUser(adminUser, dto)).rejects.toBeInstanceOf(BadRequestException);
       await expect(sut.updateUser(adminUser, dto)).rejects.toBeInstanceOf(BadRequestException);
     });
     });
@@ -351,15 +332,15 @@ describe(UserService.name, () => {
 
 
   describe('restoreUser', () => {
   describe('restoreUser', () => {
     it('should require an admin', async () => {
     it('should require an admin', async () => {
-      when(userRepositoryMock.get).calledWith(adminUser.id, true).mockResolvedValue(adminUser);
+      when(userMock.get).calledWith(adminUser.id, true).mockResolvedValue(adminUser);
       await expect(sut.restoreUser(immichUserAuth, adminUser.id)).rejects.toBeInstanceOf(ForbiddenException);
       await expect(sut.restoreUser(immichUserAuth, adminUser.id)).rejects.toBeInstanceOf(ForbiddenException);
-      expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUser.id, true);
+      expect(userMock.get).toHaveBeenCalledWith(adminUser.id, true);
     });
     });
 
 
     it('should require the auth user be an admin', async () => {
     it('should require the auth user be an admin', async () => {
       await expect(sut.deleteUser(immichUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException);
       await expect(sut.deleteUser(immichUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException);
 
 
-      expect(userRepositoryMock.delete).not.toHaveBeenCalled();
+      expect(userMock.delete).not.toHaveBeenCalled();
     });
     });
   });
   });
 
 
@@ -371,13 +352,13 @@ 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.deleteUser(immichUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException);
       await expect(sut.deleteUser(immichUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException);
 
 
-      expect(userRepositoryMock.delete).not.toHaveBeenCalled();
+      expect(userMock.delete).not.toHaveBeenCalled();
     });
     });
   });
   });
 
 
   describe('update', () => {
   describe('update', () => {
     it('should not create a user if there is no local admin account', async () => {
     it('should not create a user if there is no local admin account', async () => {
-      when(userRepositoryMock.getAdmin).calledWith().mockResolvedValueOnce(null);
+      when(userMock.getAdmin).calledWith().mockResolvedValueOnce(null);
 
 
       await expect(
       await expect(
         sut.createUser({
         sut.createUser({
@@ -393,35 +374,35 @@ describe(UserService.name, () => {
   describe('createProfileImage', () => {
   describe('createProfileImage', () => {
     it('should throw an error if the user does not exist', async () => {
     it('should throw an error if the user does not exist', async () => {
       const file = { path: '/profile/path' } as Express.Multer.File;
       const file = { path: '/profile/path' } as Express.Multer.File;
-      userRepositoryMock.update.mockResolvedValue({ ...adminUser, profileImagePath: file.path });
+      userMock.update.mockResolvedValue({ ...adminUser, profileImagePath: file.path });
 
 
       await sut.createProfileImage(adminUserAuth, file);
       await sut.createProfileImage(adminUserAuth, file);
 
 
-      expect(userRepositoryMock.update).toHaveBeenCalledWith(adminUserAuth.id, { profileImagePath: file.path });
+      expect(userMock.update).toHaveBeenCalledWith(adminUserAuth.id, { profileImagePath: file.path });
     });
     });
   });
   });
 
 
   describe('getUserProfileImage', () => {
   describe('getUserProfileImage', () => {
     it('should throw an error if the user does not exist', async () => {
     it('should throw an error if the user does not exist', async () => {
-      userRepositoryMock.get.mockResolvedValue(null);
+      userMock.get.mockResolvedValue(null);
 
 
       await expect(sut.getUserProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException);
       await expect(sut.getUserProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException);
 
 
-      expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined);
+      expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined);
     });
     });
 
 
     it('should throw an error if the user does not have a picture', async () => {
     it('should throw an error if the user does not have a picture', async () => {
-      userRepositoryMock.get.mockResolvedValue(adminUser);
+      userMock.get.mockResolvedValue(adminUser);
 
 
       await expect(sut.getUserProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException);
       await expect(sut.getUserProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException);
 
 
-      expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined);
+      expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined);
     });
     });
   });
   });
 
 
   describe('resetAdminPassword', () => {
   describe('resetAdminPassword', () => {
     it('should only work when there is an admin account', async () => {
     it('should only work when there is an admin account', async () => {
-      userRepositoryMock.getAdmin.mockResolvedValue(null);
+      userMock.getAdmin.mockResolvedValue(null);
       const ask = jest.fn().mockResolvedValue('new-password');
       const ask = jest.fn().mockResolvedValue('new-password');
 
 
       await expect(sut.resetAdminPassword(ask)).rejects.toBeInstanceOf(BadRequestException);
       await expect(sut.resetAdminPassword(ask)).rejects.toBeInstanceOf(BadRequestException);
@@ -430,12 +411,12 @@ describe(UserService.name, () => {
     });
     });
 
 
     it('should default to a random password', async () => {
     it('should default to a random password', async () => {
-      userRepositoryMock.getAdmin.mockResolvedValue(adminUser);
+      userMock.getAdmin.mockResolvedValue(adminUser);
       const ask = jest.fn().mockResolvedValue(undefined);
       const ask = jest.fn().mockResolvedValue(undefined);
 
 
       const response = await sut.resetAdminPassword(ask);
       const response = await sut.resetAdminPassword(ask);
 
 
-      const [id, update] = userRepositoryMock.update.mock.calls[0];
+      const [id, update] = userMock.update.mock.calls[0];
 
 
       expect(response.provided).toBe(false);
       expect(response.provided).toBe(false);
       expect(ask).toHaveBeenCalled();
       expect(ask).toHaveBeenCalled();
@@ -444,12 +425,12 @@ describe(UserService.name, () => {
     });
     });
 
 
     it('should use the supplied password', async () => {
     it('should use the supplied password', async () => {
-      userRepositoryMock.getAdmin.mockResolvedValue(adminUser);
+      userMock.getAdmin.mockResolvedValue(adminUser);
       const ask = jest.fn().mockResolvedValue('new-password');
       const ask = jest.fn().mockResolvedValue('new-password');
 
 
       const response = await sut.resetAdminPassword(ask);
       const response = await sut.resetAdminPassword(ask);
 
 
-      const [id, update] = userRepositoryMock.update.mock.calls[0];
+      const [id, update] = userMock.update.mock.calls[0];
 
 
       expect(response.provided).toBe(true);
       expect(response.provided).toBe(true);
       expect(ask).toHaveBeenCalled();
       expect(ask).toHaveBeenCalled();
@@ -460,7 +441,7 @@ describe(UserService.name, () => {
 
 
   describe('handleQueueUserDelete', () => {
   describe('handleQueueUserDelete', () => {
     it('should skip users not ready for deletion', async () => {
     it('should skip users not ready for deletion', async () => {
-      userRepositoryMock.getDeletedUsers.mockResolvedValue([
+      userMock.getDeletedUsers.mockResolvedValue([
         {},
         {},
         { deletedAt: undefined },
         { deletedAt: undefined },
         { deletedAt: null },
         { deletedAt: null },
@@ -469,17 +450,17 @@ describe(UserService.name, () => {
 
 
       await sut.handleUserDeleteCheck();
       await sut.handleUserDeleteCheck();
 
 
-      expect(userRepositoryMock.getDeletedUsers).toHaveBeenCalled();
+      expect(userMock.getDeletedUsers).toHaveBeenCalled();
       expect(jobMock.queue).not.toHaveBeenCalled();
       expect(jobMock.queue).not.toHaveBeenCalled();
     });
     });
 
 
     it('should queue user ready for deletion', async () => {
     it('should queue user ready for deletion', async () => {
       const user = { deletedAt: makeDeletedAt(10) };
       const user = { deletedAt: makeDeletedAt(10) };
-      userRepositoryMock.getDeletedUsers.mockResolvedValue([user] as UserEntity[]);
+      userMock.getDeletedUsers.mockResolvedValue([user] as UserEntity[]);
 
 
       await sut.handleUserDeleteCheck();
       await sut.handleUserDeleteCheck();
 
 
-      expect(userRepositoryMock.getDeletedUsers).toHaveBeenCalled();
+      expect(userMock.getDeletedUsers).toHaveBeenCalled();
       expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.USER_DELETION, data: { user } });
       expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.USER_DELETION, data: { user } });
     });
     });
   });
   });
@@ -491,7 +472,7 @@ describe(UserService.name, () => {
       await sut.handleUserDelete({ user });
       await sut.handleUserDelete({ user });
 
 
       expect(storageMock.unlinkDir).not.toHaveBeenCalled();
       expect(storageMock.unlinkDir).not.toHaveBeenCalled();
-      expect(userRepositoryMock.delete).not.toHaveBeenCalled();
+      expect(userMock.delete).not.toHaveBeenCalled();
     });
     });
 
 
     it('should delete the user and associated assets', async () => {
     it('should delete the user and associated assets', async () => {
@@ -506,11 +487,9 @@ describe(UserService.name, () => {
       expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/profile/deleted-user', options);
       expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/profile/deleted-user', options);
       expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/thumbs/deleted-user', options);
       expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/thumbs/deleted-user', options);
       expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/encoded-video/deleted-user', options);
       expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/encoded-video/deleted-user', options);
-      expect(tokenMock.deleteAll).toHaveBeenCalledWith(user.id);
-      expect(keyMock.deleteAll).toHaveBeenCalledWith(user.id);
       expect(albumMock.deleteAll).toHaveBeenCalledWith(user.id);
       expect(albumMock.deleteAll).toHaveBeenCalledWith(user.id);
       expect(assetMock.deleteAll).toHaveBeenCalledWith(user.id);
       expect(assetMock.deleteAll).toHaveBeenCalledWith(user.id);
-      expect(userRepositoryMock.delete).toHaveBeenCalledWith(user, true);
+      expect(userMock.delete).toHaveBeenCalledWith(user, true);
     });
     });
 
 
     it('should delete the library path for a storage label', async () => {
     it('should delete the library path for a storage label', async () => {
@@ -530,7 +509,7 @@ describe(UserService.name, () => {
 
 
       await sut.handleUserDelete({ user });
       await sut.handleUserDelete({ user });
 
 
-      expect(userRepositoryMock.delete).not.toHaveBeenCalled();
+      expect(userMock.delete).not.toHaveBeenCalled();
     });
     });
   });
   });
 });
 });

+ 0 - 6
server/libs/domain/src/user/user.service.ts

@@ -3,14 +3,12 @@ import { BadRequestException, Inject, Injectable, Logger, NotFoundException } fr
 import { randomBytes } from 'crypto';
 import { randomBytes } from 'crypto';
 import { ReadStream } from 'fs';
 import { ReadStream } from 'fs';
 import { IAlbumRepository } from '../album/album.repository';
 import { IAlbumRepository } from '../album/album.repository';
-import { IKeyRepository } from '../api-key/api-key.repository';
 import { IAssetRepository } from '../asset/asset.repository';
 import { IAssetRepository } from '../asset/asset.repository';
 import { AuthUserDto } from '../auth';
 import { AuthUserDto } from '../auth';
 import { ICryptoRepository } from '../crypto/crypto.repository';
 import { ICryptoRepository } from '../crypto/crypto.repository';
 import { IJobRepository, IUserDeletionJob, JobName } from '../job';
 import { IJobRepository, IUserDeletionJob, JobName } from '../job';
 import { StorageCore, StorageFolder } from '../storage';
 import { StorageCore, StorageFolder } from '../storage';
 import { IStorageRepository } from '../storage/storage.repository';
 import { IStorageRepository } from '../storage/storage.repository';
-import { IUserTokenRepository } from '../user-token/user-token.repository';
 import { IUserRepository } from '../user/user.repository';
 import { IUserRepository } from '../user/user.repository';
 import { CreateUserDto, UpdateUserDto, UserCountDto } from './dto';
 import { CreateUserDto, UpdateUserDto, UserCountDto } from './dto';
 import {
 import {
@@ -36,9 +34,7 @@ export class UserService {
     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
     @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
     @Inject(IAssetRepository) private assetRepository: IAssetRepository,
     @Inject(IAssetRepository) private assetRepository: IAssetRepository,
     @Inject(IJobRepository) private jobRepository: IJobRepository,
     @Inject(IJobRepository) private jobRepository: IJobRepository,
-    @Inject(IKeyRepository) private keyRepository: IKeyRepository,
     @Inject(IStorageRepository) private storageRepository: IStorageRepository,
     @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-    @Inject(IUserTokenRepository) private tokenRepository: IUserTokenRepository,
   ) {
   ) {
     this.userCore = new UserCore(userRepository, cryptoRepository);
     this.userCore = new UserCore(userRepository, cryptoRepository);
   }
   }
@@ -174,8 +170,6 @@ export class UserService {
 
 
       this.logger.warn(`Removing user from database: ${user.id}`);
       this.logger.warn(`Removing user from database: ${user.id}`);
 
 
-      await this.tokenRepository.deleteAll(user.id);
-      await this.keyRepository.deleteAll(user.id);
       await this.albumRepository.deleteAll(user.id);
       await this.albumRepository.deleteAll(user.id);
       await this.assetRepository.deleteAll(user.id);
       await this.assetRepository.deleteAll(user.id);
       await this.userRepository.delete(user, true);
       await this.userRepository.delete(user, true);

+ 0 - 1
server/libs/domain/test/api-key.repository.mock.ts

@@ -5,7 +5,6 @@ export const newKeyRepositoryMock = (): jest.Mocked<IKeyRepository> => {
     create: jest.fn(),
     create: jest.fn(),
     update: jest.fn(),
     update: jest.fn(),
     delete: jest.fn(),
     delete: jest.fn(),
-    deleteAll: jest.fn(),
     getKey: jest.fn(),
     getKey: jest.fn(),
     getById: jest.fn(),
     getById: jest.fn(),
     getByUserId: jest.fn(),
     getByUserId: jest.fn(),

+ 0 - 1
server/libs/domain/test/user-token.repository.mock.ts

@@ -5,7 +5,6 @@ export const newUserTokenRepositoryMock = (): jest.Mocked<IUserTokenRepository>
     create: jest.fn(),
     create: jest.fn(),
     save: jest.fn(),
     save: jest.fn(),
     delete: jest.fn(),
     delete: jest.fn(),
-    deleteAll: jest.fn(),
     getByToken: jest.fn(),
     getByToken: jest.fn(),
     getAll: jest.fn(),
     getAll: jest.fn(),
   };
   };

+ 1 - 1
server/libs/infra/src/entities/api-key.entity.ts

@@ -12,7 +12,7 @@ export class APIKeyEntity {
   @Column({ select: false })
   @Column({ select: false })
   key?: string;
   key?: string;
 
 
-  @ManyToOne(() => UserEntity)
+  @ManyToOne(() => UserEntity, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
   user?: UserEntity;
   user?: UserEntity;
 
 
   @Column()
   @Column()

+ 1 - 1
server/libs/infra/src/entities/user-token.entity.ts

@@ -12,7 +12,7 @@ export class UserTokenEntity {
   @Column()
   @Column()
   userId!: string;
   userId!: string;
 
 
-  @ManyToOne(() => UserEntity)
+  @ManyToOne(() => UserEntity, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
   user!: UserEntity;
   user!: UserEntity;
 
 
   @CreateDateColumn({ type: 'timestamptz' })
   @CreateDateColumn({ type: 'timestamptz' })

+ 20 - 0
server/libs/infra/src/migrations/1684867360825-AddUserTokenAndAPIKeyCascades.ts

@@ -0,0 +1,20 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddUserTokenAndAPIKeyCascades1684867360825 implements MigrationInterface {
+    name = 'AddUserTokenAndAPIKeyCascades1684867360825'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "FK_6c2e267ae764a9413b863a29342"`);
+        await queryRunner.query(`ALTER TABLE "user_token" DROP CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918"`);
+        await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
+        await queryRunner.query(`ALTER TABLE "user_token" ADD CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "user_token" DROP CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918"`);
+        await queryRunner.query(`ALTER TABLE "api_keys" DROP CONSTRAINT "FK_6c2e267ae764a9413b863a29342"`);
+        await queryRunner.query(`ALTER TABLE "user_token" ADD CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+    }
+
+}

+ 0 - 4
server/libs/infra/src/repositories/api-key.repository.ts

@@ -21,10 +21,6 @@ export class APIKeyRepository implements IKeyRepository {
     await this.repository.delete({ userId, id });
     await this.repository.delete({ userId, id });
   }
   }
 
 
-  async deleteAll(userId: string): Promise<void> {
-    await this.repository.delete({ userId });
-  }
-
   getKey(hashedToken: string): Promise<APIKeyEntity | null> {
   getKey(hashedToken: string): Promise<APIKeyEntity | null> {
     return this.repository.findOne({
     return this.repository.findOne({
       select: {
       select: {

+ 0 - 4
server/libs/infra/src/repositories/user-token.repository.ts

@@ -38,8 +38,4 @@ export class UserTokenRepository implements IUserTokenRepository {
   async delete(userId: string, id: string): Promise<void> {
   async delete(userId: string, id: string): Promise<void> {
     await this.repository.delete({ userId, id });
     await this.repository.delete({ userId, id });
   }
   }
-
-  async deleteAll(userId: string): Promise<void> {
-    await this.repository.delete({ userId });
-  }
 }
 }