import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { isVerifyError, verify } from '../../common/jwt'; import { getPublicKey } from '../../common/jwt/jwt'; import { makePassword } from '../../common/password/password'; import { SortDirection, TaskListSortableAttribute, isSortDirection, isTaskListSortableAttribute, } from '../../common/types/sort'; import { AdB2cService, ConflictError, isConflictError, } from '../../gateways/adb2c/adb2c.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service'; import { User as EntityUser, newUser, } from '../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { GetRelationsResponse, User } from './types/types'; import { AuthorIdAlreadyExistsError, EmailAlreadyVerifiedError, EncryptionPasswordNeedError, InvalidRoleChangeError, UpdateTermsVersionNotSetError, UserNotFoundError, } from '../../repositories/users/errors/types'; import { LICENSE_EXPIRATION_THRESHOLD_DAYS, MANUAL_RECOVERY_REQUIRED, OPTION_ITEM_VALUE_TYPE_NUMBER, USER_AUDIO_FORMAT, USER_LICENSE_STATUS, USER_ROLES, } from '../../constants'; import { DateWithZeroTime } from '../licenses/types/types'; import { Context } from '../../common/log'; import { UserRoles } from '../../common/types/role'; import { LicenseAlreadyDeallocatedError, LicenseExpiredError, LicenseUnavailableError, } from '../../repositories/licenses/errors/types'; import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; @Injectable() export class UsersService { private readonly logger = new Logger(UsersService.name); private readonly mailFrom: string; private readonly appDomain: string; constructor( private readonly accountsRepository: AccountsRepositoryService, private readonly usersRepository: UsersRepositoryService, private readonly licensesRepository: LicensesRepositoryService, private readonly sortCriteriaRepository: SortCriteriaRepositoryService, private readonly adB2cService: AdB2cService, private readonly configService: ConfigService, private readonly sendgridService: SendGridService, ) { this.mailFrom = this.configService.getOrThrow('MAIL_FROM'); this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); } /** * Confirms user * @param token ユーザ仮登録時に払いだされるトークン */ async confirmUser(context: Context, token: string): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.confirmUser.name}`, ); const pubKey = getPublicKey(this.configService); const decodedToken = verify<{ accountId: number; userId: number; email: string; }>(token, pubKey); if (isVerifyError(decodedToken)) { throw new HttpException( makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST, ); } try { // トランザクションで取得と更新をまとめる const userId = decodedToken.userId; await this.usersRepository.updateUserVerifiedAndCreateTrialLicense( context, userId, ); try { const { company_name: companyName } = await this.accountsRepository.findAccountById( context, decodedToken.accountId, ); // アカウント認証が完了した旨をメール送信する await this.sendgridService.sendMailWithU101( context, decodedToken.email, companyName, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); // メール送信に関する例外はログだけ出して握りつぶす } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case EmailAlreadyVerifiedError: throw new HttpException( makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.confirmUser.name}`, ); } } /** * Creates user * @param accessToken * @param name * @param role * @param email * @param autoRenew * @param licenseAlert * @param notification * @param [authorId] * @param [encryption] * @param [encryptionPassword] * @param [prompt] * @returns void */ async createUser( context: Context, externalId: string, name: string, role: UserRoles, email: string, autoRenew: boolean, licenseAlert: boolean, notification: boolean, authorId?: string | undefined, encryption?: boolean | undefined, encryptionPassword?: string | undefined, prompt?: boolean | undefined, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.createUser.name} | params: { ` + `externalId: ${externalId}, ` + `role: ${role}, ` + `autoRenew: ${autoRenew}, ` + `licenseAlert: ${licenseAlert}, ` + `notification: ${notification}, ` + `authorId: ${authorId}, ` + `encryption: ${encryption}, ` + `prompt: ${prompt} };`, ); //DBよりアクセス者の所属するアカウントIDを取得する let adminUser: EntityUser; let account: Account | null; try { adminUser = await this.usersRepository.findUserByExternalId( context, externalId, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } const accountId = adminUser.account_id; account = adminUser.account; //authorIdが重複していないかチェックする if (authorId) { let isAuthorIdDuplicated = false; try { isAuthorIdDuplicated = await this.usersRepository.existsAuthorId( context, accountId, authorId, ); } 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, ); } } // ランダムなパスワードを生成する const ramdomPassword = makePassword(); //Azure AD B2Cにユーザーを新規登録する let externalUser: { sub: string } | ConflictError; try { // 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, role, accountId, externalUser.sub, autoRenew, licenseAlert, 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, ); } } //Email送信用のコンテンツを作成する try { if (account === null) { throw new Error(`account is null. account_id=${accountId}`); } const { primary_admin_user_id: primaryAdminUserId } = account; if (primaryAdminUserId === null) { throw new Error( `primary_admin_user_id is null. account_id=${accountId}`, ); } const { external_id: extarnalId } = await this.usersRepository.findUserById(context, primaryAdminUserId); const primaryAdmimAdb2cUser = await this.adB2cService.getUser( context, extarnalId, ); const { displayName: primaryAdminUserName } = getUserNameAndMailAddress( primaryAdmimAdb2cUser, ); await this.sendgridService.sendMailWithU114( context, accountId, newUser.id, email, primaryAdminUserName, ); } 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); // DBからユーザーを削除する await this.deleteUser(newUser.id, context); 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, role: UserRoles, accountId: number, externalId: string, autoRenew: boolean, licenseAlert: 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: { ` + `role: ${role}, ` + `accountId: ${accountId}, ` + `authorId: ${authorId}, ` + `externalId: ${externalId}, ` + `autoRenew: ${autoRenew}, ` + `licenseAlert: ${licenseAlert}, ` + `notification: ${notification}, ` + `authorId: ${authorId}, ` + `encryption: ${encryption}, ` + `prompt: ${prompt} };`, ); try { switch (role) { case USER_ROLES.NONE: case USER_ROLES.TYPIST: return { account_id: accountId, external_id: externalId, auto_renew: autoRenew, license_alert: licenseAlert, 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 { account_id: accountId, external_id: externalId, auto_renew: autoRenew, license_alert: licenseAlert, 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}`, ); } } /** * confirm User And Init Password * @param token ユーザ仮登録時に払いだされるトークン */ async confirmUserAndInitPassword( context: Context, token: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.confirmUserAndInitPassword.name }`, ); const pubKey = getPublicKey(this.configService); const decodedToken = verify<{ accountId: number; userId: number; email: string; }>(token, pubKey); if (isVerifyError(decodedToken)) { throw new HttpException( makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST, ); } // ランダムなパスワードを生成する const ramdomPassword = makePassword(); const { userId, email } = decodedToken; try { // ユーザー情報からAzure AD B2CのIDを特定する const user = await this.usersRepository.findUserById(context, userId); const extarnalId = user.external_id; // パスワードを変更する await this.adB2cService.changePassword( context, extarnalId, ramdomPassword, ); // ユーザを認証済みにする await this.usersRepository.updateUserVerified(context, userId); // TODO [Task2163] ODMS側が正式にメッセージを決めるまで仮のメール内容とする const subject = 'A temporary password has been issued.'; const text = 'temporary password: ' + ramdomPassword; const html = `

OMDS TOP PAGE URL.

${this.appDomain}
temporary password: ${ramdomPassword}`; // メールを送信 await this.sendgridService.sendMail( context, [email], [], this.mailFrom, subject, text, html, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case EmailAlreadyVerifiedError: throw new HttpException( makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${ this.confirmUserAndInitPassword.name }`, ); } } /** * Get Users * @param accessToken * @returns users */ async getUsers(context: Context, externalId: string): Promise { this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`); try { // DBから同一アカウントのユーザ一覧を取得する const dbUsers = await this.usersRepository.findSameAccountUsers( externalId, context, ); // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する const externalIds = dbUsers.map((x) => x.external_id); const adb2cUsers = await this.adB2cService.getUsers(context, externalIds); // DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出 const users = dbUsers.map((dbUser): User => { // ユーザーの所属グループ名を取得する const userGroupMembers = dbUser.userGroupMembers !== null ? dbUser.userGroupMembers : []; //所属グループ名の配列にする const groupNames = userGroupMembers.flatMap((userGroupMember) => userGroupMember.userGroup ? [userGroupMember.userGroup.name] : [], ); const adb2cUser = adb2cUsers.find( (user) => user.id === dbUser.external_id, ); if (adb2cUser == null) { throw new Error('mail not found.'); // TODO: リファクタ時に挙動を変更しないようエラー文面をmail not foundのまま据え置き。影響がない事が確認できたらエラー文面を変更する。 } // メールアドレスを取得する const { emailAddress: mail } = getUserNameAndMailAddress(adb2cUser); //メールアドレスが取得できない場合はエラー if (!mail) { throw new Error('mail not found.'); } let status = USER_LICENSE_STATUS.NORMAL; // ライセンスの有効期限と残日数は、ライセンスが存在する場合のみ算出する // ライセンスが存在しない場合は、undefinedのままとする let expiration: string | undefined = undefined; let remaining: number | undefined = undefined; if (dbUser.license) { // 有効期限日付 YYYY/MM/DD const expiry_date = dbUser.license.expiry_date ?? undefined; expiration = expiry_date !== undefined ? `${expiry_date.getFullYear()}/${ expiry_date.getMonth() + 1 }/${expiry_date.getDate()}` : undefined; const currentDate = new DateWithZeroTime(); // 有効期限までの日数 remaining = expiry_date !== undefined ? Math.floor( (expiry_date.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24), ) : undefined; if ( remaining !== undefined && remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS ) { status = dbUser.auto_renew ? USER_LICENSE_STATUS.RENEW : USER_LICENSE_STATUS.ALERT; } } else { status = USER_LICENSE_STATUS.NO_LICENSE; } return { id: dbUser.id, name: adb2cUser.displayName, role: dbUser.role, authorId: dbUser.author_id ?? undefined, typistGroupName: groupNames, email: mail, emailVerified: dbUser.email_verified, autoRenew: dbUser.auto_renew, licenseAlert: dbUser.license_alert, notification: dbUser.notification, encryption: dbUser.encryption, prompt: dbUser.prompt, expiration: expiration, remaining: remaining, licenseStatus: status, }; }); return users; } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.NOT_FOUND, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.getUsers.name}`, ); } } /** * Updates sort criteria * @param paramName * @param direction * @param token * @returns sort criteria */ async updateSortCriteria( context: Context, paramName: TaskListSortableAttribute, direction: SortDirection, externalId: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.updateSortCriteria.name } | params: { paramName: ${paramName}, direction: ${direction}, externalId: ${externalId} };`, ); let user: EntityUser; try { // ユーザー情報を取得 user = await this.usersRepository.findUserByExternalId( context, externalId, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } try { // ユーザーのソート条件を更新 await this.sortCriteriaRepository.updateSortCriteria( context, user.id, paramName, direction, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.updateSortCriteria.name}`, ); } } /** * Gets sort criteria * @param token * @returns sort criteria */ async getSortCriteria( context: Context, externalId: string, ): Promise<{ paramName: TaskListSortableAttribute; direction: SortDirection; }> { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.getSortCriteria.name } | params: { externalId: ${externalId} };`, ); let user: EntityUser; try { // ユーザー情報を取得 user = await this.usersRepository.findUserByExternalId( context, externalId, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } try { // ユーザーのソート条件を取得 const sortCriteria = await this.sortCriteriaRepository.getSortCriteria( context, user.id, ); const { direction, parameter } = sortCriteria; //型チェック if ( !isTaskListSortableAttribute(parameter) || !isSortDirection(direction) ) { throw new Error('The value stored in the DB is invalid.'); } return { direction, paramName: parameter }; } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.getSortCriteria.name}`, ); } } /** * 指定したユーザーの文字起こし業務に関連する情報を取得します * @param userId * @returns relations */ async getRelations( context: Context, userId: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.getRelations.name } | params: { userId: ${userId} };`, ); try { const { id } = await this.usersRepository.findUserByExternalId( context, userId, ); // ユーザー関連情報を取得 const { user, authors, worktypes, activeWorktype } = await this.usersRepository.getUserRelations(context, id); // AuthorIDのリストを作成 const authorIds = authors.flatMap((author) => author.author_id ? [author.author_id] : [], ); const workTypeList = worktypes?.map((worktype) => { return { workTypeId: worktype.custom_worktype_id, optionItemList: worktype.option_items.map((optionItem) => { const initialValueType = OPTION_ITEM_VALUE_TYPE_NUMBER.find( (x) => x.type === optionItem.default_value_type, )?.value; if (!initialValueType) { throw new Error( `invalid default_value_type ${optionItem.default_value_type}`, ); } return { label: optionItem.item_label, initialValueType, defaultValue: optionItem.initial_value, }; }), }; }); return { authorId: user.author_id ?? undefined, authorIdList: authorIds, workTypeList, isEncrypted: user.encryption, encryptionPassword: user.encryption_password ?? undefined, activeWorktype: activeWorktype?.custom_worktype_id ?? '', audioFormat: USER_AUDIO_FORMAT, prompt: user.prompt, }; } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case UserNotFoundError: throw new HttpException( makeErrorResponse('E010204'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.getRelations.name}`, ); } } /** * 指定したユーザーの情報を更新します * @param context * @param extarnalId * @param id * @param role * @param authorId * @param autoRenew * @param licenseAlart * @param notification * @param encryption * @param encryptionPassword * @param prompt * @returns user */ async updateUser( context: Context, extarnalId: string, id: number, role: string, authorId: string | undefined, autoRenew: boolean, licenseAlart: boolean, notification: boolean, encryption: boolean | undefined, encryptionPassword: string | undefined, prompt: boolean | undefined, ): Promise { try { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.updateUser.name } | params: { ` + `extarnalId: ${extarnalId}, ` + `id: ${id}, ` + `role: ${role}, ` + `authorId: ${authorId}, ` + `autoRenew: ${autoRenew}, ` + `licenseAlart: ${licenseAlart}, ` + `notification: ${notification}, ` + `encryption: ${encryption}, ` + `prompt: ${prompt} }`, ); // 実行ユーザーのアカウントIDを取得 const accountId = ( await this.usersRepository.findUserByExternalId(context, extarnalId) ).account_id; // ユーザー情報を更新 await this.usersRepository.update( context, accountId, id, role, authorId, autoRenew, licenseAlart, notification, encryption, encryptionPassword, prompt, ); // メール送信処理 try { const { adminEmails } = await this.getAccountInformation( context, accountId, ); // 変更ユーザー情報を取得 const { external_id: userExtarnalId } = await this.usersRepository.findUserById(context, id); const adb2cUser = await this.adB2cService.getUser( context, userExtarnalId, ); const { displayName: userName, emailAddress: userEmail } = getUserNameAndMailAddress(adb2cUser); if (userEmail === undefined) { throw new Error(`userEmail is null. externalId=${extarnalId}`); } // プライマリ管理者を取得 const { external_id: adminExternalId } = await this.getPrimaryAdminUser( context, accountId, ); const adb2cAdminUser = await this.adB2cService.getUser( context, adminExternalId, ); const { displayName: primaryAdminName } = getUserNameAndMailAddress(adb2cAdminUser); await this.sendgridService.sendMailWithU115( context, userName, userEmail, primaryAdminName, adminEmails, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); // メール送信に関する例外はログだけ出して握りつぶす } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case UserNotFoundError: throw new HttpException( makeErrorResponse('E010204'), HttpStatus.BAD_REQUEST, ); case AuthorIdAlreadyExistsError: throw new HttpException( makeErrorResponse('E010302'), HttpStatus.BAD_REQUEST, ); case InvalidRoleChangeError: throw new HttpException( makeErrorResponse('E010207'), HttpStatus.BAD_REQUEST, ); case EncryptionPasswordNeedError: throw new HttpException( makeErrorResponse('E010208'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.updateUser.name}`, ); } } /** * ライセンスをユーザーに割り当てます * @param context * @param userId * @param newLicenseId */ async allocateLicense( context: Context, userId: number, newLicenseId: number, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.allocateLicense.name } | params: { ` + `userId: ${userId}, ` + `newLicenseId: ${newLicenseId}, };`, ); try { const { external_id: externalId, account_id: accountId } = await this.usersRepository.findUserById(context, userId); await this.licensesRepository.allocateLicense( context, userId, newLicenseId, accountId, ); // メール送信処理 try { const { parent_account_id: dealerId } = await this.accountsRepository.findAccountById(context, accountId); if (dealerId == null) { throw new Error(`dealer is null. account_id=${accountId}`); } const { company_name: dealerName } = await this.accountsRepository.findAccountById(context, dealerId); const { companyName, adminEmails } = await this.getAccountInformation( context, accountId, ); const adb2cUser = await this.adB2cService.getUser(context, externalId); const { displayName, emailAddress } = getUserNameAndMailAddress(adb2cUser); if (emailAddress == null) { throw new Error(`emailAddress is null. externalId=${externalId}`); } // 管理者に割り当てた場合にはTOに管理者のメールアドレスを設定するので、CCには管理者のメールアドレスを設定しない const ccAdminEmails = adminEmails.filter((x) => x !== emailAddress); await this.sendgridService.sendMailWithU108( context, displayName, emailAddress, ccAdminEmails, companyName, dealerName, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); // メール送信に関する例外はログだけ出して握りつぶす } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case LicenseExpiredError: throw new HttpException( makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST, ); case LicenseUnavailableError: throw new HttpException( makeErrorResponse('E010806'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.allocateLicense.name}`, ); } } /** * ユーザーに割り当てられているライセンスを解除します * @param context * @param userId */ async deallocateLicense(context: Context, userId: number): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.deallocateLicense.name } | params: { ` + `userId: ${userId}, };`, ); try { const accountId = ( await this.usersRepository.findUserById(context, userId) ).account_id; await this.licensesRepository.deallocateLicense( context, userId, accountId, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case LicenseAlreadyDeallocatedError: throw new HttpException( makeErrorResponse('E010807'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.deallocateLicense.name}`, ); } } /** * 同意済み利用規約バージョンを更新する * @param context * @param idToken * @param eulaVersion * @param privacyNoticeVersion * @param dpaVersion */ async updateAcceptedVersion( context: Context, externalId: string, eulaVersion: string, privacyNoticeVersion: string, dpaVersion?: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.updateAcceptedVersion.name } | params: { ` + `externalId: ${externalId}, ` + `eulaVersion: ${eulaVersion}, ` + `privacyNoticeVersion: ${privacyNoticeVersion}, ` + `dpaVersion: ${dpaVersion}, };`, ); try { await this.usersRepository.updateAcceptedTermsVersion( context, externalId, eulaVersion, privacyNoticeVersion, dpaVersion, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case UserNotFoundError: throw new HttpException( makeErrorResponse('E010204'), HttpStatus.BAD_REQUEST, ); case AccountNotFoundError: throw new HttpException( makeErrorResponse('E010501'), HttpStatus.BAD_REQUEST, ); case UpdateTermsVersionNotSetError: throw new HttpException( makeErrorResponse('E010001'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.updateAcceptedVersion.name}`, ); } } /** * Azure AD B2Cからユーザー名を取得する * @param context * @param externalId */ async getUserName(context: Context, externalId: string): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.getUserName.name } | params: { externalId: ${externalId} };`, ); try { // extarnalIdの存在チェックを行う await this.usersRepository.findUserByExternalId(context, externalId); // ADB2Cからユーザー名を取得する const adb2cUser = await this.adB2cService.getUser(context, externalId); return adb2cUser.displayName; } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { case UserNotFoundError: throw new HttpException( makeErrorResponse('E010204'), HttpStatus.BAD_REQUEST, ); default: throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } } throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.getUserName.name}`, ); } } /** * アカウントIDを指定して、アカウント情報と管理者情報を取得する * @param context * @param accountId 対象アカウントID * @returns 企業名/管理者メールアドレス */ private async getAccountInformation( context: Context, accountId: number, ): Promise<{ companyName: string; adminEmails: string[]; }> { // アカウントIDから企業名を取得する const { company_name } = await this.accountsRepository.findAccountById( context, accountId, ); // 管理者一覧を取得 const admins = await this.usersRepository.findAdminUsers( context, accountId, ); const adminExternalIDs = admins.map((x) => x.external_id); // ADB2Cから管理者IDを元にメールアドレスを取得する const usersInfo = await this.adB2cService.getUsers( context, adminExternalIDs, ); // 生のAzure AD B2Cのユーザー情報からメールアドレスを抽出する const adminEmails = usersInfo.map((x) => { const { emailAddress } = getUserNameAndMailAddress(x); if (emailAddress == null) { throw new Error('dealer admin email-address is not found'); } return emailAddress; }); return { companyName: company_name, adminEmails: adminEmails, }; } /** * アカウントのプライマリ管理者を取得する * @param context * @param accountId * @returns primary admin user */ private async getPrimaryAdminUser( context: Context, accountId: number, ): Promise { const accountInfo = await this.accountsRepository.findAccountById( context, accountId, ); if (!accountInfo || !accountInfo.primary_admin_user_id) { throw new Error(`account or primary admin not found. id=${accountId}`); } const primaryAdmin = await this.usersRepository.findUserById( context, accountInfo.primary_admin_user_id, ); return primaryAdmin; } }