## 概要 [Task3594: API実装(ユーザー削除|Repository以外)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3594) ユーザー削除API実装 ユニットテスト実装 ## レビューポイント - `'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった)`が用意されているが、 `'E014002', // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)`とわけて実装する必要あるか。 管理者でしか削除処理は行えない&管理者ユーザは削除できない。 - `ExistsCheckoutPermissionDeleteFailedError` 削除対象ユーザーがチェックアウト権限を持っている事が原因の削除失敗エラーは、ユーザ削除エラーの一つとして、`code.ts`にコードを用意してあげる必要があるか? (引継ぎ時あえて用意していないように見えなくもなかったので) ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば
1181 lines
38 KiB
TypeScript
1181 lines
38 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
||
import { User, UserArchive, newUser } from './entity/user.entity';
|
||
import {
|
||
DataSource,
|
||
FindOptionsWhere,
|
||
In,
|
||
IsNull,
|
||
Not,
|
||
UpdateResult,
|
||
} from 'typeorm';
|
||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||
import {
|
||
getDirection,
|
||
getTaskListSortableAttribute,
|
||
} from '../../common/types/sort/util';
|
||
import {
|
||
UserNotFoundError,
|
||
EmailAlreadyVerifiedError,
|
||
AuthorIdAlreadyExistsError,
|
||
InvalidRoleChangeError,
|
||
EncryptionPasswordNeedError,
|
||
TermInfoNotFoundError,
|
||
UpdateTermsVersionNotSetError,
|
||
DelegationNotAllowedError,
|
||
ExistsGroupMemberDeleteFailedError,
|
||
AssignedWorkflowWithTypistDeleteFailedError,
|
||
AssignedWorkflowWithAuthorDeleteFailedError,
|
||
AdminDeleteFailedError,
|
||
ExistsValidTaskDeleteFailedError,
|
||
ExistsCheckoutPermissionDeleteFailedError,
|
||
ExistsValidLicenseDeleteFailedError,
|
||
} from './errors/types';
|
||
import {
|
||
LICENSE_ALLOCATED_STATUS,
|
||
LICENSE_TYPE,
|
||
SWITCH_FROM_TYPE,
|
||
TASK_STATUS,
|
||
TERM_TYPE,
|
||
TIERS,
|
||
TRIAL_LICENSE_ISSUE_NUM,
|
||
USER_ROLES,
|
||
USER_ROLE_ORDERS,
|
||
} from '../../constants';
|
||
import {
|
||
License,
|
||
LicenseAllocationHistory,
|
||
} from '../licenses/entity/license.entity';
|
||
import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types';
|
||
import { Term } from '../terms/entity/term.entity';
|
||
import { TermsCheckInfo } from '../../features/auth/types/types';
|
||
import {
|
||
AccountNotFoundError,
|
||
AdminUserNotFoundError,
|
||
} from '../accounts/errors/types';
|
||
import { Account } from '../accounts/entity/account.entity';
|
||
import { Workflow } from '../workflows/entity/workflow.entity';
|
||
import { Worktype } from '../worktypes/entity/worktype.entity';
|
||
import { Context } from '../../common/log';
|
||
import {
|
||
insertEntity,
|
||
insertEntities,
|
||
updateEntity,
|
||
deleteEntity,
|
||
} from '../../common/repository';
|
||
import { UserGroup } from '../user_groups/entity/user_group.entity';
|
||
import { Task } from '../tasks/entity/task.entity';
|
||
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
|
||
import { WorkflowTypist } from '../workflows/entity/workflow_typists.entity';
|
||
|
||
@Injectable()
|
||
export class UsersRepositoryService {
|
||
// クエリログにコメントを出力するかどうか
|
||
private readonly isCommentOut = process.env.STAGE !== 'local';
|
||
|
||
constructor(private dataSource: DataSource) {}
|
||
|
||
/**
|
||
* 一般ユーザーを作成する
|
||
* @param user
|
||
* @returns User
|
||
*/
|
||
async createNormalUser(context: Context, user: newUser): Promise<User> {
|
||
const {
|
||
account_id: accountId,
|
||
external_id: externalUserId,
|
||
role,
|
||
auto_renew,
|
||
notification,
|
||
author_id,
|
||
accepted_eula_version,
|
||
accepted_dpa_version,
|
||
encryption,
|
||
encryption_password: encryptionPassword,
|
||
prompt,
|
||
} = user;
|
||
const userEntity = new User();
|
||
|
||
userEntity.role = role;
|
||
userEntity.account_id = accountId;
|
||
userEntity.external_id = externalUserId;
|
||
userEntity.auto_renew = auto_renew;
|
||
userEntity.notification = notification;
|
||
userEntity.author_id = author_id;
|
||
userEntity.accepted_eula_version = accepted_eula_version;
|
||
userEntity.accepted_dpa_version = accepted_dpa_version;
|
||
userEntity.encryption = encryption;
|
||
userEntity.encryption_password = encryptionPassword;
|
||
userEntity.prompt = prompt;
|
||
|
||
const createdEntity = await this.dataSource.transaction(
|
||
async (entityManager) => {
|
||
const repo = entityManager.getRepository(User);
|
||
const newUser = repo.create(userEntity);
|
||
const persisted = await insertEntity(
|
||
User,
|
||
repo,
|
||
newUser,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// ユーザーのタスクソート条件を作成
|
||
const sortCriteria = new SortCriteria();
|
||
{
|
||
sortCriteria.parameter = getTaskListSortableAttribute('JOB_NUMBER');
|
||
sortCriteria.direction = getDirection('ASC');
|
||
sortCriteria.user_id = persisted.id;
|
||
}
|
||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||
const newSortCriteria = sortCriteriaRepo.create(sortCriteria);
|
||
await insertEntity(
|
||
SortCriteria,
|
||
sortCriteriaRepo,
|
||
newSortCriteria,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
return persisted;
|
||
},
|
||
);
|
||
return createdEntity;
|
||
}
|
||
|
||
async findVerifiedUser(
|
||
context: Context,
|
||
sub: string,
|
||
): Promise<User | undefined> {
|
||
const user = await this.dataSource.getRepository(User).findOne({
|
||
where: {
|
||
external_id: sub,
|
||
email_verified: true,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!user) {
|
||
return undefined;
|
||
}
|
||
return user;
|
||
}
|
||
|
||
async findUserByExternalId(context: Context, sub: string): Promise<User> {
|
||
const user = await this.dataSource.getRepository(User).findOne({
|
||
where: {
|
||
external_id: sub,
|
||
},
|
||
relations: {
|
||
account: true,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!user) {
|
||
throw new UserNotFoundError(`User not found. externalId: ${sub}`);
|
||
}
|
||
return user;
|
||
}
|
||
|
||
async findUserById(context: Context, id: number): Promise<User> {
|
||
const user = await this.dataSource.getRepository(User).findOne({
|
||
where: {
|
||
id: id,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!user) {
|
||
throw new UserNotFoundError(`User not Found.`);
|
||
}
|
||
return user;
|
||
}
|
||
|
||
/**
|
||
* AuthorIDをもとにユーザーを取得します。
|
||
* AuthorIDがセットされていない場合や、ユーザーが存在しない場合はエラーを返します。
|
||
* @param context
|
||
* @param authorId 検索対象のAuthorID
|
||
* @param accountId 検索対象のアカウントID
|
||
* @returns user by author id
|
||
*/
|
||
async findUserByAuthorId(
|
||
context: Context,
|
||
authorId: string,
|
||
accountId: number,
|
||
): Promise<User> {
|
||
if (!authorId) {
|
||
throw new Error('authorId is not set.');
|
||
}
|
||
|
||
const user = await this.dataSource.getRepository(User).findOne({
|
||
where: {
|
||
author_id: authorId,
|
||
account_id: accountId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!user) {
|
||
throw new UserNotFoundError(`User not Found.`);
|
||
}
|
||
return user;
|
||
}
|
||
|
||
/**
|
||
* 指定したアカウントIDを持つアカウントのプライマリ管理者とセカンダリ管理者を取得する
|
||
* @param context context
|
||
* @param accountId アカウントID
|
||
* @throws AccountNotFoundError
|
||
* @returns admin users
|
||
*/
|
||
async findAdminUsers(context: Context, accountId: number): Promise<User[]> {
|
||
return this.dataSource.transaction(async (entityManager) => {
|
||
const account = await entityManager.getRepository(Account).findOne({
|
||
where: {
|
||
id: accountId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
if (account == null) {
|
||
throw new AccountNotFoundError('account not found');
|
||
}
|
||
const primaryAdminId = account.primary_admin_user_id;
|
||
const secondaryAdminId = account.secondary_admin_user_id;
|
||
|
||
// IDが有効なユーザーだけを検索対象とする
|
||
const targets = [primaryAdminId, secondaryAdminId]
|
||
.flatMap((x) => (x == null ? [] : [x]))
|
||
.map((x): FindOptionsWhere<User> => ({ id: x }));
|
||
|
||
const users = await entityManager.getRepository(User).find({
|
||
where: targets,
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
return users;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* AuthorIdが既に存在するか確認する
|
||
* @param user
|
||
* @returns 存在する:true 存在しない:false
|
||
*/
|
||
async existsAuthorId(
|
||
context: Context,
|
||
accountId: number,
|
||
authorId: string,
|
||
): Promise<boolean> {
|
||
const user = await this.dataSource.getRepository(User).findOne({
|
||
where: [
|
||
{
|
||
account_id: accountId,
|
||
author_id: authorId,
|
||
},
|
||
],
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (user) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 特定の情報でユーザーを更新する
|
||
* @param user
|
||
* @returns update
|
||
*/
|
||
async update(
|
||
context: Context,
|
||
accountId: number,
|
||
id: number,
|
||
role: string,
|
||
authorId: string | undefined,
|
||
autoRenew: boolean,
|
||
notification: boolean,
|
||
encryption: boolean | undefined,
|
||
encryptionPassword: string | undefined,
|
||
prompt: boolean | undefined,
|
||
): Promise<UpdateResult> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const repo = entityManager.getRepository(User);
|
||
|
||
// 変更対象のユーザーを取得
|
||
const targetUser = await repo.findOne({
|
||
where: { id: id, account_id: accountId },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
|
||
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!targetUser) {
|
||
throw new UserNotFoundError(`User not Found.`);
|
||
}
|
||
|
||
// ユーザーのロールがNoneの場合以外はロールを変更できない
|
||
if (targetUser.role !== USER_ROLES.NONE && targetUser.role !== role) {
|
||
throw new InvalidRoleChangeError('Not None user cannot change role.');
|
||
}
|
||
|
||
if (role === USER_ROLES.AUTHOR) {
|
||
// ユーザーのロールがAuthorの場合はAuthorIDの重複チェックを行う
|
||
const authorIdDuplicatedUser = await repo.findOne({
|
||
where: { account_id: accountId, id: Not(id), author_id: authorId },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// 重複したAuthorIDがあった場合はエラー
|
||
if (authorIdDuplicatedUser) {
|
||
throw new AuthorIdAlreadyExistsError('authorId already exists.');
|
||
}
|
||
|
||
// 暗号化を有効にする場合はパスワードを設定する
|
||
if (encryption) {
|
||
// 暗号化パスワードが設定されている場合は更新する(undefinedの場合は変更なしとして元のパスワードを維持)
|
||
if (encryptionPassword) {
|
||
targetUser.encryption_password = encryptionPassword;
|
||
} else if (!targetUser.encryption_password) {
|
||
// DBにパスワードが設定されていない場合にはパスワードを設定しないとエラー
|
||
throw new EncryptionPasswordNeedError(
|
||
'encryption_password need to set value.',
|
||
);
|
||
}
|
||
} else {
|
||
targetUser.encryption_password = null;
|
||
}
|
||
|
||
// Author用項目を更新
|
||
targetUser.author_id = authorId ?? null;
|
||
targetUser.encryption = encryption ?? false;
|
||
targetUser.prompt = prompt ?? false;
|
||
} else {
|
||
// ユーザーのロールがAuthor以外の場合はAuthor用項目はundefinedにする
|
||
targetUser.author_id = null;
|
||
targetUser.encryption = false;
|
||
targetUser.encryption_password = null;
|
||
targetUser.prompt = false;
|
||
}
|
||
|
||
// 共通項目を更新
|
||
targetUser.role = role;
|
||
targetUser.auto_renew = autoRenew;
|
||
targetUser.notification = notification;
|
||
|
||
const result = await updateEntity(
|
||
repo,
|
||
{ id: id },
|
||
targetUser,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// 想定外の更新が行われた場合はロールバックを行った上でエラー送出
|
||
if (result.affected !== 1) {
|
||
throw new Error(`invalid update. result.affected=${result.affected}`);
|
||
}
|
||
return result;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 管理ユーザーがメール認証済みなら認証情報を更新する
|
||
* @param user
|
||
* @returns update
|
||
*/
|
||
async updateUserVerified(
|
||
context: Context,
|
||
id: number,
|
||
): Promise<UpdateResult> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const repo = entityManager.getRepository(User);
|
||
const targetUser = await repo.findOne({
|
||
where: {
|
||
id: id,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!targetUser) {
|
||
throw new UserNotFoundError(`User not Found.`);
|
||
}
|
||
|
||
if (targetUser.email_verified) {
|
||
throw new EmailAlreadyVerifiedError(`Email already verified user.`);
|
||
}
|
||
|
||
targetUser.email_verified = true;
|
||
|
||
return await updateEntity(
|
||
repo,
|
||
{ id: targetUser.id },
|
||
targetUser,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Emailを認証済みにして、トライアルライセンスを作成する
|
||
* @param id
|
||
* @returns user verified and create trial license
|
||
*/
|
||
async updateUserVerifiedAndCreateTrialLicense(
|
||
context: Context,
|
||
id: number,
|
||
): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
const userRepo = entityManager.getRepository(User);
|
||
const targetUser = await userRepo.findOne({
|
||
relations: {
|
||
account: true,
|
||
},
|
||
where: {
|
||
id: id,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!targetUser) {
|
||
throw new UserNotFoundError(`User not Found.`);
|
||
}
|
||
|
||
if (targetUser.email_verified) {
|
||
throw new EmailAlreadyVerifiedError(`Email already verified user.`);
|
||
}
|
||
|
||
targetUser.email_verified = true;
|
||
|
||
await updateEntity(
|
||
userRepo,
|
||
{ id: targetUser.id },
|
||
targetUser,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// トライアルライセンス100件を作成する
|
||
const licenseRepo = entityManager.getRepository(License);
|
||
const licenses: License[] = [];
|
||
|
||
// トライアルライセンスの有効期限は今日を起算日として30日後の日付が変わるまで
|
||
const expiryDate = new NewTrialLicenseExpirationDate();
|
||
|
||
for (let i = 0; i < TRIAL_LICENSE_ISSUE_NUM; i++) {
|
||
const license = new License();
|
||
license.expiry_date = expiryDate;
|
||
license.account_id = targetUser.account_id;
|
||
license.type = LICENSE_TYPE.TRIAL;
|
||
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
|
||
licenses.push(license);
|
||
}
|
||
|
||
await insertEntities(
|
||
License,
|
||
licenseRepo,
|
||
licenses,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 同じアカウントIDを持つユーザーを探す
|
||
* @param externalId
|
||
* @returns User[]
|
||
*/
|
||
async findSameAccountUsers(
|
||
external_id: string,
|
||
context: Context,
|
||
): Promise<User[]> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const repo = entityManager.getRepository(User);
|
||
|
||
const accountId = (
|
||
await repo.findOne({
|
||
where: { external_id },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
})
|
||
)?.account_id;
|
||
|
||
if (!accountId) {
|
||
throw new AccountNotFoundError('Account is Not Found.');
|
||
}
|
||
|
||
const dbUsers = await repo.find({
|
||
relations: {
|
||
userGroupMembers: {
|
||
userGroup: true,
|
||
},
|
||
license: true,
|
||
},
|
||
where: { account_id: accountId },
|
||
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// RoleのAuthor、Typist、Noneの順に並び替える
|
||
const roleSortedUsers = dbUsers.sort((a, b) => {
|
||
// Roleが同じ場合はIDの昇順で並び替える
|
||
if (a.role === b.role) {
|
||
return a.id - b.id;
|
||
}
|
||
|
||
return (
|
||
USER_ROLE_ORDERS.indexOf(a.role) - USER_ROLE_ORDERS.indexOf(b.role)
|
||
);
|
||
});
|
||
|
||
return roleSortedUsers;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* アカウント内のメール認証済みのタイピストユーザーを取得する
|
||
* @param sub
|
||
* @returns typist users
|
||
*/
|
||
async findTypistUsers(context: Context, sub: string): Promise<User[]> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const repo = entityManager.getRepository(User);
|
||
|
||
const user = await repo.findOne({
|
||
where: {
|
||
external_id: sub,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!user) {
|
||
throw new UserNotFoundError(`User not Found.`);
|
||
}
|
||
|
||
const typists = await repo.find({
|
||
where: {
|
||
account_id: user.account_id,
|
||
role: USER_ROLES.TYPIST,
|
||
email_verified: true,
|
||
deleted_at: IsNull(),
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
return typists;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* アカウント内のEmail認証済みのAuthorユーザーを取得する
|
||
* @param accountId
|
||
* @returns author users
|
||
*/
|
||
async findAuthorUsers(context: Context, accountId: number): Promise<User[]> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const repo = entityManager.getRepository(User);
|
||
const authors = await repo.find({
|
||
where: {
|
||
account_id: accountId,
|
||
role: USER_ROLES.AUTHOR,
|
||
email_verified: true,
|
||
deleted_at: IsNull(),
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
return authors;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Deletes user
|
||
* @param context Context
|
||
* @param userId 削除対象ユーザーのID
|
||
* @param currentTime ライセンス有効期限のチェックに使用する現在時刻
|
||
* @returns user
|
||
*/
|
||
async deleteUser(
|
||
context: Context,
|
||
userId: number,
|
||
currentTime: Date,
|
||
): Promise<{ isSuccess: boolean }> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const userRepo = entityManager.getRepository(User);
|
||
// 削除対象ユーザーをロックを取った上で取得
|
||
const target = await userRepo.findOne({
|
||
relations: {
|
||
account: true,
|
||
},
|
||
where: {
|
||
id: userId,
|
||
},
|
||
lock: { mode: 'pessimistic_write' },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
// 削除済みであれば失敗する
|
||
if (target == null) {
|
||
return { isSuccess: false };
|
||
}
|
||
|
||
const { account } = target;
|
||
if (account == null) {
|
||
// 通常ありえないが、アカウントが存在しない場合はエラー
|
||
throw new AccountNotFoundError('Account is Not Found.');
|
||
}
|
||
|
||
// 管理者IDの一覧を作成
|
||
const adminIds = [account]
|
||
.flatMap((x) => [x?.primary_admin_user_id, x?.secondary_admin_user_id])
|
||
.flatMap((x) => (x != null ? [x] : []));
|
||
// 削除対象が管理者であるかを確認
|
||
if (adminIds.some((adminId) => adminId === target.id)) {
|
||
throw new AdminDeleteFailedError('User is an admin.');
|
||
}
|
||
const userGroupRepo = entityManager.getRepository(UserGroup);
|
||
const groups = await userGroupRepo.find({
|
||
relations: {
|
||
userGroupMembers: true,
|
||
},
|
||
where: {
|
||
account_id: target.account_id,
|
||
},
|
||
lock: { mode: 'pessimistic_write' },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
const workflowRepo = entityManager.getRepository(Workflow);
|
||
const workflows = await workflowRepo.find({
|
||
where: {
|
||
account_id: target.account_id,
|
||
},
|
||
lock: { mode: 'pessimistic_write' },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// WorkflowのAuthor枠に設定されているユーザーは削除できない
|
||
if (workflows.some((x) => x.author_id === target.id)) {
|
||
throw new AssignedWorkflowWithAuthorDeleteFailedError(
|
||
'Author is assigned to a workflow.',
|
||
);
|
||
}
|
||
const workflowTypistsRepo = entityManager.getRepository(WorkflowTypist);
|
||
const workflowTypists = await workflowTypistsRepo.find({
|
||
where: {
|
||
typist_id: target.id,
|
||
},
|
||
lock: { mode: 'pessimistic_write' },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
// Workflowに直接個人で指定されているTypistは削除できない
|
||
if (workflowTypists.some((x) => x.typist_id === target.id)) {
|
||
throw new AssignedWorkflowWithTypistDeleteFailedError(
|
||
'Typist is assigned to a workflow.',
|
||
);
|
||
}
|
||
// いずれかのGroupに属しているユーザーIDの一覧を作成
|
||
const groupMemberIds = groups
|
||
.flatMap((group) => group.userGroupMembers)
|
||
.flatMap((member) => (member != null ? [member.user_id] : []));
|
||
|
||
// 削除対象ユーザーがGroupに属しているユーザーに含まれていたら削除できない
|
||
if (groupMemberIds.some((id) => id === target.id)) {
|
||
throw new ExistsGroupMemberDeleteFailedError(
|
||
'User is a member of a group',
|
||
);
|
||
}
|
||
// 削除対象ユーザーがAuthorであった時、
|
||
if (target.role === USER_ROLES.AUTHOR) {
|
||
const taskRepo = entityManager.getRepository(Task);
|
||
// 自分が所有者のタスクの一覧を取得する
|
||
const tasks = await taskRepo.find({
|
||
relations: {
|
||
file: true,
|
||
},
|
||
where: {
|
||
account_id: target.account_id,
|
||
status: Not(TASK_STATUS.BACKUP), // 数が膨大になりうる&有効なタスクへの状態遷移ができないBACKUPは除いて取得
|
||
file: {
|
||
owner_user_id: target.id,
|
||
},
|
||
},
|
||
lock: { mode: 'pessimistic_write' }, // lockする事で状態遷移の競合をブロックし、新規追加以外で所有タスク群の状態変更を防ぐ
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
// 未完了タスクが残っていたら削除できない
|
||
const enableStatus: string[] = [
|
||
TASK_STATUS.UPLOADED,
|
||
TASK_STATUS.IN_PROGRESS,
|
||
TASK_STATUS.PENDING,
|
||
];
|
||
// 未完了タスクを列挙
|
||
const enableTasks = tasks.filter((task) =>
|
||
enableStatus.includes(task.status),
|
||
);
|
||
if (enableTasks.length > 0) {
|
||
throw new ExistsValidTaskDeleteFailedError('User has valid tasks.');
|
||
}
|
||
}
|
||
// 削除対象ユーザーがTypistであった時、
|
||
if (target.role === USER_ROLES.TYPIST) {
|
||
const checkoutPermissionRepo =
|
||
entityManager.getRepository(CheckoutPermission);
|
||
const permissions = await checkoutPermissionRepo.find({
|
||
where: {
|
||
user_id: target.id,
|
||
},
|
||
lock: { mode: 'pessimistic_write' },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// タスクのチェックアウト権限が残っていたら削除できない
|
||
if (permissions.length !== 0) {
|
||
throw new ExistsCheckoutPermissionDeleteFailedError(
|
||
'User has checkout permissions.',
|
||
);
|
||
}
|
||
}
|
||
// 対象ユーザーのライセンス割り当て状態を取得
|
||
const licenseRepo = entityManager.getRepository(License);
|
||
const allocatedLicense = await licenseRepo.findOne({
|
||
where: {
|
||
allocated_user_id: userId,
|
||
},
|
||
lock: { mode: 'pessimistic_write' },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// ライセンスが割り当て済みかつ、
|
||
if (allocatedLicense !== null) {
|
||
const { status, expiry_date } = allocatedLicense;
|
||
// 有効な状態かつ、
|
||
if (status === LICENSE_ALLOCATED_STATUS.ALLOCATED) {
|
||
// 有効期限を迎えていない場合は削除できない
|
||
if (expiry_date !== null && expiry_date > currentTime) {
|
||
throw new ExistsValidLicenseDeleteFailedError(
|
||
'User has valid licenses.',
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ユーザーを削除する前に、削除対象ユーザーをアーカイブする
|
||
const userArchiveRepo = entityManager.getRepository(UserArchive);
|
||
await insertEntity(
|
||
UserArchive,
|
||
userArchiveRepo,
|
||
target,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
// 期限切れライセンスが割り当てられていた場合、ユーザーを削除する前にライセンスを割り当て解除する
|
||
// ※この処理時点で有効期限外ライセンスであることは確定であるため、期限切れ判定をここでは行わない
|
||
if (allocatedLicense != null) {
|
||
allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
|
||
allocatedLicense.allocated_user_id = null;
|
||
|
||
await updateEntity(
|
||
licenseRepo,
|
||
{ id: allocatedLicense.id },
|
||
allocatedLicense,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// ライセンス割り当て履歴テーブルへ登録
|
||
const licenseAllocationHistoryRepo = entityManager.getRepository(
|
||
LicenseAllocationHistory,
|
||
);
|
||
const deallocationHistory = new LicenseAllocationHistory();
|
||
deallocationHistory.user_id = userId;
|
||
deallocationHistory.license_id = allocatedLicense.id;
|
||
deallocationHistory.account_id = account.id;
|
||
deallocationHistory.is_allocated = false;
|
||
deallocationHistory.executed_at = new Date();
|
||
deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE;
|
||
await insertEntity(
|
||
LicenseAllocationHistory,
|
||
licenseAllocationHistoryRepo,
|
||
deallocationHistory,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
}
|
||
// ユーザテーブルのレコードを削除する
|
||
await deleteEntity(
|
||
userRepo,
|
||
{ id: target.id },
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
// ソート条件のテーブルのレコードを削除する
|
||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||
await deleteEntity(
|
||
sortCriteriaRepo,
|
||
{ user_id: target.id },
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
return { isSuccess: true };
|
||
});
|
||
}
|
||
|
||
/**
|
||
* UserID指定のユーザーとソート条件を同時に削除する
|
||
* @param userId
|
||
* @returns delete
|
||
*/
|
||
async deleteNormalUser(context: Context, userId: number): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
const usersRepo = entityManager.getRepository(User);
|
||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||
// ソート条件を削除
|
||
await deleteEntity(
|
||
sortCriteriaRepo,
|
||
{ user_id: userId },
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
// プライマリ管理者を削除
|
||
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 同意済み利用規約バージョンの情報を取得する
|
||
* @param externalId
|
||
* @returns TermsCheckInfo
|
||
*/
|
||
async getAcceptedAndLatestVersion(
|
||
context: Context,
|
||
externalId: string,
|
||
): Promise<TermsCheckInfo> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const userRepo = entityManager.getRepository(User);
|
||
const user = await userRepo.findOne({
|
||
where: {
|
||
external_id: externalId,
|
||
},
|
||
relations: {
|
||
account: true,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!user) {
|
||
throw new UserNotFoundError(`User not Found.`);
|
||
}
|
||
if (!user.account) {
|
||
throw new AccountNotFoundError('Account is Not Found.');
|
||
}
|
||
|
||
const termRepo = entityManager.getRepository(Term);
|
||
const latestEulaInfo = await termRepo.findOne({
|
||
where: {
|
||
document_type: TERM_TYPE.EULA,
|
||
},
|
||
order: {
|
||
id: 'DESC',
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
const latestPrivacyNoticeInfo = await termRepo.findOne({
|
||
where: {
|
||
document_type: TERM_TYPE.PRIVACY_NOTICE,
|
||
},
|
||
order: {
|
||
id: 'DESC',
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
const latestDpaInfo = await termRepo.findOne({
|
||
where: {
|
||
document_type: TERM_TYPE.DPA,
|
||
},
|
||
order: {
|
||
id: 'DESC',
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) {
|
||
throw new TermInfoNotFoundError(`Terms info is not found.`);
|
||
}
|
||
|
||
return {
|
||
tier: user.account.tier,
|
||
acceptedEulaVersion: user.accepted_eula_version ?? undefined,
|
||
acceptedPrivacyNoticeVersion:
|
||
user.accepted_privacy_notice_version ?? undefined,
|
||
acceptedDpaVersion: user.accepted_dpa_version ?? undefined,
|
||
latestEulaVersion: latestEulaInfo.version,
|
||
latestPrivacyNoticeVersion: latestPrivacyNoticeInfo.version,
|
||
latestDpaVersion: latestDpaInfo.version,
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 同意済み利用規約のバージョンを更新する
|
||
* @param externalId
|
||
* @param eulaVersion
|
||
* @param privacyNoticeVersion
|
||
* @param dpaVersion
|
||
* @returns update
|
||
*/
|
||
async updateAcceptedTermsVersion(
|
||
context: Context,
|
||
externalId: string,
|
||
eulaVersion: string,
|
||
privacyNoticeVersion: string,
|
||
dpaVersion: string | undefined,
|
||
): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
const userRepo = entityManager.getRepository(User);
|
||
const user = await userRepo.findOne({
|
||
where: {
|
||
external_id: externalId,
|
||
},
|
||
relations: {
|
||
account: true,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!user) {
|
||
throw new UserNotFoundError(
|
||
`User not found. externalId: ${externalId}`,
|
||
);
|
||
}
|
||
|
||
if (!user.account) {
|
||
throw new AccountNotFoundError('Account is Not Found.');
|
||
}
|
||
|
||
// パラメータが不在の場合はエラーを返却
|
||
if (!eulaVersion) {
|
||
throw new UpdateTermsVersionNotSetError(`EULA version param not set.`);
|
||
}
|
||
if (!privacyNoticeVersion) {
|
||
throw new UpdateTermsVersionNotSetError(
|
||
`PrivacyNotice version param not set.`,
|
||
);
|
||
}
|
||
if (user.account.tier !== TIERS.TIER5 && !dpaVersion) {
|
||
throw new UpdateTermsVersionNotSetError(
|
||
`DPA version param not set. User's tier: ${user.account.tier}`,
|
||
);
|
||
}
|
||
|
||
user.accepted_eula_version = eulaVersion;
|
||
user.accepted_privacy_notice_version =
|
||
privacyNoticeVersion ?? user.accepted_privacy_notice_version;
|
||
user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version;
|
||
await updateEntity(
|
||
userRepo,
|
||
{ id: user.id },
|
||
user,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 代行操作対象のユーザー情報を取得する
|
||
* @param delegateAccountId 代行操作者のアカウントID
|
||
* @param originAccountId 代行操作対象のアカウントID
|
||
* @returns delegate accounts
|
||
*/
|
||
async findDelegateUser(
|
||
context: Context,
|
||
delegateAccountId: number,
|
||
originAccountId: number,
|
||
): Promise<User> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const accountRepo = entityManager.getRepository(Account);
|
||
|
||
// 代行操作対象のアカウントを取得 ※親アカウントが代行操作者のアカウントIDと一致すること
|
||
const account = await accountRepo.findOne({
|
||
where: {
|
||
id: originAccountId,
|
||
parent_account_id: delegateAccountId,
|
||
tier: TIERS.TIER5,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!account) {
|
||
throw new AccountNotFoundError(
|
||
`Account is not found. originAccountId: ${originAccountId}, delegateAccountId: ${delegateAccountId}`,
|
||
);
|
||
}
|
||
|
||
// 代行操作が許可されていない場合はエラー
|
||
if (!account.delegation_permission) {
|
||
throw new DelegationNotAllowedError(
|
||
`Delegation is not allowed. id: ${originAccountId}`,
|
||
);
|
||
}
|
||
|
||
const adminUserId = account.primary_admin_user_id;
|
||
|
||
// 運用上、代行操作対象アカウントの管理者ユーザーがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!adminUserId) {
|
||
throw new Error(`Admin user is not found. id: ${originAccountId}`);
|
||
}
|
||
|
||
// 代行操作対象のアカウントの管理者ユーザーを取得
|
||
const userRepo = entityManager.getRepository(User);
|
||
const primaryUser = await userRepo.findOne({
|
||
where: {
|
||
account_id: originAccountId,
|
||
id: adminUserId,
|
||
},
|
||
relations: {
|
||
account: true,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// 運用上、代行操作対象アカウントの管理者ユーザーがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!primaryUser) {
|
||
throw new Error(`Admin user is not found. id: ${originAccountId}`);
|
||
}
|
||
|
||
return primaryUser;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 代行操作対象のユーザーの所属するアカウントの代行操作が許可されているか
|
||
* @param delegateAccountId 代行操作者のアカウントID
|
||
* @param originAccountId 代行操作対象のアカウントID
|
||
* @returns delegate accounts
|
||
*/
|
||
async isAllowDelegationPermission(
|
||
context: Context,
|
||
delegateAccountId: number,
|
||
originUserExternalId: string,
|
||
): Promise<boolean> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const userRepo = entityManager.getRepository(User);
|
||
const primaryUser = await userRepo.findOne({
|
||
where: {
|
||
external_id: originUserExternalId,
|
||
account: {
|
||
parent_account_id: delegateAccountId,
|
||
tier: TIERS.TIER5,
|
||
},
|
||
},
|
||
relations: {
|
||
account: true,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!primaryUser) {
|
||
throw new AdminUserNotFoundError(
|
||
`Admin user is not found. externalId: ${originUserExternalId}`,
|
||
);
|
||
}
|
||
|
||
const originAccount = primaryUser.account;
|
||
|
||
// 運用上、アカウントがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!originAccount) {
|
||
throw new Error(`Account is Not Found. id: ${primaryUser.account_id}`);
|
||
}
|
||
|
||
// 代行操作の許可の有無を返却
|
||
return originAccount.delegation_permission;
|
||
});
|
||
}
|
||
/**
|
||
* ユーザーに紐づく各種情報を取得する
|
||
* @param userId
|
||
* @returns user relations
|
||
*/
|
||
async getUserRelations(
|
||
context: Context,
|
||
userId: number,
|
||
): Promise<{
|
||
user: User;
|
||
authors: User[];
|
||
worktypes: Worktype[];
|
||
activeWorktype: Worktype | undefined;
|
||
}> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const userRepo = entityManager.getRepository(User);
|
||
|
||
const user = await userRepo.findOne({
|
||
where: { id: userId },
|
||
relations: { account: true },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
if (!user) {
|
||
throw new UserNotFoundError(`User is Not Found. id: ${userId}`);
|
||
}
|
||
|
||
// 運用上、アカウントがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||
if (!user.account) {
|
||
throw new AccountNotFoundError(
|
||
`Account is Not Found. user.id: ${userId}`,
|
||
);
|
||
}
|
||
|
||
// ユーザーの所属するアカウント内のすべてのメール認証済みAuthorユーザーを取得する
|
||
const authors = await userRepo.find({
|
||
where: {
|
||
account_id: user.account_id,
|
||
role: USER_ROLES.AUTHOR,
|
||
email_verified: true,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// ユーザーの所属するアカウント内のアクティブワークタイプを取得する
|
||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||
let activeWorktype: Worktype | undefined = undefined;
|
||
const activeWorktypeId = user.account.active_worktype_id;
|
||
if (activeWorktypeId !== null) {
|
||
activeWorktype =
|
||
(await worktypeRepo.findOne({
|
||
where: {
|
||
account_id: user.account_id,
|
||
id: activeWorktypeId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
})) ?? undefined;
|
||
}
|
||
|
||
let worktypes: Worktype[] = [];
|
||
// ユーザーのロールがAuthorの場合はルーティングルールに紐づいたワークタイプを取得する
|
||
if (user.role === USER_ROLES.AUTHOR) {
|
||
const workflowRepo = entityManager.getRepository(Workflow);
|
||
const workflows = await workflowRepo.find({
|
||
where: {
|
||
account_id: user.account_id,
|
||
author_id: user.id,
|
||
worktype_id: Not(IsNull()),
|
||
},
|
||
relations: {
|
||
worktype: {
|
||
option_items: true,
|
||
},
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
worktypes = workflows.flatMap((workflow) =>
|
||
workflow.worktype ? [workflow.worktype] : [],
|
||
);
|
||
}
|
||
|
||
return { user, authors, worktypes, activeWorktype };
|
||
});
|
||
}
|
||
}
|