diff --git a/docs/docs/features/img/disable-password-login.png b/docs/docs/features/img/disable-password-login.png new file mode 100644 index 0000000000..e83059b763 Binary files /dev/null and b/docs/docs/features/img/disable-password-login.png differ diff --git a/docs/docs/features/img/enable-password-login.png b/docs/docs/features/img/enable-password-login.png new file mode 100644 index 0000000000..e4827a2c9d Binary files /dev/null and b/docs/docs/features/img/enable-password-login.png differ diff --git a/docs/docs/features/img/reset-admin-password.png b/docs/docs/features/img/reset-admin-password.png new file mode 100644 index 0000000000..57ee230d34 Binary files /dev/null and b/docs/docs/features/img/reset-admin-password.png differ diff --git a/docs/docs/features/server-commands.md b/docs/docs/features/server-commands.md index d8c017cade..3ad7d3363f 100644 --- a/docs/docs/features/server-commands.md +++ b/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 `. +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 `. ## 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) diff --git a/docs/docs/guides/docker-help.md b/docs/docs/guides/docker-help.md index a2e633efd6..e85d62258a 100644 --- a/docs/docs/guides/docker-help.md +++ b/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 # 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 # see the logs for a specific container (by id or name) docker logs immich_server diff --git a/server/apps/cli/src/commands/reset-admin-password.command.ts b/server/apps/cli/src/commands/reset-admin-password.command.ts index 2c66c8a961..b12a549963 100644 --- a/server/apps/cli/src/commands/reset-admin-password.command.ts +++ b/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 { - 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'); } } } diff --git a/server/libs/domain/src/user/user.service.spec.ts b/server/libs/domain/src/user/user.service.spec.ts index ebafa8ac68..fac5166a1a 100644 --- a/server/libs/domain/src/user/user.service.spec.ts +++ b/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(); + }); + }); }); diff --git a/server/libs/domain/src/user/user.service.ts b/server/libs/domain/src/user/user.service.ts index 77d52c9087..e0d02876b9 100644 --- a/server/libs/domain/src/user/user.service.ts +++ b/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) { + 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 }; + } }