import { Injectable } from '@nestjs/common'; import { User, newUser } from './entity/user.entity'; import { DataSource, FindOptionsWhere, 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, } from './errors/types'; import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE, TERM_TYPE, TIERS, TRIAL_LICENSE_ISSUE_NUM, USER_ROLES, USER_ROLE_ORDERS, } from '../../constants'; import { License } 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'; @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 { 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 { 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 { 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 { 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 { 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 { 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 => ({ 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; }); } /** * UserID指定のユーザーとソート条件を同時に削除する * @param userId * @returns delete */ async deleteNormalUser(context: Context, userId: number): Promise { 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 { 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 { 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 { 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 { 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 }; }); } }