mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	fix(server): require local admin account (#1070)
This commit is contained in:
		
							
								
								
									
										12
									
								
								server/apps/immich/src/api-v1/user/dto/user-count.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								server/apps/immich/src/api-v1/user/dto/user-count.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
|  | ||||
| export class UserCountDto { | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   @Transform(({ value }) => value === 'true') | ||||
|   /** | ||||
|    * When true, return the number of admins accounts | ||||
|    */ | ||||
|   admin?: boolean = false; | ||||
| } | ||||
							
								
								
									
										30
									
								
								server/apps/immich/src/api-v1/user/user-repository.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								server/apps/immich/src/api-v1/user/user-repository.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { UserEntity } from '@app/database/entities/user.entity'; | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import { Repository } from 'typeorm'; | ||||
| import { UserRepository } from './user-repository'; | ||||
|  | ||||
| describe('UserRepository', () => { | ||||
|   let sui: UserRepository; | ||||
|   let userRepositoryMock: jest.Mocked<Repository<UserEntity>>; | ||||
|  | ||||
|   beforeAll(() => { | ||||
|     userRepositoryMock = { | ||||
|       findOne: jest.fn(), | ||||
|       save: jest.fn(), | ||||
|     } as unknown as jest.Mocked<Repository<UserEntity>>; | ||||
|  | ||||
|     sui = new UserRepository(userRepositoryMock); | ||||
|   }); | ||||
|  | ||||
|   it('should be defined', () => { | ||||
|     expect(sui).toBeDefined(); | ||||
|   }); | ||||
|  | ||||
|   describe('create', () => { | ||||
|     it('should not create a user if there is no local admin account', async () => { | ||||
|       userRepositoryMock.findOne.mockResolvedValue(null); | ||||
|       await expect(sui.create({ isAdmin: false })).rejects.toBeInstanceOf(BadRequestException); | ||||
|       expect(userRepositoryMock.findOne).toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -60,6 +60,11 @@ export class UserRepository implements IUserRepository { | ||||
|   } | ||||
|  | ||||
|   public async create(user: Partial<UserEntity>): Promise<UserEntity> { | ||||
|     const localAdmin = await this.getAdmin(); | ||||
|     if (!localAdmin && !user.isAdmin) { | ||||
|       throw new BadRequestException('The first registered account must the administrator.'); | ||||
|     } | ||||
|  | ||||
|     if (user.password) { | ||||
|       user.salt = await bcrypt.genSalt(); | ||||
|       user.password = await this.hashPassword(user.password, user.salt); | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import { UserResponseDto } from './response-dto/user-response.dto'; | ||||
| import { UserCountResponseDto } from './response-dto/user-count-response.dto'; | ||||
| import { CreateProfileImageDto } from './dto/create-profile-image.dto'; | ||||
| import { CreateProfileImageResponseDto } from './response-dto/create-profile-image-response.dto'; | ||||
| import { UserCountDto } from './dto/user-count.dto'; | ||||
|  | ||||
| @ApiTags('User') | ||||
| @Controller('user') | ||||
| @@ -64,8 +65,8 @@ export class UserController { | ||||
|   } | ||||
|  | ||||
|   @Get('/count') | ||||
|   async getUserCount(): Promise<UserCountResponseDto> { | ||||
|     return await this.userService.getUserCount(); | ||||
|   async getUserCount(@Query(new ValidationPipe({ transform: true })) dto: UserCountDto): Promise<UserCountResponseDto> { | ||||
|     return await this.userService.getUserCount(dto); | ||||
|   } | ||||
|  | ||||
|   @Authenticated({ admin: true }) | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import { createReadStream } from 'fs'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { CreateUserDto } from './dto/create-user.dto'; | ||||
| import { UpdateUserDto } from './dto/update-user.dto'; | ||||
| import { UserCountDto } from './dto/user-count.dto'; | ||||
| import { | ||||
|   CreateProfileImageResponseDto, | ||||
|   mapCreateProfileImageResponse, | ||||
| @@ -57,8 +58,12 @@ export class UserService { | ||||
|     return mapUser(user); | ||||
|   } | ||||
|  | ||||
|   async getUserCount(): Promise<UserCountResponseDto> { | ||||
|     const users = await this.userRepository.getList(); | ||||
|   async getUserCount(dto: UserCountDto): Promise<UserCountResponseDto> { | ||||
|     let users = await this.userRepository.getList(); | ||||
|  | ||||
|     if (dto.admin) { | ||||
|       users = users.filter((user) => user.isAdmin); | ||||
|     } | ||||
|  | ||||
|     return mapUserCountResponse(users.length); | ||||
|   } | ||||
|   | ||||
| @@ -166,7 +166,17 @@ | ||||
|     "/user/count": { | ||||
|       "get": { | ||||
|         "operationId": "getUserCount", | ||||
|         "parameters": [], | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "admin", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "default": false, | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user