mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04:00
refactor(server): cli service (#9672)
This commit is contained in:
parent
967d195a05
commit
13cbdf6851
@ -1,18 +1,18 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'list-users',
|
name: 'list-users',
|
||||||
description: 'List Immich users',
|
description: 'List Immich users',
|
||||||
})
|
})
|
||||||
export class ListUsersCommand extends CommandRunner {
|
export class ListUsersCommand extends CommandRunner {
|
||||||
constructor(private userService: UserService) {
|
constructor(private service: CliService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const users = await this.userService.listUsers();
|
const users = await this.service.listUsers();
|
||||||
console.dir(users);
|
console.dir(users);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'enable-oauth-login',
|
name: 'enable-oauth-login',
|
||||||
description: 'Enable OAuth login',
|
description: 'Enable OAuth login',
|
||||||
})
|
})
|
||||||
export class EnableOAuthLogin extends CommandRunner {
|
export class EnableOAuthLogin extends CommandRunner {
|
||||||
constructor(private configService: SystemConfigService) {
|
constructor(private service: CliService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
const config = await this.configService.getConfig();
|
await this.service.enableOAuthLogin();
|
||||||
config.oauth.enabled = true;
|
|
||||||
await this.configService.updateConfig(config);
|
|
||||||
console.log('OAuth login has been enabled.');
|
console.log('OAuth login has been enabled.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,14 +21,12 @@ export class EnableOAuthLogin extends CommandRunner {
|
|||||||
description: 'Disable OAuth login',
|
description: 'Disable OAuth login',
|
||||||
})
|
})
|
||||||
export class DisableOAuthLogin extends CommandRunner {
|
export class DisableOAuthLogin extends CommandRunner {
|
||||||
constructor(private configService: SystemConfigService) {
|
constructor(private service: CliService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
const config = await this.configService.getConfig();
|
await this.service.disableOAuthLogin();
|
||||||
config.oauth.enabled = false;
|
|
||||||
await this.configService.updateConfig(config);
|
|
||||||
console.log('OAuth login has been disabled.');
|
console.log('OAuth login has been disabled.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'enable-password-login',
|
name: 'enable-password-login',
|
||||||
description: 'Enable password login',
|
description: 'Enable password login',
|
||||||
})
|
})
|
||||||
export class EnablePasswordLoginCommand extends CommandRunner {
|
export class EnablePasswordLoginCommand extends CommandRunner {
|
||||||
constructor(private configService: SystemConfigService) {
|
constructor(private service: CliService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
const config = await this.configService.getConfig();
|
await this.service.enablePasswordLogin();
|
||||||
config.passwordLogin.enabled = true;
|
|
||||||
await this.configService.updateConfig(config);
|
|
||||||
console.log('Password login has been enabled.');
|
console.log('Password login has been enabled.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,14 +21,12 @@ export class EnablePasswordLoginCommand extends CommandRunner {
|
|||||||
description: 'Disable password login',
|
description: 'Disable password login',
|
||||||
})
|
})
|
||||||
export class DisablePasswordLoginCommand extends CommandRunner {
|
export class DisablePasswordLoginCommand extends CommandRunner {
|
||||||
constructor(private configService: SystemConfigService) {
|
constructor(private service: CliService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
const config = await this.configService.getConfig();
|
await this.service.disablePasswordLogin();
|
||||||
config.passwordLogin.enabled = false;
|
|
||||||
await this.configService.updateConfig(config);
|
|
||||||
console.log('Password login has been disabled.');
|
console.log('Password login has been disabled.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
|
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
|
||||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
|
|
||||||
@Command({
|
const prompt = (inquirer: InquirerService) => {
|
||||||
name: 'reset-admin-password',
|
return function ask(admin: UserResponseDto) {
|
||||||
description: 'Reset the admin password',
|
|
||||||
})
|
|
||||||
export class ResetAdminPasswordCommand extends CommandRunner {
|
|
||||||
constructor(
|
|
||||||
private userService: UserService,
|
|
||||||
private inquirer: InquirerService,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
ask = (admin: UserResponseDto) => {
|
|
||||||
const { id, oauthId, email, name } = admin;
|
const { id, oauthId, email, name } = admin;
|
||||||
console.log(`Found Admin:
|
console.log(`Found Admin:
|
||||||
- ID=${id}
|
- ID=${id}
|
||||||
@ -22,12 +11,25 @@ export class ResetAdminPasswordCommand extends CommandRunner {
|
|||||||
- Email=${email}
|
- Email=${email}
|
||||||
- Name=${name}`);
|
- 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> {
|
async run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { password, provided } = await this.userService.resetAdminPassword(this.ask);
|
const { password, provided } = await this.service.resetAdminPassword(prompt(this.inquirer));
|
||||||
|
|
||||||
if (provided) {
|
if (provided) {
|
||||||
console.log(`The admin password has been updated.`);
|
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 { AssetService } from 'src/services/asset.service';
|
||||||
import { AuditService } from 'src/services/audit.service';
|
import { AuditService } from 'src/services/audit.service';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
|
import { CliService } from 'src/services/cli.service';
|
||||||
import { DatabaseService } from 'src/services/database.service';
|
import { DatabaseService } from 'src/services/database.service';
|
||||||
import { DownloadService } from 'src/services/download.service';
|
import { DownloadService } from 'src/services/download.service';
|
||||||
import { DuplicateService } from 'src/services/duplicate.service';
|
import { DuplicateService } from 'src/services/duplicate.service';
|
||||||
@ -44,6 +45,7 @@ export const services = [
|
|||||||
AssetServiceV1,
|
AssetServiceV1,
|
||||||
AuditService,
|
AuditService,
|
||||||
AuthService,
|
AuthService,
|
||||||
|
CliService,
|
||||||
DatabaseService,
|
DatabaseService,
|
||||||
DownloadService,
|
DownloadService,
|
||||||
DuplicateService,
|
DuplicateService,
|
||||||
|
@ -27,7 +27,7 @@ import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.moc
|
|||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||||
import { Mocked, vitest } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
const makeDeletedAt = (daysAgo: number) => {
|
const makeDeletedAt = (daysAgo: number) => {
|
||||||
const deletedAt = new Date();
|
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', () => {
|
describe('handleQueueUserDelete', () => {
|
||||||
it('should skip users not ready for deletion', async () => {
|
it('should skip users not ready for deletion', async () => {
|
||||||
userMock.getDeletedUsers.mockResolvedValue([
|
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> {
|
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||||
await this.userRepository.syncUsage();
|
await this.userRepository.syncUsage();
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user