## 概要 [Task4182: ユーザー認証API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4182) - 認証済みチェックをパスワード変更より先に行うように修正 - パスワード変更に失敗したら、認証済みフラグをfalseにするリカバリ処理追加 - リカバリに失敗したら手動復旧ログを出力 - メール送信に失敗したらエラーを返すように修正 - メール送信に失敗したらリカバリ処理を行うように修正 - リカバリに失敗したら手動復旧ログを出力 - テスト修正 - リカバリ処理を考慮したケースを追加 ## レビューポイント - リカバリ処理の記述 - メール送信でエラーが起きたときにエラーを握りつぶさないようにしたが問題ないか - メール送信で失敗したときにエラーを握りつぶすと、ユーザーは届かないメールを待つしかなくなる - 失敗を伝えて、リカバリをしてあげると再実行してもらうことができる。 ## クエリの変更 - クエリの変更はなし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 既存のテストケースをDBを使うテストに置き換え - 結果は変えずに通ることを確認 - テストケースを追加し、新たな観点でテストを作成 ## 補足 - 相談、参考資料などがあれば
1847 lines
56 KiB
TypeScript
1847 lines
56 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 {
|
||
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}`,
|
||
);
|
||
}
|
||
}
|
||
}
|