From f63d251490de6d94dc47b853337b2a47801e9cc6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 30 Sep 2024 16:04:24 -0400 Subject: [PATCH] refactor(server): user core (#13063) --- server/src/cores/user.core.ts | 51 ------------------- server/src/services/auth.service.ts | 38 +++++++------- server/src/services/user-admin.service.ts | 7 +-- server/src/utils/user.ts | 35 +++++++++++++ .../test/repositories/user.repository.mock.ts | 7 +-- 5 files changed, 59 insertions(+), 79 deletions(-) delete mode 100644 server/src/cores/user.core.ts create mode 100644 server/src/utils/user.ts diff --git a/server/src/cores/user.core.ts b/server/src/cores/user.core.ts deleted file mode 100644 index 153463a9cc..0000000000 --- a/server/src/cores/user.core.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; -import sanitize from 'sanitize-filename'; -import { SALT_ROUNDS } from 'src/constants'; -import { UserEntity } from 'src/entities/user.entity'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; - -let instance: UserCore | null; - -export class UserCore { - private constructor( - private cryptoRepository: ICryptoRepository, - private userRepository: IUserRepository, - ) {} - - static create(cryptoRepository: ICryptoRepository, userRepository: IUserRepository) { - if (!instance) { - instance = new UserCore(cryptoRepository, userRepository); - } - - return instance; - } - - static reset() { - instance = null; - } - - async createUser(dto: Partial & { email: string }): Promise { - const user = await this.userRepository.getByEmail(dto.email); - if (user) { - throw new BadRequestException('User exists'); - } - - if (!dto.isAdmin) { - const localAdmin = await this.userRepository.getAdmin(); - if (!localAdmin) { - throw new BadRequestException('The first registered account must the administrator.'); - } - } - - const payload: Partial = { ...dto }; - if (payload.password) { - payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); - } - if (payload.storageLabel) { - payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); - } - - return this.userRepository.create(payload); - } -} diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 6b1e4c512f..0917fc2198 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -14,7 +14,6 @@ import { Issuer, UserinfoResponse, custom, generators } from 'openid-client'; import { SystemConfig } from 'src/config'; import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { UserCore } from 'src/cores/user.core'; import { AuthDto, ChangePasswordDto, @@ -42,6 +41,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf import { IUserRepository } from 'src/interfaces/user.interface'; import { isGranted } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; +import { createUser } from 'src/utils/user'; export interface LoginDetails { isSecure: boolean; @@ -72,7 +72,6 @@ export type ValidateRequest = { @Injectable() export class AuthService { private configCore: SystemConfigCore; - private userCore: UserCore; constructor( @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @@ -86,7 +85,6 @@ export class AuthService { ) { this.logger.setContext(AuthService.name); this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); - this.userCore = UserCore.create(cryptoRepository, userRepository); custom.setHttpOptionsDefaults({ timeout: 30_000 }); } @@ -150,13 +148,16 @@ export class AuthService { throw new BadRequestException('The server already has an admin'); } - const admin = await this.userCore.createUser({ - isAdmin: true, - email: dto.email, - name: dto.name, - password: dto.password, - storageLabel: 'admin', - }); + const admin = await createUser( + { userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, + { + isAdmin: true, + email: dto.email, + name: dto.name, + password: dto.password, + storageLabel: 'admin', + }, + ); return mapUserAdmin(admin); } @@ -271,13 +272,16 @@ export class AuthService { }); const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`; - user = await this.userCore.createUser({ - name: userName, - email: profile.email, - oauthId: profile.sub, - quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null, - storageLabel: storageLabel || null, - }); + user = await createUser( + { userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, + { + name: userName, + email: profile.email, + oauthId: profile.sub, + quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null, + storageLabel: storageLabel || null, + }, + ); } return this.createLoginResponse(user, loginDetails); diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 6a5b6ea06e..75dff32f16 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; import { SALT_ROUNDS } from 'src/constants'; -import { UserCore } from 'src/cores/user.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { @@ -19,11 +18,10 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; +import { createUser } from 'src/utils/user'; @Injectable() export class UserAdminService { - private userCore: UserCore; - constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @@ -32,7 +30,6 @@ export class UserAdminService { @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.userCore = UserCore.create(cryptoRepository, userRepository); this.logger.setContext(UserAdminService.name); } @@ -43,7 +40,7 @@ export class UserAdminService { async create(dto: UserAdminCreateDto): Promise { const { notify, ...rest } = dto; - const user = await this.userCore.createUser(rest); + const user = await createUser({ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, rest); await this.eventRepository.emit('user.signup', { notify: !!notify, diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts new file mode 100644 index 0000000000..c7029a1eca --- /dev/null +++ b/server/src/utils/user.ts @@ -0,0 +1,35 @@ +import { BadRequestException } from '@nestjs/common'; +import sanitize from 'sanitize-filename'; +import { SALT_ROUNDS } from 'src/constants'; +import { UserEntity } from 'src/entities/user.entity'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; + +type RepoDeps = { userRepo: IUserRepository; cryptoRepo: ICryptoRepository }; + +export const createUser = async ( + { userRepo, cryptoRepo }: RepoDeps, + dto: Partial & { email: string }, +): Promise => { + const user = await userRepo.getByEmail(dto.email); + if (user) { + throw new BadRequestException('User exists'); + } + + if (!dto.isAdmin) { + const localAdmin = await userRepo.getAdmin(); + if (!localAdmin) { + throw new BadRequestException('The first registered account must the administrator.'); + } + } + + const payload: Partial = { ...dto }; + if (payload.password) { + payload.password = await cryptoRepo.hashBcrypt(payload.password, SALT_ROUNDS); + } + if (payload.storageLabel) { + payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); + } + + return userRepo.create(payload); +}; diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts index 6071ae47fa..6362ab6a99 100644 --- a/server/test/repositories/user.repository.mock.ts +++ b/server/test/repositories/user.repository.mock.ts @@ -1,12 +1,7 @@ -import { UserCore } from 'src/cores/user.core'; import { IUserRepository } from 'src/interfaces/user.interface'; import { Mocked, vitest } from 'vitest'; -export const newUserRepositoryMock = (reset = true): Mocked => { - if (reset) { - UserCore.reset(); - } - +export const newUserRepositoryMock = (): Mocked => { return { get: vitest.fn(), getAdmin: vitest.fn(),