saito.k 0e68f26c57 Merged PR 906: ユーザー認証API修正
## 概要
[Task4182: ユーザー認証API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4182)

- 認証済みチェックをパスワード変更より先に行うように修正
- パスワード変更に失敗したら、認証済みフラグをfalseにするリカバリ処理追加
  - リカバリに失敗したら手動復旧ログを出力
- メール送信に失敗したらエラーを返すように修正
  - メール送信に失敗したらリカバリ処理を行うように修正
    - リカバリに失敗したら手動復旧ログを出力
- テスト修正
  - リカバリ処理を考慮したケースを追加

## レビューポイント
- リカバリ処理の記述
- メール送信でエラーが起きたときにエラーを握りつぶさないようにしたが問題ないか
  - メール送信で失敗したときにエラーを握りつぶすと、ユーザーは届かないメールを待つしかなくなる
    - 失敗を伝えて、リカバリをしてあげると再実行してもらうことができる。

## クエリの変更
- クエリの変更はなし

## 動作確認状況
- ローカルで確認
- 行った修正がデグレを発生させていないことを確認できるか
  - 既存のテストケースをDBを使うテストに置き換え
    - 結果は変えずに通ることを確認
  - テストケースを追加し、新たな観点でテストを作成

## 補足
- 相談、参考資料などがあれば
2024-05-30 00:18:59 +00:00

1847 lines
56 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
MultipleImportUser,
GetRelationsResponse,
MultipleImportErrors,
User,
} from './types/types';
import {
AdminDeleteFailedError,
AssignedWorkflowWithAuthorDeleteFailedError,
AssignedWorkflowWithTypistDeleteFailedError,
AuthorIdAlreadyExistsError,
EmailAlreadyVerifiedError,
EncryptionPasswordNeedError,
ExistsCheckoutPermissionDeleteFailedError,
ExistsGroupMemberDeleteFailedError,
ExistsValidLicenseDeleteFailedError,
ExistsValidTaskDeleteFailedError,
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_EXPIRY_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 { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
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,
private readonly blobStorageService: BlobstorageService,
) {}
/**
* 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 context
* @param externalId
* @param name
* @param role
* @param email
* @param autoRenew
* @param notification
* @param [authorId]
* @param [encryption]
* @param [encryptionPassword]
* @param [prompt]
* @returns user
*/
async createUser(
context: Context,
externalId: string,
name: string,
role: UserRoles,
email: string,
autoRenew: 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}, ` +
`notification: ${notification}, ` +
`authorId: ${authorId}, ` +
`encryption: ${encryption}, ` +
`prompt: ${prompt} };`,
);
//DBよりアクセス者の所属するアカウントIDを取得する
let adminUser: EntityUser;
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;
const 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,
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.internalDeleteB2cUser(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.internalDeleteB2cUser(externalUser.sub, context);
// DBからユーザーを削除する
await this.internalDeleteUser(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 internalDeleteB2cUser(
externalUserId: string,
context: Context,
) {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.internalDeleteB2cUser.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.internalDeleteB2cUser.name}`,
);
}
}
// DBに登録したユーザー情報を削除する
private async internalDeleteUser(userId: number, context: Context) {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.internalDeleteUser.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.internalDeleteUser.name}`,
);
}
}
// roleを受け取って、roleに応じたnewUserを作成して返却する
private createNewUserInfo(
context: Context,
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: { ` +
`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 {
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 {
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}`,
);
}
}
/**
* 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 { accountId, userId, email } = decodedToken;
try {
// ユーザを認証済みにする
await this.usersRepository.updateUserVerified(context, userId);
} 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,
);
}
}
}
// ランダムなパスワードを生成する
const ramdomPassword = makePassword();
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,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] change password failed. userId=${userId}`,
);
// リカバリー処理
// ユーザを未認証に戻す
await this.updateUserUnverified(context, userId);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メール送信処理
try {
const { external_id: primaryAdminUserExternalId } =
await this.getPrimaryAdminUser(context, accountId);
const adb2cUser = await this.adB2cService.getUser(
context,
primaryAdminUserExternalId,
);
const { displayName: primaryAdminName } =
getUserNameAndMailAddress(adb2cUser);
await this.sendgridService.sendMailWithU113(
context,
email,
primaryAdminName,
ramdomPassword,
);
} catch (e) {
// リカバリー処理
// ユーザーを未認証に戻す
await this.updateUserUnverified(context, userId);
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_EXPIRY_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_EXPIRY_STATUS.RENEW
: USER_LICENSE_EXPIRY_STATUS.ALERT;
}
} else {
status = USER_LICENSE_EXPIRY_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,
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}`,
);
}
}
/**
* Updates user
* @param context
* @param extarnalId
* @param id
* @param role
* @param authorId
* @param autoRenew
* @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,
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}, ` +
`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,
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);
let dealerName: string | null = null;
if (dealerId !== null) {
const { company_name } =
await this.accountsRepository.findAccountById(context, dealerId);
dealerName = company_name;
}
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}`);
}
await this.sendgridService.sendMailWithU108(
context,
displayName,
emailAddress,
adminEmails,
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}`,
);
}
}
/**
* ユーザーを削除する
* @param context
* @param extarnalId
* @param currentTime ライセンス有効期限のチェックに使用する現在時刻
* @returns user
*/
async deleteUser(
context: Context,
userId: number,
currentTime: Date,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteUser.name
} | params: { userId: ${userId} };`,
);
try {
// 削除対象のユーザーが存在するかを確認する
let user: EntityUser;
try {
user = await this.usersRepository.findUserById(context, userId);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E014001'),
HttpStatus.BAD_REQUEST,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// 削除対象のユーザーが管理者かどうかを確認する
const admins = await this.usersRepository.findAdminUsers(
context,
user.account_id,
);
const adminIds = admins.map((admin) => admin.id);
if (adminIds.includes(user.id)) {
throw new HttpException(
makeErrorResponse('E014002'),
HttpStatus.BAD_REQUEST,
);
}
// Azure AD B2Cからユーザー情報(e-mail, name)を取得する
const adminExternalIds = admins.map((admin) => admin.external_id);
const externalIds = [user.external_id, ...adminExternalIds];
// Azure AD B2CのRateLimit対策のため、ユーザー情報を一括取得する
const details = await this.adB2cService.getUsers(context, externalIds);
// 削除対象のユーザーがAzure AD B2Cに存在するかを確認する
const deleteTargetDetail = details.find(
(details) => details.id === user.external_id,
);
if (deleteTargetDetail == null) {
throw new HttpException(
makeErrorResponse('E014001'),
HttpStatus.BAD_REQUEST,
);
}
// 管理者の情報が0件(=競合でアカウントが削除された場合等)の場合はエラーを返す
const adminDetails = details.filter((details) =>
adminExternalIds.includes(details.id),
);
if (adminDetails.length === 0) {
// 通常ユーザーが取得できていて管理者が取得できない事は通常ありえないため、汎用エラー
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const { emailAddress } = getUserNameAndMailAddress(deleteTargetDetail);
// メールアドレスが設定されていない場合はエラーを返す
if (emailAddress == null) {
throw new Error(`emailAddress is null. externalId=${user.external_id}`);
}
// 管理者のメールアドレスを取得
const { adminEmails } = await this.getAccountInformation(
context,
user.account_id,
);
// プライマリ管理者を取得
const { external_id: adminExternalId } = await this.getPrimaryAdminUser(
context,
user.account_id,
);
const adb2cAdminUser = await this.adB2cService.getUser(
context,
adminExternalId,
);
const { displayName: primaryAdminName } =
getUserNameAndMailAddress(adb2cAdminUser);
let isSuccess = false;
try {
const result = await this.usersRepository.deleteUser(
context,
userId,
currentTime,
);
isSuccess = result.isSuccess;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
switch (e.constructor) {
case AdminDeleteFailedError:
throw new HttpException(
makeErrorResponse('E014002'),
HttpStatus.BAD_REQUEST,
);
case AssignedWorkflowWithAuthorDeleteFailedError:
throw new HttpException(
makeErrorResponse('E014003'),
HttpStatus.BAD_REQUEST,
);
case AssignedWorkflowWithTypistDeleteFailedError:
throw new HttpException(
makeErrorResponse('E014004'),
HttpStatus.BAD_REQUEST,
);
case ExistsGroupMemberDeleteFailedError:
throw new HttpException(
makeErrorResponse('E014005'),
HttpStatus.BAD_REQUEST,
);
case ExistsValidTaskDeleteFailedError:
throw new HttpException(
makeErrorResponse('E014006'),
HttpStatus.BAD_REQUEST,
);
case ExistsValidLicenseDeleteFailedError:
throw new HttpException(
makeErrorResponse('E014007'),
HttpStatus.BAD_REQUEST,
);
case ExistsCheckoutPermissionDeleteFailedError:
throw new HttpException(
makeErrorResponse('E014009'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
// トランザクションレベルで厳密に削除が成功したかを判定する
if (!isSuccess) {
// 既に削除されている場合はエラーを返す
throw new HttpException(
makeErrorResponse('E014001'),
HttpStatus.BAD_REQUEST,
);
}
try {
// 削除を実施したことが確定したので、Azure AD B2Cからユーザーを削除する
await this.adB2cService.deleteUser(user.external_id, context);
this.logger.log(
`[${context.getTrackingId()}] delete externalUser: ${
user.external_id
} | params: { ` + `externalUserId: ${user.external_id}, };`,
);
} catch (error) {
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${
user.external_id
}`,
);
}
// 削除を実施したことが確定したので、メール送信処理を実施する
try {
await this.sendgridService.sendMailWithU116(
context,
deleteTargetDetail.displayName,
emailAddress,
primaryAdminName,
adminEmails,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`,
);
}
}
/**
* ユーザー一括登録完了メールを送信する
* @param context
* @param accountId
* @param fileName
* @param requestTime
* @param errors
* @returns imports complate
*/
async multipleImportsComplate(
context: Context,
accountId: number,
fileName: string,
requestTime: number,
errors: MultipleImportErrors[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.multipleImportsComplate.name
} | params: { accountId: ${accountId}, fileName: ${fileName}, requestTime: ${requestTime} };`,
);
try {
const account = await this.accountsRepository.findAccountById(
context,
accountId,
);
if (!account) {
throw new Error(`account not found. id=${accountId}`);
}
const dealerId = account.parent_account_id;
let dealerName: string | null = null;
if (dealerId !== null) {
const { company_name } = await this.accountsRepository.findAccountById(
context,
dealerId,
);
dealerName = company_name;
}
// アカウント情報を取得
const { companyName, adminEmails } = await this.getAccountInformation(
context,
accountId,
);
if (errors.length === 0) {
const requestTimeDate = new Date(requestTime * 1000);
// 完了メールを通知する
await this.sendgridService.sendMailWithU121(
context,
adminEmails,
companyName,
dealerName,
`${requestTimeDate.getFullYear()}.${
requestTimeDate.getMonth() + 1
}.${requestTimeDate.getDate()}`,
fileName,
);
} else {
const duplicateEmails: number[] = [];
const duplicateAuthorIds: number[] = [];
const otherErrors: number[] = [];
// エラーを仕分ける
for (const error of errors) {
switch (error.errorCode) {
// メールアドレス重複エラー
case 'E010301':
duplicateEmails.push(error.line);
break;
// AuthorID重複エラー
case 'E010302':
duplicateAuthorIds.push(error.line);
break;
// その他エラー
default:
otherErrors.push(error.line);
break;
}
}
// エラーメールを通知する
await this.sendgridService.sendMailWithU122(
context,
adminEmails,
companyName,
dealerName,
duplicateEmails,
duplicateAuthorIds,
otherErrors,
);
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
this.multipleImportsComplate.name
}`,
);
}
}
/**
* ユーザー一括登録用のファイルをBlobにアップロードする
* @param context
* @param externalId
* @param fileName
* @param users
* @returns imports
*/
async multipleImports(
context: Context,
externalId: string,
fileName: string,
users: MultipleImportUser[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.multipleImports.name
} | params: { externalId: ${externalId}, ` +
`fileName: ${fileName}, ` +
`users.length: ${users.length} };`,
);
try {
// ユーザー情報を取得
const user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
if (user == null) {
throw new Error(`user not found. externalId=${externalId}`);
}
const now = new Date();
// 日時を生成(YYYYMMDD_HHMMSS)
const dateTime =
`${now.getFullYear().toString().padStart(4, '0')}` +
`${(now.getMonth() + 1).toString().padStart(2, '0')}` + // 月は0から始まるため+1する
`${now.getDate().toString().padStart(2, '0')}` +
`_${now.getHours().toString().padStart(2, '0')}` +
`${now.getMinutes().toString().padStart(2, '0')}` +
`${now.getSeconds().toString().padStart(2, '0')}`;
// ファイル名を生成(U_YYYYMMDD_HHMMSS_アカウントID_ユーザーID.json)
const jsonFileName = `U_${dateTime}_${user.account_id}_${user.id}.json`;
// ユーザー情報をJSON形式に変換
const usersJson = JSON.stringify({
account_id: user.account_id,
user_id: user.id,
user_role: user.role,
external_id: user.external_id,
file_name: fileName,
date: Math.floor(now.getTime() / 1000),
data: users.map((user) => {
return {
name: user.name,
email: user.email,
role: user.role,
author_id: user.authorId,
auto_renew: user.autoRenew,
notification: user.notification,
encryption: user.encryption,
encryption_password: user.encryptionPassword,
prompt: user.prompt,
};
}),
});
// Blobにファイルをアップロードユーザー一括登録用
await this.blobStorageService.uploadImportsBlob(
context,
jsonFileName,
usersJson,
);
// 受付完了メールを送信
try {
// アカウント・管理者情報を取得
const { adminEmails, companyName } = await this.getAccountInformation(
context,
user.account_id,
);
// Dealer情報を取得
const dealer = await this.accountsRepository.findParentAccount(
context,
user.account_id,
);
const dealerName = dealer?.company_name ?? null;
const now = new Date();
// 日時を生成(YYYY.MM.DD)
const date = `${now.getFullYear()}.${
now.getMonth() + 1 // 月は0から始まるため+1する
}.${now.getDate()}`;
await this.sendgridService.sendMailWithU120(
context,
adminEmails,
companyName,
dealerName,
date,
fileName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.multipleImports.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;
}
/**
* ユーザーを未認証にする
* @param context
* @param userId
* @returns void
*/
private async updateUserUnverified(
context: Context,
userId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateUserUnverified.name
} | params: { userId: ${userId} };`,
);
try {
await this.usersRepository.updateUserUnverified(context, userId);
this.logger.log(
`[${context.getTrackingId()}] update user unverified: ${userId}`,
);
} catch (error) {
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to update user unverified: ${userId}`,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateUserUnverified.name}`,
);
}
}
}