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:
parent
967d195a05
commit
13cbdf6851
9 changed files with 176 additions and 91 deletions
|
@ -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);
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.`);
|
||||
|
|
72
server/src/services/cli.service.spec.ts
Normal file
72
server/src/services/cli.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
70
server/src/services/cli.service.ts
Normal file
70
server/src/services/cli.service.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue