## 概要 [Task3311: ユーザー情報変更完了通知 [U-115] の実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3311) - ユーザー情報変更時のメール通知を実装しました。 ## レビューポイント - メールの送信先に不自然な点はないでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認
1369 lines
41 KiB
TypeScript
1369 lines
41 KiB
TypeScript
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<string>('MAIL_FROM');
|
|
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
|
|
}
|
|
|
|
/**
|
|
* Confirms user
|
|
* @param token ユーザ仮登録時に払いだされるトークン
|
|
*/
|
|
async confirmUser(context: Context, token: string): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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 = `<p>OMDS TOP PAGE URL.<p><a href="${this.appDomain}">${this.appDomain}</a><br>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<User[]> {
|
|
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<void> {
|
|
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<GetRelationsResponse> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string> {
|
|
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<EntityUser> {
|
|
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;
|
|
}
|
|
}
|