浏览代码

refactor: reset admin password (#1335)

* refactor: reset-admin-password

* chore: docs
Jason Rasmussen 2 年之前
父节点
当前提交
1e2f02613f

二进制
docs/docs/features/img/disable-password-login.png


二进制
docs/docs/features/img/enable-password-login.png


二进制
docs/docs/features/img/reset-admin-password.png


+ 7 - 18
docs/docs/features/server-commands.md

@@ -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)

+ 18 - 2
docs/docs/guides/docker-help.md

@@ -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

+ 24 - 23
server/apps/cli/src/commands/reset-admin-password.command.ts

@@ -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 { password: providedPassword } = await this.inquirer.ask<{ password: string }>('prompt-password', undefined);
-    const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, '');
-
-    await this.userCore.updateUser(user, user.id, { password });
-
-    if (providedPassword) {
-      console.log('The admin password has been updated.');
-    } else {
-      console.log(`The admin password has been updated to:\n${password}`);
+    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}`);
+
+      return this.inquirer.ask<{ password: string }>('prompt-password', undefined).then(({ password }) => password);
+    };
+
+    try {
+      const { password, provided } = await this.userService.resetAdminPassword(ask);
+
+      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');
     }
   }
 }

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

@@ -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();
+    });
+  });
 });

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

@@ -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 };
+  }
 }