mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	refactor: reset admin password (#1335)
* refactor: reset-admin-password * chore: docs
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								docs/docs/features/img/disable-password-login.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/features/img/disable-password-login.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/docs/features/img/enable-password-login.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/features/img/enable-password-login.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/docs/features/img/reset-admin-password.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/docs/features/img/reset-admin-password.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 16 KiB | 
| @@ -11,29 +11,18 @@ The `immich-server` docker image comes preinstalled with an administrative CLI ( | |||||||
|  |  | ||||||
| ## How to run a command | ## How to run a command | ||||||
|  |  | ||||||
| To run a command, connect to the container and then execute it by running `immich <command>`. | To run a command, [connect](/docs/guides/docker-help.md#attach-to-a-container) to the `immich_server` container and then execute the command via `immich <command>`. | ||||||
|  |  | ||||||
| ## Examples | ## Examples | ||||||
|  |  | ||||||
| ```bash title="Reset Admin Password" | Reset Admin Password | ||||||
| docker exec -it immich_server sh |  | ||||||
|  |  | ||||||
| /usr/src/app$ immich reset-admin-password |  | ||||||
| ? Please choose a new password (optional) immich-is-awesome-unlike-this-password |  | ||||||
| New password: |  | ||||||
| immich-is-awesome-unlike-this-password |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ```bash title="Disable Password Login" | Disable Password Login | ||||||
| docker exec -it immich_server sh |  | ||||||
|  |  | ||||||
| /usr/src/app$ immich disable-password-login |  | ||||||
| Password login has been disabled. |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ```bash title="Enable Password Login" | Enabled Password Login | ||||||
| docker exec -it immich_server sh |  | ||||||
|  |  | ||||||
| /usr/src/app$ immich enable-password-login |  | ||||||
| Password login has been enabled. |  | ||||||
| ``` |  | ||||||
|   | |||||||
| @@ -4,11 +4,27 @@ sidebar_position: 1 | |||||||
|  |  | ||||||
| # Docker Help | # Docker Help | ||||||
|  |  | ||||||
| ## Logs | ## Containers | ||||||
|  |  | ||||||
| ```bash title="Log Examples" | ```bash | ||||||
| docker ps                         # see a list of running containers | docker ps                         # see a list of running containers | ||||||
| docker ps -a                      # see a list of running and stopped containers | docker ps -a                      # see a list of running and stopped containers | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Attach to a Container | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | docker exec -it <id or name> <command>          # attach to a container with a command | ||||||
|  | docker exec -it immich_server sh | ||||||
|  | docker exec -it immich_microservices sh | ||||||
|  | docker exec -it immich_machine_learning sh | ||||||
|  | docker exec -it immich_web sh | ||||||
|  | docker exec -it immich_proxy sh | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Logs | ||||||
|  |  | ||||||
|  | ```bash | ||||||
| docker logs <id or name>          # see the logs for a specific container (by id or name) | docker logs <id or name>          # see the logs for a specific container (by id or name) | ||||||
|  |  | ||||||
| docker logs immich_server | docker logs immich_server | ||||||
|   | |||||||
| @@ -1,37 +1,38 @@ | |||||||
| import { Inject } from '@nestjs/common'; | import { UserResponseDto, UserService } from '@app/domain'; | ||||||
| import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; | import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; | ||||||
| import { randomBytes } from 'node:crypto'; |  | ||||||
| import { IUserRepository, UserCore } from '@app/domain'; |  | ||||||
|  |  | ||||||
| @Command({ | @Command({ | ||||||
|   name: 'reset-admin-password', |   name: 'reset-admin-password', | ||||||
|   description: 'Reset the admin password', |   description: 'Reset the admin password', | ||||||
| }) | }) | ||||||
| export class ResetAdminPasswordCommand extends CommandRunner { | export class ResetAdminPasswordCommand extends CommandRunner { | ||||||
|   userCore: UserCore; |   constructor(private userService: UserService, private readonly inquirer: InquirerService) { | ||||||
|  |  | ||||||
|   constructor(private readonly inquirer: InquirerService, @Inject(IUserRepository) userRepository: IUserRepository) { |  | ||||||
|     super(); |     super(); | ||||||
|  |  | ||||||
|     this.userCore = new UserCore(userRepository); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async run(): Promise<void> { |   async run(): Promise<void> { | ||||||
|     const user = await this.userCore.getAdmin(); |     const ask = (admin: UserResponseDto) => { | ||||||
|     if (!user) { |       const { id, oauthId, email, firstName, lastName } = admin; | ||||||
|       console.log('Unable to reset password: no admin user.'); |       console.log(`Found Admin:  | ||||||
|       return; | - ID=${id} | ||||||
|     } | - OAuth ID=${oauthId} | ||||||
|  | - Email=${email} | ||||||
|  | - Name=${firstName} ${lastName}`); | ||||||
|  |  | ||||||
|     const { password: providedPassword } = await this.inquirer.ask<{ password: string }>('prompt-password', undefined); |       return this.inquirer.ask<{ password: string }>('prompt-password', undefined).then(({ password }) => password); | ||||||
|     const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, ''); |     }; | ||||||
|  |  | ||||||
|     await this.userCore.updateUser(user, user.id, { password }); |     try { | ||||||
|  |       const { password, provided } = await this.userService.resetAdminPassword(ask); | ||||||
|  |  | ||||||
|     if (providedPassword) { |       if (provided) { | ||||||
|       console.log('The admin password has been updated.'); |         console.log(`The admin password has been updated.`); | ||||||
|     } else { |       } else { | ||||||
|       console.log(`The admin password has been updated to:\n${password}`); |         console.log(`The admin password has been updated to:\n${password}`); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error(error); | ||||||
|  |       console.error('Unable to reset admin password'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -340,4 +340,43 @@ describe('UserService', () => { | |||||||
|       expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined); |       expect(userRepositoryMock.get).toHaveBeenCalledWith(adminUserAuth.id, undefined); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   describe('resetAdminPassword', () => { | ||||||
|  |     it('should only work when there is an admin account', async () => { | ||||||
|  |       userRepositoryMock.getAdmin.mockResolvedValue(null); | ||||||
|  |       const ask = jest.fn().mockResolvedValue('new-password'); | ||||||
|  |  | ||||||
|  |       await expect(sut.resetAdminPassword(ask)).rejects.toBeInstanceOf(BadRequestException); | ||||||
|  |  | ||||||
|  |       expect(ask).not.toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should default to a random password', async () => { | ||||||
|  |       userRepositoryMock.getAdmin.mockResolvedValue(adminUser); | ||||||
|  |       const ask = jest.fn().mockResolvedValue(undefined); | ||||||
|  |  | ||||||
|  |       const response = await sut.resetAdminPassword(ask); | ||||||
|  |  | ||||||
|  |       const [id, update] = userRepositoryMock.update.mock.calls[0]; | ||||||
|  |  | ||||||
|  |       expect(response.provided).toBe(false); | ||||||
|  |       expect(ask).toHaveBeenCalled(); | ||||||
|  |       expect(id).toEqual(adminUser.id); | ||||||
|  |       expect(update.password).toBeDefined(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should use the supplied password', async () => { | ||||||
|  |       userRepositoryMock.getAdmin.mockResolvedValue(adminUser); | ||||||
|  |       const ask = jest.fn().mockResolvedValue('new-password'); | ||||||
|  |  | ||||||
|  |       const response = await sut.resetAdminPassword(ask); | ||||||
|  |  | ||||||
|  |       const [id, update] = userRepositoryMock.update.mock.calls[0]; | ||||||
|  |  | ||||||
|  |       expect(response.provided).toBe(true); | ||||||
|  |       expect(ask).toHaveBeenCalled(); | ||||||
|  |       expect(id).toEqual(adminUser.id); | ||||||
|  |       expect(update.password).toBeDefined(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; | ||||||
|  | import { randomBytes } from 'crypto'; | ||||||
| import { ReadStream } from 'fs'; | import { ReadStream } from 'fs'; | ||||||
| import { AuthUserDto } from '../auth'; | import { AuthUserDto } from '../auth'; | ||||||
| import { IUserRepository } from '../user'; | import { IUserRepository } from '../user'; | ||||||
| @@ -104,4 +105,18 @@ export class UserService { | |||||||
|     } |     } | ||||||
|     return this.userCore.getUserProfileImage(user); |     return this.userCore.getUserProfileImage(user); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) { | ||||||
|  |     const admin = await this.userCore.getAdmin(); | ||||||
|  |     if (!admin) { | ||||||
|  |       throw new BadRequestException('Admin account does not exist'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const providedPassword = await ask(admin); | ||||||
|  |     const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, ''); | ||||||
|  |  | ||||||
|  |     await this.userCore.updateUser(admin, admin.id, { password }); | ||||||
|  |  | ||||||
|  |     return { admin, password, provided: !!providedPassword }; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user