mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:39:03 -04:00 
			
		
		
		
	refactor(server): user core (#4733)
This commit is contained in:
		
							parent
							
								
									2377df9dae
								
							
						
					
					
						commit
						088d5addf2
					
				| @ -174,7 +174,7 @@ describe(AlbumService.name, () => { | |||||||
|         albumThumbnailAssetId: '123', |         albumThumbnailAssetId: '123', | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.get).toHaveBeenCalledWith('user-id'); |       expect(userMock.get).toHaveBeenCalledWith('user-id', {}); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should require valid userIds', async () => { |     it('should require valid userIds', async () => { | ||||||
| @ -185,7 +185,7 @@ describe(AlbumService.name, () => { | |||||||
|           sharedWithUserIds: ['user-3'], |           sharedWithUserIds: ['user-3'], | ||||||
|         }), |         }), | ||||||
|       ).rejects.toBeInstanceOf(BadRequestException); |       ).rejects.toBeInstanceOf(BadRequestException); | ||||||
|       expect(userMock.get).toHaveBeenCalledWith('user-3'); |       expect(userMock.get).toHaveBeenCalledWith('user-3', {}); | ||||||
|       expect(albumMock.create).not.toHaveBeenCalled(); |       expect(albumMock.create).not.toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | |||||||
| @ -95,7 +95,7 @@ export class AlbumService { | |||||||
| 
 | 
 | ||||||
|   async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> { |   async create(authUser: AuthUserDto, 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) { | ||||||
|         throw new BadRequestException('User not found'); |         throw new BadRequestException('User not found'); | ||||||
|       } |       } | ||||||
| @ -238,7 +238,7 @@ export class AlbumService { | |||||||
|         throw new BadRequestException('User already added'); |         throw new BadRequestException('User already added'); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const user = await this.userRepository.get(userId); |       const user = await this.userRepository.get(userId, {}); | ||||||
|       if (!user) { |       if (!user) { | ||||||
|         throw new BadRequestException('User not found'); |         throw new BadRequestException('User not found'); | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -343,7 +343,7 @@ export class LibraryService { | |||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const user = await this.userRepository.get(library.ownerId); |     const user = await this.userRepository.get(library.ownerId, {}); | ||||||
|     if (!user?.externalPath) { |     if (!user?.externalPath) { | ||||||
|       this.logger.warn('User has no external path set, cannot refresh library'); |       this.logger.warn('User has no external path set, cannot refresh library'); | ||||||
|       return false; |       return false; | ||||||
|  | |||||||
| @ -13,10 +13,14 @@ export interface UserStatsQueryResponse { | |||||||
|   usage: number; |   usage: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface UserFindOptions { | ||||||
|  |   withDeleted?: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const IUserRepository = 'IUserRepository'; | export const IUserRepository = 'IUserRepository'; | ||||||
| 
 | 
 | ||||||
| export interface IUserRepository { | export interface IUserRepository { | ||||||
|   get(id: string, withDeleted?: boolean): Promise<UserEntity | null>; |   get(id: string, options: UserFindOptions): Promise<UserEntity | null>; | ||||||
|   getAdmin(): Promise<UserEntity | null>; |   getAdmin(): Promise<UserEntity | null>; | ||||||
|   hasAdmin(): Promise<boolean>; |   hasAdmin(): Promise<boolean>; | ||||||
|   getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | null>; |   getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | null>; | ||||||
|  | |||||||
| @ -75,7 +75,7 @@ export class StorageTemplateService { | |||||||
|   async handleMigrationSingle({ id }: IEntityJob) { |   async handleMigrationSingle({ id }: IEntityJob) { | ||||||
|     const [asset] = await this.assetRepository.getByIds([id]); |     const [asset] = await this.assetRepository.getByIds([id]); | ||||||
| 
 | 
 | ||||||
|     const user = await this.userRepository.get(asset.ownerId); |     const user = await this.userRepository.get(asset.ownerId, {}); | ||||||
|     const storageLabel = user?.storageLabel || null; |     const storageLabel = user?.storageLabel || null; | ||||||
|     const filename = asset.originalFileName || asset.id; |     const filename = asset.originalFileName || asset.id; | ||||||
|     await this.moveAsset(asset, { storageLabel, filename }); |     await this.moveAsset(asset, { storageLabel, filename }); | ||||||
|  | |||||||
| @ -122,33 +122,4 @@ export class UserCore { | |||||||
|       throw new InternalServerErrorException('Failed to register new user'); |       throw new InternalServerErrorException('Failed to register new user'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   async restoreUser(authUser: AuthUserDto, userToRestore: UserEntity): Promise<UserEntity> { |  | ||||||
|     if (!authUser.isAdmin) { |  | ||||||
|       throw new ForbiddenException('Unauthorized'); |  | ||||||
|     } |  | ||||||
|     try { |  | ||||||
|       return this.userRepository.restore(userToRestore); |  | ||||||
|     } catch (e) { |  | ||||||
|       Logger.error(e, 'Failed to restore deleted user'); |  | ||||||
|       throw new InternalServerErrorException('Failed to restore deleted user'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async deleteUser(authUser: AuthUserDto, userToDelete: UserEntity): Promise<UserEntity> { |  | ||||||
|     if (!authUser.isAdmin) { |  | ||||||
|       throw new ForbiddenException('Unauthorized'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (userToDelete.isAdmin) { |  | ||||||
|       throw new ForbiddenException('Cannot delete admin user'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
|       return this.userRepository.delete(userToDelete); |  | ||||||
|     } catch (e) { |  | ||||||
|       Logger.error(e, 'Failed to delete user'); |  | ||||||
|       throw new InternalServerErrorException('Failed to delete user'); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import { | |||||||
|   NotFoundException, |   NotFoundException, | ||||||
| } from '@nestjs/common'; | } from '@nestjs/common'; | ||||||
| import { | import { | ||||||
|  |   authStub, | ||||||
|   newAlbumRepositoryMock, |   newAlbumRepositoryMock, | ||||||
|   newAssetRepositoryMock, |   newAssetRepositoryMock, | ||||||
|   newCryptoRepositoryMock, |   newCryptoRepositoryMock, | ||||||
| @ -17,7 +18,6 @@ import { | |||||||
| } from '@test'; | } from '@test'; | ||||||
| import { when } from 'jest-when'; | import { when } from 'jest-when'; | ||||||
| import { Readable } from 'stream'; | import { Readable } from 'stream'; | ||||||
| import { AuthUserDto } from '../auth'; |  | ||||||
| import { JobName } from '../job'; | import { JobName } from '../job'; | ||||||
| import { | import { | ||||||
|   IAlbumRepository, |   IAlbumRepository, | ||||||
| @ -29,7 +29,7 @@ import { | |||||||
|   IUserRepository, |   IUserRepository, | ||||||
| } from '../repositories'; | } from '../repositories'; | ||||||
| import { UpdateUserDto } from './dto/update-user.dto'; | import { UpdateUserDto } from './dto/update-user.dto'; | ||||||
| import { UserResponseDto, mapUser } from './response-dto'; | import { mapUser } from './response-dto'; | ||||||
| import { UserService } from './user.service'; | import { UserService } from './user.service'; | ||||||
| 
 | 
 | ||||||
| const makeDeletedAt = (daysAgo: number) => { | const makeDeletedAt = (daysAgo: number) => { | ||||||
| @ -38,95 +38,6 @@ const makeDeletedAt = (daysAgo: number) => { | |||||||
|   return deletedAt; |   return deletedAt; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const adminUserAuth: AuthUserDto = Object.freeze({ |  | ||||||
|   id: 'admin_id', |  | ||||||
|   email: 'admin@test.com', |  | ||||||
|   isAdmin: true, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const immichUserAuth: AuthUserDto = Object.freeze({ |  | ||||||
|   id: 'user-id', |  | ||||||
|   email: 'immich@test.com', |  | ||||||
|   isAdmin: false, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const adminUser: UserEntity = Object.freeze({ |  | ||||||
|   id: adminUserAuth.id, |  | ||||||
|   email: 'admin@test.com', |  | ||||||
|   password: 'admin_password', |  | ||||||
|   firstName: 'admin_first_name', |  | ||||||
|   lastName: 'admin_last_name', |  | ||||||
|   isAdmin: true, |  | ||||||
|   oauthId: '', |  | ||||||
|   shouldChangePassword: false, |  | ||||||
|   profileImagePath: '', |  | ||||||
|   createdAt: new Date('2021-01-01'), |  | ||||||
|   deletedAt: null, |  | ||||||
|   updatedAt: new Date('2021-01-01'), |  | ||||||
|   tags: [], |  | ||||||
|   assets: [], |  | ||||||
|   storageLabel: 'admin', |  | ||||||
|   externalPath: null, |  | ||||||
|   memoriesEnabled: true, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const immichUser: UserEntity = Object.freeze({ |  | ||||||
|   id: immichUserAuth.id, |  | ||||||
|   email: 'immich@test.com', |  | ||||||
|   password: 'immich_password', |  | ||||||
|   firstName: 'immich_first_name', |  | ||||||
|   lastName: 'immich_last_name', |  | ||||||
|   isAdmin: false, |  | ||||||
|   oauthId: '', |  | ||||||
|   shouldChangePassword: false, |  | ||||||
|   profileImagePath: '', |  | ||||||
|   createdAt: new Date('2021-01-01'), |  | ||||||
|   deletedAt: null, |  | ||||||
|   updatedAt: new Date('2021-01-01'), |  | ||||||
|   tags: [], |  | ||||||
|   assets: [], |  | ||||||
|   storageLabel: null, |  | ||||||
|   externalPath: null, |  | ||||||
|   memoriesEnabled: true, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const updatedImmichUser = Object.freeze<UserEntity>({ |  | ||||||
|   id: immichUserAuth.id, |  | ||||||
|   email: 'immich@test.com', |  | ||||||
|   password: 'immich_password', |  | ||||||
|   firstName: 'updated_immich_first_name', |  | ||||||
|   lastName: 'updated_immich_last_name', |  | ||||||
|   isAdmin: false, |  | ||||||
|   oauthId: '', |  | ||||||
|   shouldChangePassword: true, |  | ||||||
|   profileImagePath: '', |  | ||||||
|   createdAt: new Date('2021-01-01'), |  | ||||||
|   deletedAt: null, |  | ||||||
|   updatedAt: new Date('2021-01-01'), |  | ||||||
|   tags: [], |  | ||||||
|   assets: [], |  | ||||||
|   storageLabel: null, |  | ||||||
|   externalPath: null, |  | ||||||
|   memoriesEnabled: true, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const adminUserResponse = Object.freeze<UserResponseDto>({ |  | ||||||
|   id: adminUserAuth.id, |  | ||||||
|   email: 'admin@test.com', |  | ||||||
|   firstName: 'admin_first_name', |  | ||||||
|   lastName: 'admin_last_name', |  | ||||||
|   isAdmin: true, |  | ||||||
|   oauthId: '', |  | ||||||
|   shouldChangePassword: false, |  | ||||||
|   profileImagePath: '', |  | ||||||
|   createdAt: new Date('2021-01-01'), |  | ||||||
|   deletedAt: null, |  | ||||||
|   updatedAt: new Date('2021-01-01'), |  | ||||||
|   storageLabel: 'admin', |  | ||||||
|   externalPath: null, |  | ||||||
|   memoriesEnabled: true, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| describe(UserService.name, () => { | describe(UserService.name, () => { | ||||||
|   let sut: UserService; |   let sut: UserService; | ||||||
|   let userMock: jest.Mocked<IUserRepository>; |   let userMock: jest.Mocked<IUserRepository>; | ||||||
| @ -149,119 +60,92 @@ 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(adminUser.id).mockResolvedValue(adminUser); |     when(userMock.get).calledWith(authStub.admin.id, {}).mockResolvedValue(userStub.admin); | ||||||
|     when(userMock.get).calledWith(immichUser.id).mockResolvedValue(immichUser); |     when(userMock.get).calledWith(authStub.admin.id, { withDeleted: true }).mockResolvedValue(userStub.admin); | ||||||
|  |     when(userMock.get).calledWith(authStub.user1.id, {}).mockResolvedValue(userStub.user1); | ||||||
|  |     when(userMock.get).calledWith(authStub.user1.id, { withDeleted: true }).mockResolvedValue(userStub.user1); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('getAll', () => { |   describe('getAll', () => { | ||||||
|     it('should get all users', async () => { |     it('should get all users', async () => { | ||||||
|       userMock.getList.mockResolvedValue([adminUser]); |       userMock.getList.mockResolvedValue([userStub.admin]); | ||||||
| 
 |       await expect(sut.getAll(authStub.admin, false)).resolves.toEqual([ | ||||||
|       const response = await sut.getAll(adminUserAuth, false); |         expect.objectContaining({ | ||||||
| 
 |           id: authStub.admin.id, | ||||||
|       expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true }); |           email: authStub.admin.email, | ||||||
|       expect(response).toEqual([ |         }), | ||||||
|         { |  | ||||||
|           id: adminUserAuth.id, |  | ||||||
|           email: 'admin@test.com', |  | ||||||
|           firstName: 'admin_first_name', |  | ||||||
|           lastName: 'admin_last_name', |  | ||||||
|           isAdmin: true, |  | ||||||
|           oauthId: '', |  | ||||||
|           shouldChangePassword: false, |  | ||||||
|           profileImagePath: '', |  | ||||||
|           createdAt: new Date('2021-01-01'), |  | ||||||
|           deletedAt: null, |  | ||||||
|           updatedAt: new Date('2021-01-01'), |  | ||||||
|           storageLabel: 'admin', |  | ||||||
|           externalPath: null, |  | ||||||
|           memoriesEnabled: true, |  | ||||||
|         }, |  | ||||||
|       ]); |       ]); | ||||||
|  |       expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('get', () => { |   describe('get', () => { | ||||||
|     it('should get a user by id', async () => { |     it('should get a user by id', async () => { | ||||||
|       userMock.get.mockResolvedValue(adminUser); |       userMock.get.mockResolvedValue(userStub.admin); | ||||||
| 
 |       await sut.get(authStub.admin.id); | ||||||
|       const response = await sut.get(adminUser.id); |       expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false }); | ||||||
| 
 |  | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(adminUser.id, false); |  | ||||||
|       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 () => { | ||||||
|       userMock.get.mockResolvedValue(null); |       userMock.get.mockResolvedValue(null); | ||||||
| 
 |       await expect(sut.get(authStub.admin.id)).rejects.toBeInstanceOf(NotFoundException); | ||||||
|       await expect(sut.get(adminUser.id)).rejects.toBeInstanceOf(NotFoundException); |       expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, { withDeleted: false }); | ||||||
| 
 |  | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(adminUser.id, false); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('getMe', () => { |   describe('getMe', () => { | ||||||
|     it("should get the auth user's info", async () => { |     it("should get the auth user's info", async () => { | ||||||
|       userMock.get.mockResolvedValue(adminUser); |       userMock.get.mockResolvedValue(userStub.admin); | ||||||
| 
 |       await sut.getMe(authStub.admin); | ||||||
|       const response = await sut.getMe(adminUser); |       expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, {}); | ||||||
| 
 |  | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(adminUser.id); |  | ||||||
|       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 () => { | ||||||
|       userMock.get.mockResolvedValue(null); |       userMock.get.mockResolvedValue(null); | ||||||
| 
 |       await expect(sut.getMe(authStub.admin)).rejects.toBeInstanceOf(BadRequestException); | ||||||
|       await expect(sut.getMe(adminUser)).rejects.toBeInstanceOf(BadRequestException); |       expect(userMock.get).toHaveBeenCalledWith(authStub.admin.id, {}); | ||||||
| 
 |  | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(adminUser.id); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('update', () => { |   describe('update', () => { | ||||||
|     it('should update user', async () => { |     it('should update user', async () => { | ||||||
|       const update: UpdateUserDto = { |       const update: UpdateUserDto = { | ||||||
|         id: immichUser.id, |         id: userStub.user1.id, | ||||||
|         shouldChangePassword: true, |         shouldChangePassword: true, | ||||||
|         email: 'immich@test.com', |         email: 'immich@test.com', | ||||||
|         storageLabel: 'storage_label', |         storageLabel: 'storage_label', | ||||||
|       }; |       }; | ||||||
|       userMock.getByEmail.mockResolvedValue(null); |       userMock.getByEmail.mockResolvedValue(null); | ||||||
|       userMock.getByStorageLabel.mockResolvedValue(null); |       userMock.getByStorageLabel.mockResolvedValue(null); | ||||||
|       userMock.update.mockResolvedValue({ ...updatedImmichUser, isAdmin: true, storageLabel: 'storage_label' }); |       userMock.update.mockResolvedValue(userStub.user1); | ||||||
|  | 
 | ||||||
|  |       await sut.update({ ...authStub.user1, isAdmin: true }, update); | ||||||
| 
 | 
 | ||||||
|       const updatedUser = await sut.update({ ...immichUserAuth, isAdmin: true }, update); |  | ||||||
|       expect(updatedUser.shouldChangePassword).toEqual(true); |  | ||||||
|       expect(userMock.getByEmail).toHaveBeenCalledWith(update.email); |       expect(userMock.getByEmail).toHaveBeenCalledWith(update.email); | ||||||
|       expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel); |       expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     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(updatedImmichUser); |       userMock.update.mockResolvedValue(userStub.user1); | ||||||
| 
 |       await sut.update(userStub.admin, { id: userStub.user1.id, storageLabel: '' }); | ||||||
|       await sut.update(adminUserAuth, { id: immichUser.id, storageLabel: '' }); |       expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.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 () => { | ||||||
|       userMock.update.mockResolvedValue(updatedImmichUser); |       userMock.update.mockResolvedValue(userStub.user1); | ||||||
| 
 |       await sut.update(userStub.user1, { id: userStub.user1.id, storageLabel: 'admin' }); | ||||||
|       await sut.update(immichUserAuth, { id: immichUser.id, storageLabel: 'admin' }); |       expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.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(userMock.get) |       when(userMock.get) | ||||||
|         .calledWith('not_immich_auth_user_id') |         .calledWith('not_immich_auth_user_id', {}) | ||||||
|         .mockResolvedValueOnce({ |         .mockResolvedValueOnce({ | ||||||
|           ...immichUser, |           ...userStub.user1, | ||||||
|           id: 'not_immich_auth_user_id', |           id: 'not_immich_auth_user_id', | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|       const result = sut.update(immichUserAuth, { |       const result = sut.update(userStub.user1, { | ||||||
|         id: 'not_immich_auth_user_id', |         id: 'not_immich_auth_user_id', | ||||||
|         password: 'I take over your account now', |         password: 'I take over your account now', | ||||||
|       }); |       }); | ||||||
| @ -269,107 +153,104 @@ 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: userStub.user1.id, email: 'updated@test.com' }; | ||||||
| 
 | 
 | ||||||
|       userMock.get.mockResolvedValue(immichUser); |       userMock.get.mockResolvedValue(userStub.user1); | ||||||
|       userMock.update.mockResolvedValue(immichUser); |       userMock.update.mockResolvedValue(userStub.user1); | ||||||
| 
 | 
 | ||||||
|       await sut.update(immichUser, dto); |       await sut.update(userStub.user1, dto); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.update).toHaveBeenCalledWith(immichUser.id, { |       expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { | ||||||
|         id: 'user-id', |         id: 'user-id', | ||||||
|         email: 'updated@test.com', |         email: 'updated@test.com', | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     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: userStub.user1.id, email: 'updated@test.com' }; | ||||||
| 
 | 
 | ||||||
|       userMock.get.mockResolvedValue(immichUser); |       userMock.get.mockResolvedValue(userStub.user1); | ||||||
|       userMock.getByEmail.mockResolvedValue(adminUser); |       userMock.getByEmail.mockResolvedValue(userStub.admin); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.update(immichUser, dto)).rejects.toBeInstanceOf(BadRequestException); |       await expect(sut.update(userStub.user1, dto)).rejects.toBeInstanceOf(BadRequestException); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.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: userStub.user1.id, storageLabel: 'admin' }; | ||||||
| 
 | 
 | ||||||
|       userMock.get.mockResolvedValue(immichUser); |       userMock.get.mockResolvedValue(userStub.user1); | ||||||
|       userMock.getByStorageLabel.mockResolvedValue(adminUser); |       userMock.getByStorageLabel.mockResolvedValue(userStub.admin); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.update(adminUser, dto)).rejects.toBeInstanceOf(BadRequestException); |       await expect(sut.update(userStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.update).not.toHaveBeenCalled(); |       expect(userMock.update).not.toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('admin can update any user information', async () => { |     it('admin can update any user information', async () => { | ||||||
|       const update: UpdateUserDto = { |       const update: UpdateUserDto = { | ||||||
|         id: immichUser.id, |         id: userStub.user1.id, | ||||||
|         shouldChangePassword: true, |         shouldChangePassword: true, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       when(userMock.update).calledWith(immichUser.id, update).mockResolvedValueOnce(updatedImmichUser); |       when(userMock.update).calledWith(userStub.user1.id, update).mockResolvedValueOnce(userStub.user1); | ||||||
| 
 |       await sut.update(userStub.admin, update); | ||||||
|       const result = await sut.update(adminUserAuth, update); |       expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { | ||||||
| 
 |         id: 'user-id', | ||||||
|       expect(result).toBeDefined(); |         shouldChangePassword: true, | ||||||
|       expect(result.id).toEqual(updatedImmichUser.id); |       }); | ||||||
|       expect(result.shouldChangePassword).toEqual(updatedImmichUser.shouldChangePassword); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     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(immichUser.id).mockResolvedValueOnce(null); |       when(userMock.get).calledWith(userStub.user1.id, {}).mockResolvedValueOnce(null); | ||||||
| 
 | 
 | ||||||
|       const result = sut.update(adminUser, { |       const result = sut.update(userStub.admin, { | ||||||
|         id: immichUser.id, |         id: userStub.user1.id, | ||||||
|         shouldChangePassword: true, |         shouldChangePassword: true, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       await expect(result).rejects.toBeInstanceOf(NotFoundException); |       await expect(result).rejects.toBeInstanceOf(BadRequestException); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     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: userStub.admin.id, shouldChangePassword: true, isAdmin: true }; | ||||||
| 
 | 
 | ||||||
|       when(userMock.update).calledWith(adminUser.id, dto).mockResolvedValueOnce(adminUser); |       when(userMock.update).calledWith(userStub.admin.id, dto).mockResolvedValueOnce(userStub.admin); | ||||||
| 
 | 
 | ||||||
|       await sut.update(adminUser, dto); |       await sut.update(userStub.admin, dto); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.update).toHaveBeenCalledWith(adminUser.id, dto); |       expect(userMock.update).toHaveBeenCalledWith(userStub.admin.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: userStub.user1.id, shouldChangePassword: true, isAdmin: true }; | ||||||
| 
 | 
 | ||||||
|       when(userMock.get).calledWith(immichUser.id).mockResolvedValueOnce(immichUser); |       when(userMock.get).calledWith(userStub.user1.id, {}).mockResolvedValueOnce(userStub.user1); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.update(adminUser, dto)).rejects.toBeInstanceOf(BadRequestException); |       await expect(sut.update(userStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('restore', () => { |   describe('restore', () => { | ||||||
|     it('should throw error if user could not be found', async () => { |     it('should throw error if user could not be found', async () => { | ||||||
|       userMock.get.mockResolvedValue(null); |       when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(null); | ||||||
| 
 |       await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException); | ||||||
|       await expect(sut.restore(immichUserAuth, adminUser.id)).rejects.toThrowError(BadRequestException); |  | ||||||
|       expect(userMock.restore).not.toHaveBeenCalled(); |       expect(userMock.restore).not.toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should require an admin', async () => { |     it('should require an admin', async () => { | ||||||
|       when(userMock.get).calledWith(adminUser.id, true).mockResolvedValue(adminUser); |       when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(userStub.admin); | ||||||
|       await expect(sut.restore(immichUserAuth, adminUser.id)).rejects.toBeInstanceOf(ForbiddenException); |       await expect(sut.restore(authStub.user1, userStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException); | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(adminUser.id, true); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should restore an user', async () => { |     it('should restore an user', async () => { | ||||||
|       userMock.get.mockResolvedValue(immichUser); |       userMock.get.mockResolvedValue(userStub.user1); | ||||||
|       userMock.restore.mockResolvedValue(immichUser); |       userMock.restore.mockResolvedValue(userStub.user1); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.restore(adminUserAuth, immichUser.id)).resolves.toEqual(mapUser(immichUser)); |       await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1)); | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(immichUser.id, true); |       expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: true }); | ||||||
|       expect(userMock.restore).toHaveBeenCalledWith(immichUser); |       expect(userMock.restore).toHaveBeenCalledWith(userStub.user1); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -377,27 +258,27 @@ describe(UserService.name, () => { | |||||||
|     it('should throw error if user could not be found', async () => { |     it('should throw error if user could not be found', async () => { | ||||||
|       userMock.get.mockResolvedValue(null); |       userMock.get.mockResolvedValue(null); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.delete(immichUserAuth, adminUser.id)).rejects.toThrowError(BadRequestException); |       await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException); | ||||||
|       expect(userMock.delete).not.toHaveBeenCalled(); |       expect(userMock.delete).not.toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('cannot delete admin user', async () => { |     it('cannot delete admin user', async () => { | ||||||
|       await expect(sut.delete(adminUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException); |       await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should require the auth user be an admin', async () => { |     it('should require the auth user be an admin', async () => { | ||||||
|       await expect(sut.delete(immichUserAuth, adminUserAuth.id)).rejects.toBeInstanceOf(ForbiddenException); |       await expect(sut.delete(authStub.user1, authStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.delete).not.toHaveBeenCalled(); |       expect(userMock.delete).not.toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should delete user', async () => { |     it('should delete user', async () => { | ||||||
|       userMock.get.mockResolvedValue(immichUser); |       userMock.get.mockResolvedValue(userStub.user1); | ||||||
|       userMock.delete.mockResolvedValue(immichUser); |       userMock.delete.mockResolvedValue(userStub.user1); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.delete(adminUserAuth, immichUser.id)).resolves.toEqual(mapUser(immichUser)); |       await expect(sut.delete(userStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1)); | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(immichUser.id); |       expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, {}); | ||||||
|       expect(userMock.delete).toHaveBeenCalledWith(immichUser); |       expect(userMock.delete).toHaveBeenCalledWith(userStub.user1); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -443,18 +324,18 @@ 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; | ||||||
|       userMock.update.mockResolvedValue({ ...adminUser, profileImagePath: file.path }); |       userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); | ||||||
| 
 | 
 | ||||||
|       await sut.createProfileImage(adminUserAuth, file); |       await sut.createProfileImage(userStub.admin, file); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.update).toHaveBeenCalledWith(adminUserAuth.id, { profileImagePath: file.path }); |       expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, { profileImagePath: file.path }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     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 () => { | ||||||
|       const file = { path: '/profile/path' } as Express.Multer.File; |       const file = { path: '/profile/path' } as Express.Multer.File; | ||||||
|       userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error')); |       userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error')); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.createProfileImage(adminUserAuth, file)).rejects.toThrowError(InternalServerErrorException); |       await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(InternalServerErrorException); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| @ -462,17 +343,17 @@ describe(UserService.name, () => { | |||||||
|     it('should throw an error if the user does not exist', async () => { |     it('should throw an error if the user does not exist', async () => { | ||||||
|       userMock.get.mockResolvedValue(null); |       userMock.get.mockResolvedValue(null); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.getProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(BadRequestException); |       await expect(sut.getProfileImage(userStub.admin.id)).rejects.toBeInstanceOf(BadRequestException); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id); |       expect(userMock.get).toHaveBeenCalledWith(userStub.admin.id, {}); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     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 () => { | ||||||
|       userMock.get.mockResolvedValue(adminUser); |       userMock.get.mockResolvedValue(userStub.admin); | ||||||
| 
 | 
 | ||||||
|       await expect(sut.getProfileImage(adminUserAuth.id)).rejects.toBeInstanceOf(NotFoundException); |       await expect(sut.getProfileImage(userStub.admin.id)).rejects.toBeInstanceOf(NotFoundException); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(adminUserAuth.id); |       expect(userMock.get).toHaveBeenCalledWith(userStub.admin.id, {}); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return the profile picture', async () => { |     it('should return the profile picture', async () => { | ||||||
| @ -483,7 +364,7 @@ describe(UserService.name, () => { | |||||||
| 
 | 
 | ||||||
|       await expect(sut.getProfileImage(userStub.profilePath.id)).resolves.toEqual({ stream }); |       await expect(sut.getProfileImage(userStub.profilePath.id)).resolves.toEqual({ stream }); | ||||||
| 
 | 
 | ||||||
|       expect(userMock.get).toHaveBeenCalledWith(userStub.profilePath.id); |       expect(userMock.get).toHaveBeenCalledWith(userStub.profilePath.id, {}); | ||||||
|       expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/profile.jpg', 'image/jpeg'); |       expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/profile.jpg', 'image/jpeg'); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| @ -499,7 +380,7 @@ describe(UserService.name, () => { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should default to a random password', async () => { |     it('should default to a random password', async () => { | ||||||
|       userMock.getAdmin.mockResolvedValue(adminUser); |       userMock.getAdmin.mockResolvedValue(userStub.admin); | ||||||
|       const ask = jest.fn().mockResolvedValue(undefined); |       const ask = jest.fn().mockResolvedValue(undefined); | ||||||
| 
 | 
 | ||||||
|       const response = await sut.resetAdminPassword(ask); |       const response = await sut.resetAdminPassword(ask); | ||||||
| @ -508,12 +389,12 @@ describe(UserService.name, () => { | |||||||
| 
 | 
 | ||||||
|       expect(response.provided).toBe(false); |       expect(response.provided).toBe(false); | ||||||
|       expect(ask).toHaveBeenCalled(); |       expect(ask).toHaveBeenCalled(); | ||||||
|       expect(id).toEqual(adminUser.id); |       expect(id).toEqual(userStub.admin.id); | ||||||
|       expect(update.password).toBeDefined(); |       expect(update.password).toBeDefined(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should use the supplied password', async () => { |     it('should use the supplied password', async () => { | ||||||
|       userMock.getAdmin.mockResolvedValue(adminUser); |       userMock.getAdmin.mockResolvedValue(userStub.admin); | ||||||
|       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); | ||||||
| @ -522,7 +403,7 @@ describe(UserService.name, () => { | |||||||
| 
 | 
 | ||||||
|       expect(response.provided).toBe(true); |       expect(response.provided).toBe(true); | ||||||
|       expect(ask).toHaveBeenCalled(); |       expect(ask).toHaveBeenCalled(); | ||||||
|       expect(id).toEqual(adminUser.id); |       expect(id).toEqual(userStub.admin.id); | ||||||
|       expect(update.password).toBeDefined(); |       expect(update.password).toBeDefined(); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { UserEntity } from '@app/infra/entities'; | import { UserEntity } from '@app/infra/entities'; | ||||||
| import { BadRequestException, 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 { AuthUserDto } from '../auth'; | ||||||
| import { IEntityJob, JobName } from '../job'; | import { IEntityJob, JobName } from '../job'; | ||||||
| @ -12,6 +12,7 @@ import { | |||||||
|   IStorageRepository, |   IStorageRepository, | ||||||
|   IUserRepository, |   IUserRepository, | ||||||
|   ImmichReadStream, |   ImmichReadStream, | ||||||
|  |   UserFindOptions, | ||||||
| } from '../repositories'; | } from '../repositories'; | ||||||
| import { StorageCore, StorageFolder } from '../storage'; | import { StorageCore, StorageFolder } from '../storage'; | ||||||
| import { CreateUserDto, UpdateUserDto } from './dto'; | import { CreateUserDto, UpdateUserDto } from './dto'; | ||||||
| @ -41,7 +42,7 @@ export class UserService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async get(userId: string): Promise<UserResponseDto> { |   async get(userId: string): Promise<UserResponseDto> { | ||||||
|     const user = await this.userRepository.get(userId, false); |     const user = await this.userRepository.get(userId, { withDeleted: false }); | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       throw new NotFoundException('User not found'); |       throw new NotFoundException('User not found'); | ||||||
|     } |     } | ||||||
| @ -49,47 +50,43 @@ export class UserService { | |||||||
|     return mapUser(user); |     return mapUser(user); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getMe(authUser: AuthUserDto): Promise<UserResponseDto> { |   getMe(authUser: AuthUserDto): Promise<UserResponseDto> { | ||||||
|     const user = await this.userRepository.get(authUser.id); |     return this.findOrFail(authUser.id, {}).then(mapUser); | ||||||
|     if (!user) { |  | ||||||
|       throw new BadRequestException('User not found'); |  | ||||||
|     } |  | ||||||
|     return mapUser(user); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async create(createUserDto: CreateUserDto): Promise<UserResponseDto> { |   create(createUserDto: CreateUserDto): Promise<UserResponseDto> { | ||||||
|     const createdUser = await this.userCore.createUser(createUserDto); |     return this.userCore.createUser(createUserDto).then(mapUser); | ||||||
|     return mapUser(createdUser); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async update(authUser: AuthUserDto, dto: UpdateUserDto): Promise<UserResponseDto> { |   async update(authUser: AuthUserDto, dto: UpdateUserDto): Promise<UserResponseDto> { | ||||||
|     const user = await this.userRepository.get(dto.id); |     await this.findOrFail(dto.id, {}); | ||||||
|     if (!user) { |     return this.userCore.updateUser(authUser, dto.id, dto).then(mapUser); | ||||||
|       throw new NotFoundException('User not found'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const updatedUser = await this.userCore.updateUser(authUser, dto.id, dto); |  | ||||||
|     return mapUser(updatedUser); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async delete(authUser: AuthUserDto, userId: string): Promise<UserResponseDto> { |   async delete(authUser: AuthUserDto, id: string): Promise<UserResponseDto> { | ||||||
|     const user = await this.userRepository.get(userId); |     if (!authUser.isAdmin) { | ||||||
|     if (!user) { |       throw new ForbiddenException('Unauthorized'); | ||||||
|       throw new BadRequestException('User not found'); |  | ||||||
|     } |     } | ||||||
|     await this.albumRepository.softDeleteAll(userId); | 
 | ||||||
|     const deletedUser = await this.userCore.deleteUser(authUser, user); |     const user = await this.findOrFail(id, {}); | ||||||
|     return mapUser(deletedUser); |     if (user.isAdmin) { | ||||||
|  |       throw new ForbiddenException('Cannot delete admin user'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await this.albumRepository.softDeleteAll(id); | ||||||
|  | 
 | ||||||
|  |     return this.userRepository.delete(user).then(mapUser); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async restore(authUser: AuthUserDto, userId: string): Promise<UserResponseDto> { |   async restore(authUser: AuthUserDto, id: string): Promise<UserResponseDto> { | ||||||
|     const user = await this.userRepository.get(userId, true); |     if (!authUser.isAdmin) { | ||||||
|     if (!user) { |       throw new ForbiddenException('Unauthorized'); | ||||||
|       throw new BadRequestException('User not found'); |  | ||||||
|     } |     } | ||||||
|     const updatedUser = await this.userCore.restoreUser(authUser, user); | 
 | ||||||
|     await this.albumRepository.restoreAll(userId); |     let user = await this.findOrFail(id, { withDeleted: true }); | ||||||
|     return mapUser(updatedUser); |     user = await this.userRepository.restore(user); | ||||||
|  |     await this.albumRepository.restoreAll(id); | ||||||
|  |     return mapUser(user); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async createProfileImage( |   async createProfileImage( | ||||||
| @ -101,7 +98,7 @@ export class UserService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getProfileImage(id: string): Promise<ImmichReadStream> { |   async getProfileImage(id: string): Promise<ImmichReadStream> { | ||||||
|     const user = await this.findOrFail(id); |     const user = await this.findOrFail(id, {}); | ||||||
|     if (!user.profileImagePath) { |     if (!user.profileImagePath) { | ||||||
|       throw new NotFoundException('User does not have a profile image'); |       throw new NotFoundException('User does not have a profile image'); | ||||||
|     } |     } | ||||||
| @ -134,7 +131,7 @@ export class UserService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async handleUserDelete({ id }: IEntityJob) { |   async handleUserDelete({ id }: IEntityJob) { | ||||||
|     const user = await this.userRepository.get(id, true); |     const user = await this.userRepository.get(id, { withDeleted: true }); | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
| @ -181,8 +178,8 @@ export class UserService { | |||||||
|     return msSinceDelete >= msDeleteWait; |     return msSinceDelete >= msDeleteWait; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async findOrFail(id: string) { |   private async findOrFail(id: string, options: UserFindOptions) { | ||||||
|     const user = await this.userRepository.get(id); |     const user = await this.userRepository.get(id, options); | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       throw new BadRequestException('User not found'); |       throw new BadRequestException('User not found'); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { IUserRepository, UserListFilter, UserStatsQueryResponse } from '@app/domain'; | import { IUserRepository, UserFindOptions, UserListFilter, UserStatsQueryResponse } from '@app/domain'; | ||||||
| import { Injectable, InternalServerErrorException } from '@nestjs/common'; | import { Injectable } from '@nestjs/common'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { IsNull, Not, Repository } from 'typeorm'; | import { IsNull, Not, Repository } from 'typeorm'; | ||||||
| import { UserEntity } from '../entities'; | import { UserEntity } from '../entities'; | ||||||
| @ -8,8 +8,12 @@ import { UserEntity } from '../entities'; | |||||||
| export class UserRepository implements IUserRepository { | export class UserRepository implements IUserRepository { | ||||||
|   constructor(@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>) {} |   constructor(@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>) {} | ||||||
| 
 | 
 | ||||||
|   async get(userId: string, withDeleted?: boolean): Promise<UserEntity | null> { |   async get(userId: string, options: UserFindOptions): Promise<UserEntity | null> { | ||||||
|     return this.userRepository.findOne({ where: { id: userId }, withDeleted: withDeleted }); |     options = options || {}; | ||||||
|  |     return this.userRepository.findOne({ | ||||||
|  |       where: { id: userId }, | ||||||
|  |       withDeleted: options.withDeleted, | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getAdmin(): Promise<UserEntity | null> { |   async getAdmin(): Promise<UserEntity | null> { | ||||||
| @ -51,19 +55,13 @@ export class UserRepository implements IUserRepository { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async create(user: Partial<UserEntity>): Promise<UserEntity> { |   create(user: Partial<UserEntity>): Promise<UserEntity> { | ||||||
|     return this.userRepository.save(user); |     return this.save(user); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async update(id: string, user: Partial<UserEntity>): Promise<UserEntity> { |   // TODO change to (user: Partial<UserEntity>)
 | ||||||
|     user.id = id; |   update(id: string, user: Partial<UserEntity>): Promise<UserEntity> { | ||||||
| 
 |     return this.save({ ...user, id }); | ||||||
|     await this.userRepository.save(user); |  | ||||||
|     const updatedUser = await this.get(id); |  | ||||||
|     if (!updatedUser) { |  | ||||||
|       throw new InternalServerErrorException('Cannot reload user after update'); |  | ||||||
|     } |  | ||||||
|     return updatedUser; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> { |   async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> { | ||||||
| @ -101,4 +99,9 @@ export class UserRepository implements IUserRepository { | |||||||
| 
 | 
 | ||||||
|     return stats; |     return stats; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   private async save(user: Partial<UserEntity>) { | ||||||
|  |     const { id } = await this.userRepository.save(user); | ||||||
|  |     return this.userRepository.findOneByOrFail({ id }); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user