import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { makeErrorResponse } from "../../common/error/makeErrorResponse"; import { makePassword } from "../../common/password/password"; import { AdB2cService, ConflictError, isConflictError, } from "../../gateways/adb2c/adb2c.service"; import { User as EntityUser, newUser, } from "../../repositories/users/entity/user.entity"; import { UsersRepositoryService } from "../../repositories/users/users.repository.service"; import { MANUAL_RECOVERY_REQUIRED, USER_ROLES } from "../../constants"; import { Context } from "../../common/log"; import { UserRoles } from "../../common/types/role"; @Injectable() export class UsersService { private readonly logger = new Logger(UsersService.name); constructor( private readonly usersRepository: UsersRepositoryService, private readonly adB2cService: AdB2cService ) {} /** * Creates user * @param context * @param name * @param role * @param email * @param autoRenew * @param notification * @param accountId * @param userid * @param [authorId] * @param [encryption] * @param [encryptionPassword] * @param [prompt] * @returns user */ async createUser( context: Context, name: string, role: UserRoles, email: string, autoRenew: boolean, notification: boolean, accountId: number, userid: number, authorId?: string | undefined, encryption?: boolean | undefined, encryptionPassword?: string | undefined, prompt?: boolean | undefined ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.createUser.name} | params: { ` + `role: ${role}, ` + `autoRenew: ${autoRenew}, ` + `notification: ${notification}, ` + `accountId: ${accountId}, ` + `userid: ${userid}, ` + `authorId: ${authorId}, ` + `encryption: ${encryption}, ` + `prompt: ${prompt} };` ); //authorIdが重複していないかチェックする if (authorId) { let isAuthorIdDuplicated = false; try { isAuthorIdDuplicated = await this.usersRepository.existsAuthorId( context, accountId, authorId ); this.logger.log( `[${context.getTrackingId()}] isAuthorIdDuplicated=${isAuthorIdDuplicated}` ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( makeErrorResponse("E009999"), HttpStatus.INTERNAL_SERVER_ERROR ); } if (isAuthorIdDuplicated) { throw new HttpException( makeErrorResponse("E010302"), HttpStatus.BAD_REQUEST ); } } this.logger.log("ランダムパスワード生成開始"); // ランダムなパスワードを生成する const ramdomPassword = makePassword(); this.logger.log("ランダムパスワード生成完了"); //Azure AD B2Cにユーザーを新規登録する let externalUser: { sub: string } | ConflictError; try { this.logger.log(`name=${name}`); // idpにユーザーを作成 externalUser = await this.adB2cService.createUser( context, email, ramdomPassword, name ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error( `[${context.getTrackingId()}] create externalUser failed` ); throw new HttpException( makeErrorResponse("E009999"), HttpStatus.INTERNAL_SERVER_ERROR ); } // メールアドレス重複エラー if (isConflictError(externalUser)) { throw new HttpException( makeErrorResponse("E010301"), HttpStatus.BAD_REQUEST ); } //Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する let newUser: EntityUser; try { //roleに応じてユーザー情報を作成する const newUserInfo = this.createNewUserInfo( context, userid, role, accountId, externalUser.sub, autoRenew, notification, authorId, encryption, encryptionPassword, prompt ); // ユーザ作成 newUser = await this.usersRepository.createNormalUser( context, newUserInfo ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}]create user failed`); //リカバリー処理 //Azure AD B2Cに登録したユーザー情報を削除する await this.deleteB2cUser(externalUser.sub, context); switch (e.code) { case "ER_DUP_ENTRY": //AuthorID重複エラー throw new HttpException( makeErrorResponse("E010302"), HttpStatus.BAD_REQUEST ); default: throw new HttpException( makeErrorResponse("E009999"), HttpStatus.INTERNAL_SERVER_ERROR ); } } this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.createUser.name}` ); return; } // Azure AD B2Cに登録したユーザー情報を削除する // TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補 private async deleteB2cUser(externalUserId: string, context: Context) { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.deleteB2cUser.name } | params: { externalUserId: ${externalUserId} }` ); try { await this.adB2cService.deleteUser(externalUserId, context); this.logger.log( `[${context.getTrackingId()}] delete externalUser: ${externalUserId}` ); } catch (error) { this.logger.error(`[${context.getTrackingId()}] error=${error}`); this.logger.error( `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}` ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.deleteB2cUser.name}` ); } } // DBに登録したユーザー情報を削除する private async deleteUser(userId: number, context: Context) { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.deleteUser.name } | params: { userId: ${userId} }` ); try { await this.usersRepository.deleteNormalUser(context, userId); this.logger.log(`[${context.getTrackingId()}] delete user: ${userId}`); } catch (error) { this.logger.error(`[${context.getTrackingId()}] error=${error}`); this.logger.error( `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete user: ${userId}` ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}` ); } } // roleを受け取って、roleに応じたnewUserを作成して返却する private createNewUserInfo( context: Context, id: number, role: UserRoles, accountId: number, externalId: string, autoRenew: boolean, notification: boolean, authorId?: string | undefined, encryption?: boolean | undefined, encryptionPassword?: string | undefined, prompt?: boolean | undefined ): newUser { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.createNewUserInfo.name } | params: { ` + `id: ${id}, ` + `role: ${role}, ` + `accountId: ${accountId}, ` + `authorId: ${authorId}, ` + `externalId: ${externalId}, ` + `autoRenew: ${autoRenew}, ` + `notification: ${notification}, ` + `authorId: ${authorId}, ` + `encryption: ${encryption}, ` + `prompt: ${prompt} };` ); try { switch (role) { case USER_ROLES.NONE: case USER_ROLES.TYPIST: return { id, account_id: accountId, external_id: externalId, auto_renew: autoRenew, notification, role, accepted_dpa_version: null, accepted_eula_version: null, accepted_privacy_notice_version: null, encryption: false, encryption_password: null, prompt: false, author_id: null, }; case USER_ROLES.AUTHOR: return { id, account_id: accountId, external_id: externalId, auto_renew: autoRenew, notification, role, author_id: authorId ?? null, encryption: encryption ?? false, encryption_password: encryptionPassword ?? null, prompt: prompt ?? false, accepted_dpa_version: null, accepted_eula_version: null, accepted_privacy_notice_version: null, }; default: //不正なroleが指定された場合はログを出力してエラーを返す this.logger.error( `[${context.getTrackingId()}] [NOT IMPLEMENT] [RECOVER] role: ${role}` ); throw new HttpException( makeErrorResponse("E009999"), HttpStatus.INTERNAL_SERVER_ERROR ); } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); return e; } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.createNewUserInfo.name}` ); } } }