From 43d18ccc36e4b5eb840c5de087d9cbbfa5c871eb Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jason@rasm.me>
Date: Thu, 24 Oct 2024 17:24:37 -0400
Subject: [PATCH] refactor(server): user create logic (#13728)

---
 server/src/services/auth.service.ts       | 35 +++++++++--------------
 server/src/services/base.service.ts       | 29 ++++++++++++++++++-
 server/src/services/user-admin.service.ts | 10 +++----
 server/src/utils/user.ts                  | 35 -----------------------
 4 files changed, 47 insertions(+), 62 deletions(-)
 delete mode 100644 server/src/utils/user.ts

diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 00324c909c..b0094ae9ed 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -23,7 +23,6 @@ import { OAuthProfile } from 'src/interfaces/oauth.interface';
 import { BaseService } from 'src/services/base.service';
 import { isGranted } from 'src/utils/access';
 import { HumanReadableSize } from 'src/utils/bytes';
-import { createUser } from 'src/utils/user';
 
 export interface LoginDetails {
   isSecure: boolean;
@@ -115,16 +114,13 @@ export class AuthService extends BaseService {
       throw new BadRequestException('The server already has an admin');
     }
 
-    const admin = await createUser(
-      { userRepo: this.userRepository, cryptoRepo: this.cryptoRepository },
-      {
-        isAdmin: true,
-        email: dto.email,
-        name: dto.name,
-        password: dto.password,
-        storageLabel: 'admin',
-      },
-    );
+    const admin = await this.createUser({
+      isAdmin: true,
+      email: dto.email,
+      name: dto.name,
+      password: dto.password,
+      storageLabel: 'admin',
+    });
 
     return mapUserAdmin(admin);
   }
@@ -234,16 +230,13 @@ export class AuthService extends BaseService {
       });
 
       const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
-      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,
-        },
-      );
+      user = await this.createUser({
+        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/base.service.ts b/server/src/services/base.service.ts
index 441a81cf91..5ee4014e91 100644
--- a/server/src/services/base.service.ts
+++ b/server/src/services/base.service.ts
@@ -1,6 +1,9 @@
-import { Inject } from '@nestjs/common';
+import { BadRequestException, Inject } from '@nestjs/common';
+import sanitize from 'sanitize-filename';
 import { SystemConfig } from 'src/config';
+import { SALT_ROUNDS } from 'src/constants';
 import { StorageCore } from 'src/cores/storage.core';
+import { UserEntity } from 'src/entities/user.entity';
 import { IAccessRepository } from 'src/interfaces/access.interface';
 import { IActivityRepository } from 'src/interfaces/activity.interface';
 import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
@@ -119,4 +122,28 @@ export class BaseService {
   checkAccess(request: AccessRequest) {
     return checkAccess(this.accessRepository, request);
   }
+
+  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);
+  }
 }
diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts
index 84a5b5842d..a4be671c22 100644
--- a/server/src/services/user-admin.service.ts
+++ b/server/src/services/user-admin.service.ts
@@ -15,7 +15,6 @@ import { JobName } from 'src/interfaces/job.interface';
 import { UserFindOptions } from 'src/interfaces/user.interface';
 import { BaseService } from 'src/services/base.service';
 import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
-import { createUser } from 'src/utils/user';
 
 @Injectable()
 export class UserAdminService extends BaseService {
@@ -25,17 +24,18 @@ export class UserAdminService extends BaseService {
   }
 
   async create(dto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
-    const { notify, ...rest } = dto;
+    const { notify, ...userDto } = dto;
     const config = await this.getConfig({ withCache: false });
-    if (!config.oauth.enabled && !rest.password) {
+    if (!config.oauth.enabled && !userDto.password) {
       throw new BadRequestException('password is required');
     }
-    const user = await createUser({ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, rest);
+
+    const user = await this.createUser(userDto);
 
     await this.eventRepository.emit('user.signup', {
       notify: !!notify,
       id: user.id,
-      tempPassword: user.shouldChangePassword ? rest.password : undefined,
+      tempPassword: user.shouldChangePassword ? userDto.password : undefined,
     });
 
     return mapUserAdmin(user);
diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts
deleted file mode 100644
index c7029a1eca..0000000000
--- a/server/src/utils/user.ts
+++ /dev/null
@@ -1,35 +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';
-
-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);
-};