mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
refactor: reset admin password (#1335)
* refactor: reset-admin-password * chore: docs
This commit is contained in:
parent
5a6a726014
commit
1e2f02613f
8 changed files with 100 additions and 40 deletions
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
|
||||
|
||||
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
|
||||
|
||||
```bash title="Reset Admin Password"
|
||||
docker exec -it immich_server sh
|
||||
Reset Admin Password
|
||||
|
||||
/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
|
||||
```
|
||||
![Reset Admin Password](./img/reset-admin-password.png)
|
||||
|
||||
```bash title="Disable Password Login"
|
||||
docker exec -it immich_server sh
|
||||
Disable Password Login
|
||||
|
||||
/usr/src/app$ immich disable-password-login
|
||||
Password login has been disabled.
|
||||
```
|
||||
![Disable Password Login](./img/disable-password-login.png)
|
||||
|
||||
```bash title="Enable Password Login"
|
||||
docker exec -it immich_server sh
|
||||
Enabled Password Login
|
||||
|
||||
/usr/src/app$ immich enable-password-login
|
||||
Password login has been enabled.
|
||||
```
|
||||
![Enable Password Login](./img/enable-password-login.png)
|
||||
|
|
|
@ -4,11 +4,27 @@ sidebar_position: 1
|
|||
|
||||
# Docker Help
|
||||
|
||||
## Logs
|
||||
## Containers
|
||||
|
||||
```bash title="Log Examples"
|
||||
```bash
|
||||
docker ps # see a list of running 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 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 { randomBytes } from 'node:crypto';
|
||||
import { IUserRepository, UserCore } from '@app/domain';
|
||||
|
||||
@Command({
|
||||
name: 'reset-admin-password',
|
||||
description: 'Reset the admin password',
|
||||
})
|
||||
export class ResetAdminPasswordCommand extends CommandRunner {
|
||||
userCore: UserCore;
|
||||
|
||||
constructor(private readonly inquirer: InquirerService, @Inject(IUserRepository) userRepository: IUserRepository) {
|
||||
constructor(private userService: UserService, private readonly inquirer: InquirerService) {
|
||||
super();
|
||||
|
||||
this.userCore = new UserCore(userRepository);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const user = await this.userCore.getAdmin();
|
||||
if (!user) {
|
||||
console.log('Unable to reset password: no admin user.');
|
||||
return;
|
||||
}
|
||||
const ask = (admin: UserResponseDto) => {
|
||||
const { id, oauthId, email, firstName, lastName } = admin;
|
||||
console.log(`Found Admin:
|
||||
- ID=${id}
|
||||
- OAuth ID=${oauthId}
|
||||
- Email=${email}
|
||||
- Name=${firstName} ${lastName}`);
|
||||
|
||||
const { password: providedPassword } = await this.inquirer.ask<{ password: string }>('prompt-password', undefined);
|
||||
const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, '');
|
||||
return this.inquirer.ask<{ password: string }>('prompt-password', undefined).then(({ password }) => password);
|
||||
};
|
||||
|
||||
await this.userCore.updateUser(user, user.id, { password });
|
||||
try {
|
||||
const { password, provided } = await this.userService.resetAdminPassword(ask);
|
||||
|
||||
if (providedPassword) {
|
||||
console.log('The admin password has been updated.');
|
||||
} else {
|
||||
console.log(`The admin password has been updated to:\n${password}`);
|
||||
if (provided) {
|
||||
console.log(`The admin password has been updated.`);
|
||||
} else {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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 { randomBytes } from 'crypto';
|
||||
import { ReadStream } from 'fs';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { IUserRepository } from '../user';
|
||||
|
@ -104,4 +105,18 @@ export class UserService {
|
|||
}
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue