diff --git a/server/src/commands/list-users.command.ts b/server/src/commands/list-users.command.ts index ea3e745463..299ea82283 100644 --- a/server/src/commands/list-users.command.ts +++ b/server/src/commands/list-users.command.ts @@ -1,18 +1,18 @@ import { Command, CommandRunner } from 'nest-commander'; -import { UserService } from 'src/services/user.service'; +import { CliService } from 'src/services/cli.service'; @Command({ name: 'list-users', description: 'List Immich users', }) export class ListUsersCommand extends CommandRunner { - constructor(private userService: UserService) { + constructor(private service: CliService) { super(); } async run(): Promise { try { - const users = await this.userService.listUsers(); + const users = await this.service.listUsers(); console.dir(users); } catch (error) { console.error(error); diff --git a/server/src/commands/oauth-login.ts b/server/src/commands/oauth-login.ts index c9bb4d5ef4..9ec7013fa5 100644 --- a/server/src/commands/oauth-login.ts +++ b/server/src/commands/oauth-login.ts @@ -1,19 +1,17 @@ import { Command, CommandRunner } from 'nest-commander'; -import { SystemConfigService } from 'src/services/system-config.service'; +import { CliService } from 'src/services/cli.service'; @Command({ name: 'enable-oauth-login', description: 'Enable OAuth login', }) export class EnableOAuthLogin extends CommandRunner { - constructor(private configService: SystemConfigService) { + constructor(private service: CliService) { super(); } async run(): Promise { - const config = await this.configService.getConfig(); - config.oauth.enabled = true; - await this.configService.updateConfig(config); + await this.service.enableOAuthLogin(); console.log('OAuth login has been enabled.'); } } @@ -23,14 +21,12 @@ export class EnableOAuthLogin extends CommandRunner { description: 'Disable OAuth login', }) export class DisableOAuthLogin extends CommandRunner { - constructor(private configService: SystemConfigService) { + constructor(private service: CliService) { super(); } async run(): Promise { - const config = await this.configService.getConfig(); - config.oauth.enabled = false; - await this.configService.updateConfig(config); + await this.service.disableOAuthLogin(); console.log('OAuth login has been disabled.'); } } diff --git a/server/src/commands/password-login.ts b/server/src/commands/password-login.ts index 3d992f8583..057abd1649 100644 --- a/server/src/commands/password-login.ts +++ b/server/src/commands/password-login.ts @@ -1,19 +1,17 @@ import { Command, CommandRunner } from 'nest-commander'; -import { SystemConfigService } from 'src/services/system-config.service'; +import { CliService } from 'src/services/cli.service'; @Command({ name: 'enable-password-login', description: 'Enable password login', }) export class EnablePasswordLoginCommand extends CommandRunner { - constructor(private configService: SystemConfigService) { + constructor(private service: CliService) { super(); } async run(): Promise { - const config = await this.configService.getConfig(); - config.passwordLogin.enabled = true; - await this.configService.updateConfig(config); + await this.service.enablePasswordLogin(); console.log('Password login has been enabled.'); } } @@ -23,14 +21,12 @@ export class EnablePasswordLoginCommand extends CommandRunner { description: 'Disable password login', }) export class DisablePasswordLoginCommand extends CommandRunner { - constructor(private configService: SystemConfigService) { + constructor(private service: CliService) { super(); } async run(): Promise { - const config = await this.configService.getConfig(); - config.passwordLogin.enabled = false; - await this.configService.updateConfig(config); + await this.service.disablePasswordLogin(); console.log('Password login has been disabled.'); } } diff --git a/server/src/commands/reset-admin-password.command.ts b/server/src/commands/reset-admin-password.command.ts index f7c0775c8b..32f77109b0 100644 --- a/server/src/commands/reset-admin-password.command.ts +++ b/server/src/commands/reset-admin-password.command.ts @@ -1,20 +1,9 @@ import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; import { UserResponseDto } from 'src/dtos/user.dto'; -import { UserService } from 'src/services/user.service'; +import { CliService } from 'src/services/cli.service'; -@Command({ - name: 'reset-admin-password', - description: 'Reset the admin password', -}) -export class ResetAdminPasswordCommand extends CommandRunner { - constructor( - private userService: UserService, - private inquirer: InquirerService, - ) { - super(); - } - - ask = (admin: UserResponseDto) => { +const prompt = (inquirer: InquirerService) => { + return function ask(admin: UserResponseDto) { const { id, oauthId, email, name } = admin; console.log(`Found Admin: - ID=${id} @@ -22,12 +11,25 @@ export class ResetAdminPasswordCommand extends CommandRunner { - Email=${email} - Name=${name}`); - return this.inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password); + return inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password); }; +}; + +@Command({ + name: 'reset-admin-password', + description: 'Reset the admin password', +}) +export class ResetAdminPasswordCommand extends CommandRunner { + constructor( + private service: CliService, + private inquirer: InquirerService, + ) { + super(); + } async run(): Promise { try { - const { password, provided } = await this.userService.resetAdminPassword(this.ask); + const { password, provided } = await this.service.resetAdminPassword(prompt(this.inquirer)); if (provided) { console.log(`The admin password has been updated.`); diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts new file mode 100644 index 0000000000..016045fc17 --- /dev/null +++ b/server/src/services/cli.service.spec.ts @@ -0,0 +1,72 @@ +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { CliService } from 'src/services/cli.service'; +import { userStub } from 'test/fixtures/user.stub'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; +import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { Mocked, describe, it } from 'vitest'; + +describe(CliService.name, () => { + let sut: CliService; + + let userMock: Mocked; + let cryptoMock: Mocked; + let libraryMock: Mocked; + let systemMock: Mocked; + let loggerMock: Mocked; + + beforeEach(() => { + cryptoMock = newCryptoRepositoryMock(); + libraryMock = newLibraryRepositoryMock(); + systemMock = newSystemMetadataRepositoryMock(); + userMock = newUserRepositoryMock(); + loggerMock = newLoggerRepositoryMock(); + + sut = new CliService(cryptoMock, libraryMock, systemMock, userMock, loggerMock); + }); + + describe('resetAdminPassword', () => { + it('should only work when there is an admin account', async () => { + userMock.getAdmin.mockResolvedValue(null); + const ask = vitest.fn().mockResolvedValue('new-password'); + + await expect(sut.resetAdminPassword(ask)).rejects.toThrowError('Admin account does not exist'); + + expect(ask).not.toHaveBeenCalled(); + }); + + it('should default to a random password', async () => { + userMock.getAdmin.mockResolvedValue(userStub.admin); + const ask = vitest.fn().mockImplementation(() => {}); + + const response = await sut.resetAdminPassword(ask); + + const [id, update] = userMock.update.mock.calls[0]; + + expect(response.provided).toBe(false); + expect(ask).toHaveBeenCalled(); + expect(id).toEqual(userStub.admin.id); + expect(update.password).toBeDefined(); + }); + + it('should use the supplied password', async () => { + userMock.getAdmin.mockResolvedValue(userStub.admin); + const ask = vitest.fn().mockResolvedValue('new-password'); + + const response = await sut.resetAdminPassword(ask); + + const [id, update] = userMock.update.mock.calls[0]; + + expect(response.provided).toBe(true); + expect(ask).toHaveBeenCalled(); + expect(id).toEqual(userStub.admin.id); + expect(update.password).toBeDefined(); + }); + }); +}); diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts new file mode 100644 index 0000000000..d4d838d0c4 --- /dev/null +++ b/server/src/services/cli.service.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { UserCore } from 'src/cores/user.core'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; + +@Injectable() +export class CliService { + private configCore: SystemConfigCore; + private userCore: UserCore; + + constructor( + @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, + @Inject(ILibraryRepository) libraryRepository: ILibraryRepository, + @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, + @Inject(IUserRepository) private userRepository: IUserRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, + ) { + this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository); + this.logger.setContext(CliService.name); + this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); + } + + async listUsers(): Promise { + const users = await this.userRepository.getList({ withDeleted: true }); + return users.map((user) => mapUser(user)); + } + + async resetAdminPassword(ask: (admin: UserResponseDto) => Promise) { + const admin = await this.userRepository.getAdmin(); + if (!admin) { + throw new Error('Admin account does not exist'); + } + + const providedPassword = await ask(mapUser(admin)); + const password = providedPassword || this.cryptoRepository.newPassword(24); + + await this.userCore.updateUser(admin, admin.id, { password }); + + return { admin, password, provided: !!providedPassword }; + } + + async disablePasswordLogin(): Promise { + const config = await this.configCore.getConfig(); + config.passwordLogin.enabled = false; + await this.configCore.updateConfig(config); + } + + async enablePasswordLogin(): Promise { + const config = await this.configCore.getConfig(); + config.passwordLogin.enabled = true; + await this.configCore.updateConfig(config); + } + + async disableOAuthLogin(): Promise { + const config = await this.configCore.getConfig(); + config.oauth.enabled = false; + await this.configCore.updateConfig(config); + } + + async enableOAuthLogin(): Promise { + const config = await this.configCore.getConfig(); + config.oauth.enabled = true; + await this.configCore.updateConfig(config); + } +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index c9331c00c7..76fe7244c0 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -6,6 +6,7 @@ import { AssetServiceV1 } from 'src/services/asset-v1.service'; import { AssetService } from 'src/services/asset.service'; import { AuditService } from 'src/services/audit.service'; import { AuthService } from 'src/services/auth.service'; +import { CliService } from 'src/services/cli.service'; import { DatabaseService } from 'src/services/database.service'; import { DownloadService } from 'src/services/download.service'; import { DuplicateService } from 'src/services/duplicate.service'; @@ -44,6 +45,7 @@ export const services = [ AssetServiceV1, AuditService, AuthService, + CliService, DatabaseService, DownloadService, DuplicateService, diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index e96cb0e663..b46984ce7e 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -27,7 +27,7 @@ import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.moc import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; -import { Mocked, vitest } from 'vitest'; +import { Mocked } from 'vitest'; const makeDeletedAt = (daysAgo: number) => { const deletedAt = new Date(); @@ -436,45 +436,6 @@ describe(UserService.name, () => { }); }); - describe('resetAdminPassword', () => { - it('should only work when there is an admin account', async () => { - userMock.getAdmin.mockResolvedValue(null); - const ask = vitest.fn().mockResolvedValue('new-password'); - - await expect(sut.resetAdminPassword(ask)).rejects.toBeInstanceOf(BadRequestException); - - expect(ask).not.toHaveBeenCalled(); - }); - - it('should default to a random password', async () => { - userMock.getAdmin.mockResolvedValue(userStub.admin); - const ask = vitest.fn().mockImplementation(() => {}); - - const response = await sut.resetAdminPassword(ask); - - const [id, update] = userMock.update.mock.calls[0]; - - expect(response.provided).toBe(false); - expect(ask).toHaveBeenCalled(); - expect(id).toEqual(userStub.admin.id); - expect(update.password).toBeDefined(); - }); - - it('should use the supplied password', async () => { - userMock.getAdmin.mockResolvedValue(userStub.admin); - const ask = vitest.fn().mockResolvedValue('new-password'); - - const response = await sut.resetAdminPassword(ask); - - const [id, update] = userMock.update.mock.calls[0]; - - expect(response.provided).toBe(true); - expect(ask).toHaveBeenCalled(); - expect(id).toEqual(userStub.admin.id); - expect(update.password).toBeDefined(); - }); - }); - describe('handleQueueUserDelete', () => { it('should skip users not ready for deletion', async () => { userMock.getDeletedUsers.mockResolvedValue([ diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index d546705d3c..e62df2225f 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -170,20 +170,6 @@ export class UserService { }); } - async resetAdminPassword(ask: (admin: UserResponseDto) => Promise) { - const admin = await this.userRepository.getAdmin(); - if (!admin) { - throw new BadRequestException('Admin account does not exist'); - } - - const providedPassword = await ask(mapUser(admin)); - const password = providedPassword || this.cryptoRepository.newPassword(24); - - await this.userCore.updateUser(admin, admin.id, { password }); - - return { admin, password, provided: !!providedPassword }; - } - async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage(); return JobStatus.SUCCESS;