1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-27 10:58:13 +02:00

refactor(server): user core (#13063)

This commit is contained in:
Jason Rasmussen 2024-09-30 16:04:24 -04:00 committed by GitHub
parent dfc2d5002b
commit f63d251490
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 59 additions and 79 deletions

View File

@ -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<UserEntity> & { email: string }): Promise<UserEntity> {
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<UserEntity> = { ...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);
}
}

View File

@ -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({
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({
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);

View File

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

35
server/src/utils/user.ts Normal file
View File

@ -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<UserEntity> & { email: string },
): Promise<UserEntity> => {
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<UserEntity> = { ...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);
};

View File

@ -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<IUserRepository> => {
if (reset) {
UserCore.reset();
}
export const newUserRepositoryMock = (): Mocked<IUserRepository> => {
return {
get: vitest.fn(),
getAdmin: vitest.fn(),