Skip to content

Commit

Permalink
refactor(server): cli service (immich-app#9672)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 authored May 22, 2024
1 parent 967d195 commit 13cbdf6
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 89 deletions.
6 changes: 3 additions & 3 deletions server/src/commands/list-users.command.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
14 changes: 5 additions & 9 deletions server/src/commands/oauth-login.ts
Original file line number Diff line number Diff line change
@@ -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.');
}
}
Expand All @@ -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.');
}
}
14 changes: 5 additions & 9 deletions server/src/commands/password-login.ts
Original file line number Diff line number Diff line change
@@ -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.');
}
}
Expand All @@ -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.');
}
}
30 changes: 16 additions & 14 deletions server/src/commands/reset-admin-password.command.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
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';

const prompt = (inquirer: InquirerService) => {
return function ask(admin: UserResponseDto) {
const { id, oauthId, email, name } = admin;
console.log(`Found Admin:
- ID=${id}
- OAuth ID=${oauthId}
- Email=${email}
- Name=${name}`);

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 userService: UserService,
private service: CliService,
private inquirer: InquirerService,
) {
super();
}

ask = (admin: UserResponseDto) => {
const { id, oauthId, email, name } = admin;
console.log(`Found Admin:
- ID=${id}
- OAuth ID=${oauthId}
- Email=${email}
- Name=${name}`);

return this.inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password);
};

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.`);
Expand Down
72 changes: 72 additions & 0 deletions server/src/services/cli.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 70 additions & 0 deletions server/src/services/cli.service.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions server/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +45,7 @@ export const services = [
AssetServiceV1,
AuditService,
AuthService,
CliService,
DatabaseService,
DownloadService,
DuplicateService,
Expand Down
41 changes: 1 addition & 40 deletions server/src/services/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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([
Expand Down
14 changes: 0 additions & 14 deletions server/src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 13cbdf6

Please sign in to comment.