## 概要 [Task2295: [Sp20]既存APIのログを強化(外部連携API以外)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2295) - 何をどう変更したか、追加したライブラリなど 誰が操作したのかを追えるようにログを強化 ## レビューポイント - 特にレビューしてほしい箇所 特になし ## 動作確認状況 - ユニットテスト
1110 lines
33 KiB
TypeScript
1110 lines
33 KiB
TypeScript
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
|
import { isVerifyError, verify } from '../../common/jwt';
|
|
import { getPublicKey } from '../../common/jwt/jwt';
|
|
import { makePassword } from '../../common/password/password';
|
|
import {
|
|
SortDirection,
|
|
TaskListSortableAttribute,
|
|
isSortDirection,
|
|
isTaskListSortableAttribute,
|
|
} from '../../common/types/sort';
|
|
import {
|
|
AdB2cService,
|
|
ConflictError,
|
|
isConflictError,
|
|
} from '../../gateways/adb2c/adb2c.service';
|
|
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
|
import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service';
|
|
import {
|
|
User as EntityUser,
|
|
newUser,
|
|
} from '../../repositories/users/entity/user.entity';
|
|
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
|
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
|
|
import { GetRelationsResponse, User } from './types/types';
|
|
import {
|
|
AuthorIdAlreadyExistsError,
|
|
EmailAlreadyVerifiedError,
|
|
EncryptionPasswordNeedError,
|
|
InvalidRoleChangeError,
|
|
UpdateTermsVersionNotSetError,
|
|
UserNotFoundError,
|
|
} from '../../repositories/users/errors/types';
|
|
import {
|
|
ADB2C_SIGN_IN_TYPE,
|
|
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
|
MANUAL_RECOVERY_REQUIRED,
|
|
OPTION_ITEM_VALUE_TYPE_NUMBER,
|
|
USER_AUDIO_FORMAT,
|
|
USER_LICENSE_STATUS,
|
|
USER_ROLES,
|
|
} from '../../constants';
|
|
import { DateWithZeroTime } from '../licenses/types/types';
|
|
import { Context } from '../../common/log';
|
|
import { UserRoles } from '../../common/types/role';
|
|
import {
|
|
LicenseAlreadyDeallocatedError,
|
|
LicenseExpiredError,
|
|
LicenseUnavailableError,
|
|
} from '../../repositories/licenses/errors/types';
|
|
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
|
|
|
|
@Injectable()
|
|
export class UsersService {
|
|
private readonly logger = new Logger(UsersService.name);
|
|
private readonly mailFrom: string;
|
|
private readonly appDomain: string;
|
|
constructor(
|
|
private readonly usersRepository: UsersRepositoryService,
|
|
private readonly licensesRepository: LicensesRepositoryService,
|
|
private readonly sortCriteriaRepository: SortCriteriaRepositoryService,
|
|
private readonly adB2cService: AdB2cService,
|
|
private readonly configService: ConfigService,
|
|
private readonly sendgridService: SendGridService,
|
|
) {
|
|
this.mailFrom = this.configService.getOrThrow<string>('MAIL_FROM');
|
|
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
|
|
}
|
|
|
|
/**
|
|
* Confirms user
|
|
* @param token ユーザ仮登録時に払いだされるトークン
|
|
*/
|
|
async confirmUser(context: Context, token: string): Promise<void> {
|
|
this.logger.log(
|
|
`[IN] [${context.getTrackingId()}] ${this.confirmUser.name}`,
|
|
);
|
|
const pubKey = getPublicKey(this.configService);
|
|
|
|
const decodedToken = verify<{
|
|
accountId: number;
|
|
userId: number;
|
|
email: string;
|
|
}>(token, pubKey);
|
|
if (isVerifyError(decodedToken)) {
|
|
throw new HttpException(
|
|
makeErrorResponse('E000101'),
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
try {
|
|
// トランザクションで取得と更新をまとめる
|
|
const userId = decodedToken.userId;
|
|
await this.usersRepository.updateUserVerifiedAndCreateTrialLicense(
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates user
|
|
* @param accessToken
|
|
* @param name
|
|
* @param role
|
|
* @param email
|
|
* @param autoRenew
|
|
* @param licenseAlert
|
|
* @param notification
|
|
* @param [authorId]
|
|
* @param [encryption]
|
|
* @param [encryptionPassword]
|
|
* @param [prompt]
|
|
* @returns void
|
|
*/
|
|
async createUser(
|
|
context: Context,
|
|
externalId: string,
|
|
name: string,
|
|
role: UserRoles,
|
|
email: string,
|
|
autoRenew: boolean,
|
|
licenseAlert: boolean,
|
|
notification: boolean,
|
|
authorId?: string | undefined,
|
|
encryption?: boolean | undefined,
|
|
encryptionPassword?: string | undefined,
|
|
prompt?: boolean | undefined,
|
|
): Promise<void> {
|
|
this.logger.log(
|
|
`[IN] [${context.getTrackingId()}] ${this.createUser.name} | params: { ` +
|
|
`externalId: ${externalId}, ` +
|
|
`role: ${role}, ` +
|
|
`autoRenew: ${autoRenew}, ` +
|
|
`licenseAlert: ${licenseAlert}, ` +
|
|
`notification: ${notification}, ` +
|
|
`authorId: ${authorId}, ` +
|
|
`encryption: ${encryption}, ` +
|
|
`prompt: ${prompt} };`,
|
|
);
|
|
//DBよりアクセス者の所属するアカウントIDを取得する
|
|
let adminUser: EntityUser;
|
|
try {
|
|
adminUser = await this.usersRepository.findUserByExternalId(externalId);
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
|
|
const accountId = adminUser.account_id;
|
|
|
|
//authorIdが重複していないかチェックする
|
|
if (authorId) {
|
|
let isAuthorIdDuplicated = false;
|
|
try {
|
|
isAuthorIdDuplicated = await this.usersRepository.existsAuthorId(
|
|
accountId,
|
|
authorId,
|
|
);
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
if (isAuthorIdDuplicated) {
|
|
throw new HttpException(
|
|
makeErrorResponse('E010302'),
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ランダムなパスワードを生成する
|
|
const ramdomPassword = makePassword();
|
|
|
|
//Azure AD B2Cにユーザーを新規登録する
|
|
let externalUser: { sub: string } | ConflictError;
|
|
try {
|
|
// idpにユーザーを作成
|
|
externalUser = await this.adB2cService.createUser(
|
|
context,
|
|
email,
|
|
ramdomPassword,
|
|
name,
|
|
);
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
this.logger.error(
|
|
`[${context.getTrackingId()}] create externalUser failed`,
|
|
);
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
|
|
// メールアドレス重複エラー
|
|
if (isConflictError(externalUser)) {
|
|
throw new HttpException(
|
|
makeErrorResponse('E010301'),
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
//Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する
|
|
let newUser: EntityUser;
|
|
|
|
try {
|
|
//roleに応じてユーザー情報を作成する
|
|
const newUserInfo = this.createNewUserInfo(
|
|
context,
|
|
role,
|
|
accountId,
|
|
externalUser.sub,
|
|
autoRenew,
|
|
licenseAlert,
|
|
notification,
|
|
authorId,
|
|
encryption,
|
|
encryptionPassword,
|
|
prompt,
|
|
);
|
|
// ユーザ作成
|
|
newUser = await this.usersRepository.createNormalUser(newUserInfo);
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
this.logger.error(`[${context.getTrackingId()}]create user failed`);
|
|
//リカバリー処理
|
|
//Azure AD B2Cに登録したユーザー情報を削除する
|
|
await this.deleteB2cUser(externalUser.sub, context);
|
|
|
|
switch (e.code) {
|
|
case 'ER_DUP_ENTRY':
|
|
//AuthorID重複エラー
|
|
throw new HttpException(
|
|
makeErrorResponse('E010302'),
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
default:
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
|
|
//Email送信用のコンテンツを作成する
|
|
try {
|
|
// メールの内容を構成
|
|
const { subject, text, html } =
|
|
await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
|
|
context,
|
|
accountId,
|
|
newUser.id,
|
|
email,
|
|
);
|
|
|
|
//SendGridAPIを呼び出してメールを送信する
|
|
await this.sendgridService.sendMail(
|
|
context,
|
|
email,
|
|
this.mailFrom,
|
|
subject,
|
|
text,
|
|
html,
|
|
);
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
this.logger.error(`[${context.getTrackingId()}] create user failed`);
|
|
//リカバリー処理
|
|
//Azure AD B2Cに登録したユーザー情報を削除する
|
|
await this.deleteB2cUser(externalUser.sub, context);
|
|
// DBからユーザーを削除する
|
|
await this.deleteUser(newUser.id, context);
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
this.logger.log(
|
|
`[OUT] [${context.getTrackingId()}] ${this.createUser.name}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Azure AD B2Cに登録したユーザー情報を削除する
|
|
// TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
|
private async deleteB2cUser(externalUserId: string, context: Context) {
|
|
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}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// DBに登録したユーザー情報を削除する
|
|
private async deleteUser(userId: number, context: Context) {
|
|
try {
|
|
await this.usersRepository.deleteNormalUser(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}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// roleを受け取って、roleに応じたnewUserを作成して返却する
|
|
private createNewUserInfo(
|
|
context: Context,
|
|
role: UserRoles,
|
|
accountId: number,
|
|
externalId: string,
|
|
autoRenew: boolean,
|
|
licenseAlert: boolean,
|
|
notification: boolean,
|
|
authorId?: string | undefined,
|
|
encryption?: boolean | undefined,
|
|
encryptionPassword?: string | undefined,
|
|
prompt?: boolean | undefined,
|
|
): newUser {
|
|
this.logger.log(
|
|
`[IN] [${context.getTrackingId()}] ${
|
|
this.createNewUserInfo.name
|
|
} | params: { ` +
|
|
`role: ${role}, ` +
|
|
`accountId: ${accountId}, ` +
|
|
`authorId: ${authorId}, ` +
|
|
`externalId: ${externalId}, ` +
|
|
`autoRenew: ${autoRenew}, ` +
|
|
`licenseAlert: ${licenseAlert}, ` +
|
|
`notification: ${notification}, ` +
|
|
`authorId: ${authorId}, ` +
|
|
`encryption: ${encryption}, ` +
|
|
`prompt: ${prompt} };`,
|
|
);
|
|
switch (role) {
|
|
case USER_ROLES.NONE:
|
|
case USER_ROLES.TYPIST:
|
|
return {
|
|
account_id: accountId,
|
|
external_id: externalId,
|
|
auto_renew: autoRenew,
|
|
license_alert: licenseAlert,
|
|
notification,
|
|
role,
|
|
accepted_dpa_version: null,
|
|
accepted_eula_version: null,
|
|
encryption: false,
|
|
encryption_password: null,
|
|
prompt: false,
|
|
author_id: null,
|
|
};
|
|
case USER_ROLES.AUTHOR:
|
|
return {
|
|
account_id: accountId,
|
|
external_id: externalId,
|
|
auto_renew: autoRenew,
|
|
license_alert: licenseAlert,
|
|
notification,
|
|
role,
|
|
author_id: authorId ?? null,
|
|
encryption: encryption ?? false,
|
|
encryption_password: encryptionPassword ?? null,
|
|
prompt: prompt ?? false,
|
|
accepted_dpa_version: null,
|
|
accepted_eula_version: null,
|
|
};
|
|
default:
|
|
//不正なroleが指定された場合はログを出力してエラーを返す
|
|
this.logger.error(
|
|
`[${context.getTrackingId()}] [NOT IMPLEMENT] [RECOVER] role: ${role}`,
|
|
);
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* confirm User And Init Password
|
|
* @param token ユーザ仮登録時に払いだされるトークン
|
|
*/
|
|
async confirmUserAndInitPassword(
|
|
context: Context,
|
|
token: string,
|
|
): Promise<void> {
|
|
this.logger.log(
|
|
`[IN] [${context.getTrackingId()}] ${
|
|
this.confirmUserAndInitPassword.name
|
|
}`,
|
|
);
|
|
|
|
const pubKey = getPublicKey(this.configService);
|
|
|
|
const decodedToken = verify<{
|
|
accountId: number;
|
|
userId: number;
|
|
email: string;
|
|
}>(token, pubKey);
|
|
if (isVerifyError(decodedToken)) {
|
|
throw new HttpException(
|
|
makeErrorResponse('E000101'),
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
// ランダムなパスワードを生成する
|
|
const ramdomPassword = makePassword();
|
|
const { userId, email } = decodedToken;
|
|
|
|
try {
|
|
// ユーザー情報からAzure AD B2CのIDを特定する
|
|
const user = await this.usersRepository.findUserById(userId);
|
|
const extarnalId = user.external_id;
|
|
// パスワードを変更する
|
|
await this.adB2cService.changePassword(
|
|
context,
|
|
extarnalId,
|
|
ramdomPassword,
|
|
);
|
|
// ユーザを認証済みにする
|
|
await this.usersRepository.updateUserVerified(userId);
|
|
// TODO [Task2163] ODMS側が正式にメッセージを決めるまで仮のメール内容とする
|
|
const subject = 'A temporary password has been issued.';
|
|
const text = 'temporary password: ' + ramdomPassword;
|
|
const html = `<p>OMDS TOP PAGE URL.<p><a href="${this.appDomain}">${this.appDomain}</a><br>temporary password: ${ramdomPassword}`;
|
|
|
|
// メールを送信
|
|
await this.sendgridService.sendMail(
|
|
context,
|
|
email,
|
|
this.mailFrom,
|
|
subject,
|
|
text,
|
|
html,
|
|
);
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
if (e instanceof Error) {
|
|
switch (e.constructor) {
|
|
case EmailAlreadyVerifiedError:
|
|
throw new HttpException(
|
|
makeErrorResponse('E010202'),
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
default:
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
);
|
|
|
|
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
|
|
const externalIds = dbUsers.map((x) => x.external_id);
|
|
const trackingId = new Context(context.trackingId);
|
|
const adb2cUsers = await this.adB2cService.getUsers(
|
|
// TODO: 外部連携以外のログ強化時に、ContollerからContextを取得するように修正する
|
|
trackingId,
|
|
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,
|
|
);
|
|
|
|
// メールアドレスを取得する
|
|
const mail = adb2cUser?.identities?.find(
|
|
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
|
)?.issuerAssignedId;
|
|
|
|
//メールアドレスが取得できない場合はエラー
|
|
if (!mail) {
|
|
throw new Error('mail not found.');
|
|
}
|
|
|
|
let status = USER_LICENSE_STATUS.NORMAL;
|
|
|
|
// ライセンスの有効期限と残日数は、ライセンスが存在する場合のみ算出する
|
|
// ライセンスが存在しない場合は、undefinedのままとする
|
|
let expiration: string | undefined = undefined;
|
|
let remaining: number | undefined = undefined;
|
|
|
|
if (dbUser.license) {
|
|
// 有効期限日付 YYYY/MM/DD
|
|
const expiry_date = dbUser.license.expiry_date ?? undefined;
|
|
expiration =
|
|
expiry_date !== undefined
|
|
? `${expiry_date.getFullYear()}/${
|
|
expiry_date.getMonth() + 1
|
|
}/${expiry_date.getDate()}`
|
|
: undefined;
|
|
|
|
const currentDate = new DateWithZeroTime();
|
|
// 有効期限までの日数
|
|
remaining =
|
|
expiry_date !== undefined
|
|
? Math.floor(
|
|
(expiry_date.getTime() - currentDate.getTime()) /
|
|
(1000 * 60 * 60 * 24),
|
|
)
|
|
: undefined;
|
|
if (
|
|
remaining !== undefined &&
|
|
remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS
|
|
) {
|
|
status = dbUser.auto_renew
|
|
? USER_LICENSE_STATUS.RENEW
|
|
: USER_LICENSE_STATUS.ALERT;
|
|
}
|
|
} else {
|
|
status = USER_LICENSE_STATUS.NO_LICENSE;
|
|
}
|
|
|
|
return {
|
|
id: dbUser.id,
|
|
name: adb2cUser.displayName,
|
|
role: dbUser.role,
|
|
authorId: dbUser.author_id ?? undefined,
|
|
typistGroupName: groupNames,
|
|
email: mail,
|
|
emailVerified: dbUser.email_verified,
|
|
autoRenew: dbUser.auto_renew,
|
|
licenseAlert: dbUser.license_alert,
|
|
notification: dbUser.notification,
|
|
encryption: dbUser.encryption,
|
|
prompt: dbUser.prompt,
|
|
expiration: expiration,
|
|
remaining: remaining,
|
|
licenseStatus: status,
|
|
};
|
|
});
|
|
|
|
return users;
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.NOT_FOUND,
|
|
);
|
|
} finally {
|
|
this.logger.log(
|
|
`[OUT] [${context.getTrackingId()}] ${this.getUsers.name}`,
|
|
);
|
|
}
|
|
}
|
|
/**
|
|
* Updates sort criteria
|
|
* @param paramName
|
|
* @param direction
|
|
* @param token
|
|
* @returns sort criteria
|
|
*/
|
|
async updateSortCriteria(
|
|
context: Context,
|
|
paramName: TaskListSortableAttribute,
|
|
direction: SortDirection,
|
|
externalId: string,
|
|
): Promise<void> {
|
|
this.logger.log(
|
|
`[IN] [${context.getTrackingId()}] ${
|
|
this.updateSortCriteria.name
|
|
} | params: { paramName: ${paramName}, direction: ${direction}, externalId: ${externalId} };`,
|
|
);
|
|
let user: EntityUser;
|
|
try {
|
|
// ユーザー情報を取得
|
|
user = await this.usersRepository.findUserByExternalId(externalId);
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
|
|
try {
|
|
// ユーザーのソート条件を更新
|
|
await this.sortCriteriaRepository.updateSortCriteria(
|
|
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(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(
|
|
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(userId);
|
|
|
|
// ユーザー関連情報を取得
|
|
const { user, authors, worktypes, activeWorktype } =
|
|
await this.usersRepository.getUserRelations(id);
|
|
|
|
// AuthorIDのリストを作成
|
|
const authorIds = authors.flatMap((author) =>
|
|
author.author_id ? [author.author_id] : [],
|
|
);
|
|
|
|
const workTypeList = worktypes?.map((worktype) => {
|
|
return {
|
|
workTypeId: worktype.custom_worktype_id,
|
|
optionItemList: worktype.option_items.map((optionItem) => {
|
|
const initialValueType = OPTION_ITEM_VALUE_TYPE_NUMBER.find(
|
|
(x) => x.type === optionItem.default_value_type,
|
|
)?.value;
|
|
|
|
if (!initialValueType) {
|
|
throw new Error(
|
|
`invalid default_value_type ${optionItem.default_value_type}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
label: optionItem.item_label,
|
|
initialValueType,
|
|
defaultValue: optionItem.initial_value,
|
|
};
|
|
}),
|
|
};
|
|
});
|
|
|
|
return {
|
|
authorId: user.author_id ?? undefined,
|
|
authorIdList: authorIds,
|
|
workTypeList,
|
|
isEncrypted: user.encryption,
|
|
encryptionPassword: user.encryption_password ?? undefined,
|
|
activeWorktype: activeWorktype?.custom_worktype_id ?? '',
|
|
audioFormat: USER_AUDIO_FORMAT,
|
|
prompt: user.prompt,
|
|
};
|
|
} catch (e) {
|
|
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
|
|
|
if (e instanceof Error) {
|
|
switch (e.constructor) {
|
|
case UserNotFoundError:
|
|
throw new HttpException(
|
|
makeErrorResponse('E010204'),
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
default:
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
|
|
throw new HttpException(
|
|
makeErrorResponse('E009999'),
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
} finally {
|
|
this.logger.log(
|
|
`[OUT] [${context.getTrackingId()}] ${this.getRelations.name}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 指定したユーザーの情報を更新します
|
|
* @param context
|
|
* @param extarnalId
|
|
* @param id
|
|
* @param role
|
|
* @param authorId
|
|
* @param autoRenew
|
|
* @param licenseAlart
|
|
* @param notification
|
|
* @param encryption
|
|
* @param encryptionPassword
|
|
* @param prompt
|
|
* @returns user
|
|
*/
|
|
async updateUser(
|
|
context: Context,
|
|
extarnalId: string,
|
|
id: number,
|
|
role: string,
|
|
authorId: string | undefined,
|
|
autoRenew: boolean,
|
|
licenseAlart: boolean,
|
|
notification: boolean,
|
|
encryption: boolean | undefined,
|
|
encryptionPassword: string | undefined,
|
|
prompt: boolean | undefined,
|
|
): Promise<void> {
|
|
try {
|
|
this.logger.log(
|
|
`[IN] [${context.getTrackingId()}] ${
|
|
this.updateUser.name
|
|
} | params: { ` +
|
|
`extarnalId: ${extarnalId}, ` +
|
|
`id: ${id}, ` +
|
|
`role: ${role}, ` +
|
|
`authorId: ${authorId}, ` +
|
|
`autoRenew: ${autoRenew}, ` +
|
|
`licenseAlart: ${licenseAlart}, ` +
|
|
`notification: ${notification}, ` +
|
|
`encryption: ${encryption}, ` +
|
|
`prompt: ${prompt} }`,
|
|
);
|
|
|
|
// 実行ユーザーのアカウントIDを取得
|
|
const accountId = (
|
|
await this.usersRepository.findUserByExternalId(extarnalId)
|
|
).account_id;
|
|
|
|
// ユーザー情報を更新
|
|
await this.usersRepository.update(
|
|
accountId,
|
|
id,
|
|
role,
|
|
authorId,
|
|
autoRenew,
|
|
licenseAlart,
|
|
notification,
|
|
encryption,
|
|
encryptionPassword,
|
|
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,
|
|
);
|
|
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 accountId = (await this.usersRepository.findUserById(userId))
|
|
.account_id;
|
|
|
|
await this.licensesRepository.allocateLicense(
|
|
userId,
|
|
newLicenseId,
|
|
accountId,
|
|
);
|
|
} 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(userId))
|
|
.account_id;
|
|
|
|
await this.licensesRepository.deallocateLicense(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 dpaVersion
|
|
*/
|
|
async updateAcceptedVersion(
|
|
context: Context,
|
|
externalId: string,
|
|
eulaVersion: string,
|
|
dpaVersion?: string,
|
|
): Promise<void> {
|
|
this.logger.log(
|
|
`[IN] [${context.getTrackingId()}] ${
|
|
this.updateAcceptedVersion.name
|
|
} | params: { ` +
|
|
`externalId: ${externalId}, ` +
|
|
`eulaVersion: ${eulaVersion}, ` +
|
|
`dpaVersion: ${dpaVersion}, };`,
|
|
);
|
|
|
|
try {
|
|
await this.usersRepository.updateAcceptedTermsVersion(
|
|
externalId,
|
|
eulaVersion,
|
|
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(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}`,
|
|
);
|
|
}
|
|
}
|
|
}
|