1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-16 16:56:46 +01:00

refactor(server): cli service (#9672)

This commit is contained in:
Jason Rasmussen 2024-05-22 16:23:47 -04:00 committed by GitHub
parent 967d195a05
commit 13cbdf6851
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 176 additions and 91 deletions

View file

@ -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<void> {
try {
const users = await this.userService.listUsers();
const users = await this.service.listUsers();
console.dir(users);
} catch (error) {
console.error(error);

View file

@ -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<void> {
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<void> {
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.');
}
}

View file

@ -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<void> {
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<void> {
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.');
}
}

View file

@ -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<void> {
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.`);

View file

@ -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<IUserRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let libraryMock: Mocked<ILibraryRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
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();
});
});
});

View file

@ -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<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: true });
return users.map((user) => mapUser(user));
}
async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) {
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<void> {
const config = await this.configCore.getConfig();
config.passwordLogin.enabled = false;
await this.configCore.updateConfig(config);
}
async enablePasswordLogin(): Promise<void> {
const config = await this.configCore.getConfig();
config.passwordLogin.enabled = true;
await this.configCore.updateConfig(config);
}
async disableOAuthLogin(): Promise<void> {
const config = await this.configCore.getConfig();
config.oauth.enabled = false;
await this.configCore.updateConfig(config);
}
async enableOAuthLogin(): Promise<void> {
const config = await this.configCore.getConfig();
config.oauth.enabled = true;
await this.configCore.updateConfig(config);
}
}

View file

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

View file

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

View file

@ -170,20 +170,6 @@ export class UserService {
});
}
async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) {
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<JobStatus> {
await this.userRepository.syncUsage();
return JobStatus.SUCCESS;