diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 9b271e8..d7dec4b 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -38,6 +38,7 @@ export const ErrorCodes = [ 'E010401', // PONumber重複エラー 'E010501', // アカウント不在エラー 'E010502', // アカウント情報変更不可エラー + 'E010503', // 代行操作不許可エラー 'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) 'E010602', // タスク変更権限不足エラー 'E010603', // タスク不在エラー diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 11b0c1d..38c5e98 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -27,6 +27,7 @@ export const errors: Errors = { E010401: 'This PoNumber already used Error', E010501: 'Account not Found Error.', E010502: 'Account information cannot be changed Error.', + E010503: 'Delegation not allowed Error.', E010601: 'Task is not Editable Error', E010602: 'No task edit permissions Error', E010603: 'Task not found Error.', diff --git a/dictation_server/src/common/log/context.ts b/dictation_server/src/common/log/context.ts index cd6079d..229cca8 100644 --- a/dictation_server/src/common/log/context.ts +++ b/dictation_server/src/common/log/context.ts @@ -1,7 +1,11 @@ import { Context } from './types'; -export const makeContext = (externalId: string): Context => { +export const makeContext = ( + externalId: string, + delegationId?: string, +): Context => { return { trackingId: externalId, + delegationId: delegationId, }; }; diff --git a/dictation_server/src/common/log/types.ts b/dictation_server/src/common/log/types.ts index da7e2ba..5c843db 100644 --- a/dictation_server/src/common/log/types.ts +++ b/dictation_server/src/common/log/types.ts @@ -3,4 +3,8 @@ export class Context { * APIの操作ユーザーを追跡するためのID */ trackingId: string; + /** + * APIの代行操作ユーザーを追跡するためのID + */ + delegationId?: string | undefined; } diff --git a/dictation_server/src/common/token/types.ts b/dictation_server/src/common/token/types.ts index 43650af..e1df6d9 100644 --- a/dictation_server/src/common/token/types.ts +++ b/dictation_server/src/common/token/types.ts @@ -1,4 +1,8 @@ export type RefreshToken = { + /** + * 外部認証サービスの識別子(代行者) + */ + delegateUserId?: string | undefined; /** * 外部認証サービスの識別子 */ @@ -14,6 +18,10 @@ export type RefreshToken = { }; export type AccessToken = { + /** + * 外部認証サービスの識別子(代行者) + */ + delegateUserId?: string | undefined; /** * 外部認証サービスの識別子 */ diff --git a/dictation_server/src/features/auth/auth.controller.ts b/dictation_server/src/features/auth/auth.controller.ts index aa82fc2..0418584 100644 --- a/dictation_server/src/features/auth/auth.controller.ts +++ b/dictation_server/src/features/auth/auth.controller.ts @@ -31,6 +31,8 @@ import { Request } from 'express'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES, TIERS } from '../../constants'; +import jwt from 'jsonwebtoken'; +import { AccessToken } from '../../common/token'; @ApiTags('auth') @Controller('auth') @@ -183,18 +185,35 @@ export class AuthController { @Body() body: DelegationTokenRequest, ): Promise { const { delegatedAccountId } = body; - const refreshToken = retrieveAuthorizationToken(req); + const token = retrieveAuthorizationToken(req); - if (!refreshToken) { + if (!token) { throw new HttpException( makeErrorResponse('E000107'), HttpStatus.UNAUTHORIZED, ); } + const decodedAccessToken = jwt.decode(token, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; - const context = makeContext(uuidv4()); + const context = makeContext(userId); + const refreshToken = await this.authService.generateDelegationRefreshToken( + context, + userId, + delegatedAccountId, + ); + const accessToken = await this.authService.generateDelegationAccessToken( + context, + refreshToken, + ); - return { accessToken: '', refreshToken: '' }; + return { accessToken, refreshToken }; } @Post('delegation/access-token') diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts index af11c93..bcb5324 100644 --- a/dictation_server/src/features/auth/auth.service.spec.ts +++ b/dictation_server/src/features/auth/auth.service.spec.ts @@ -13,6 +13,9 @@ import { makeTestAccount } from '../../common/test/utility'; import { AuthService } from './auth.service'; import { createTermInfo } from './test/utility'; import { v4 as uuidv4 } from 'uuid'; +import { TIERS, USER_ROLES } from '../../constants'; +import { decode, isVerifyError } from '../../common/jwt'; +import { RefreshToken, AccessToken } from '../../common/token'; describe('AuthService', () => { it('IDトークンの検証とペイロードの取得に成功する', async () => { @@ -276,6 +279,239 @@ describe('checkIsAcceptedLatestVersion', () => { }); }); +describe('generateDelegationRefreshToken', () => { + let source: DataSource | null = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + if (!source) return; + await source.destroy(); + source = null; + }); + it('代行操作が許可されたパートナーの代行操作用リフレッシュトークンを取得できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AuthService); + const { admin: parentAdmin, account: parentAccount } = + await makeTestAccount(source, { + tier: 4, + }); + const { admin: partnerAdmin, account: partnerAccount } = + await makeTestAccount( + source, + { + tier: 5, + parent_account_id: parentAccount.id, + delegation_permission: true, + }, + { role: USER_ROLES.NONE }, + ); + + const context = makeContext(parentAdmin.external_id); + + const delegationRefreshToken = await service.generateDelegationRefreshToken( + context, + parentAdmin.external_id, + partnerAccount.id, + ); + + // 取得できた代行操作用リフレッシュトークンをデコード + const decodeToken = decode(delegationRefreshToken); + if (isVerifyError(decodeToken)) { + fail(); + } + + expect(decodeToken.role).toBe('none admin'); + expect(decodeToken.tier).toBe(TIERS.TIER5); + expect(decodeToken.userId).toBe(partnerAdmin.external_id); + expect(decodeToken.delegateUserId).toBe(parentAdmin.external_id); + }); + it('代行操作が許可されていない場合、400エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AuthService); + const { admin: parentAdmin, account: parentAccount } = + await makeTestAccount(source, { + tier: 4, + }); + const { account: partnerAccount } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: parentAccount.id, + delegation_permission: false, + }, + { role: USER_ROLES.NONE }, + ); + + const context = makeContext(parentAdmin.external_id); + + try { + await service.generateDelegationRefreshToken( + context, + parentAdmin.external_id, + partnerAccount.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010503')); + } else { + fail(); + } + } + }); + + it('代行操作対象が存在しない場合、400エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AuthService); + const { admin: parentAdmin, account: parentAccount } = + await makeTestAccount(source, { + tier: 4, + }); + await makeTestAccount( + source, + { + tier: 5, + parent_account_id: parentAccount.id, + delegation_permission: false, + }, + { role: USER_ROLES.NONE }, + ); + + const context = makeContext(parentAdmin.external_id); + + try { + await service.generateDelegationRefreshToken( + context, + parentAdmin.external_id, + 9999, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010501')); + } else { + fail(); + } + } + }); +}); + +describe('generateDelegationAccessToken', () => { + let source: DataSource | null = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + if (!source) return; + await source.destroy(); + source = null; + }); + it('代行操作用リフレッシュトークンから代行操作用アクセストークンを取得できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AuthService); + const { admin: parentAdmin, account: parentAccount } = + await makeTestAccount(source, { + tier: 4, + }); + const { admin: partnerAdmin, account: partnerAccount } = + await makeTestAccount( + source, + { + tier: 5, + parent_account_id: parentAccount.id, + delegation_permission: true, + }, + { role: USER_ROLES.NONE }, + ); + + const context = makeContext(parentAdmin.external_id); + + const delegationRefreshToken = await service.generateDelegationRefreshToken( + context, + parentAdmin.external_id, + partnerAccount.id, + ); + + // 取得できた代行操作用リフレッシュトークンをデコード + const decodeRefreshToken = decode(delegationRefreshToken); + if (isVerifyError(decodeRefreshToken)) { + fail(); + } + + expect(decodeRefreshToken.role).toBe('none admin'); + expect(decodeRefreshToken.tier).toBe(TIERS.TIER5); + expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id); + expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id); + + const delegationAccessToken = await service.generateDelegationAccessToken( + context, + delegationRefreshToken, + ); + + // 取得できた代行操作用アクセストークンをデコード + const decodeAccessToken = decode(delegationAccessToken); + if (isVerifyError(decodeAccessToken)) { + fail(); + } + + expect(decodeAccessToken.role).toBe('none admin'); + expect(decodeAccessToken.tier).toBe(TIERS.TIER5); + expect(decodeAccessToken.userId).toBe(partnerAdmin.external_id); + expect(decodeAccessToken.delegateUserId).toBe(parentAdmin.external_id); + }); + + it('代行操作用リフレッシュトークンの形式が不正な場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AuthService); + const { admin: parentAdmin } = await makeTestAccount(source, { + tier: 4, + }); + + const context = makeContext(parentAdmin.external_id); + + try { + await service.generateDelegationAccessToken(context, 'invalid token'); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.UNAUTHORIZED); + expect(e.getResponse()).toEqual(makeErrorResponse('E000101')); + } else { + fail(); + } + } + }); +}); + const idTokenPayload = { exp: 9000000000, nbf: 1000000000, diff --git a/dictation_server/src/features/auth/auth.service.ts b/dictation_server/src/features/auth/auth.service.ts index 1454960..3e2a9cc 100644 --- a/dictation_server/src/features/auth/auth.service.ts +++ b/dictation_server/src/features/auth/auth.service.ts @@ -19,9 +19,14 @@ import { } from '../../common/token'; import { ADMIN_ROLES, TIERS, USER_ROLES } from '../../constants'; import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; -import { User } from '../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { Context } from '../../common/log'; +import { + AccountNotFoundError, + AdminUserNotFoundError, +} from '../../repositories/accounts/errors/types'; +import { DelegationNotAllowedError } from '../../repositories/users/errors/types'; +import { RoleUnexpectedError, TierUnexpectedError } from './errors/types'; @Injectable() export class AuthService { @@ -76,88 +81,77 @@ export class AuthService { `[IN] [${context.trackingId}] ${this.generateRefreshToken.name}`, ); - let user: User; // ユーザー情報とユーザーが属しているアカウント情報を取得 try { - user = await this.usersRepository.findUserByExternalId(idToken.sub); + const user = await this.usersRepository.findUserByExternalId(idToken.sub); if (!user.account) { throw new Error('Account information not found'); } + + // Tierのチェック + const minTier = 1; + const maxTier = 5; + const userTier = user.account.tier; + if (userTier < minTier || userTier > maxTier) { + throw new TierUnexpectedError( + `Tier from DB is unexpected value. tier=${user.account.tier}`, + ); + } + + // 要求された環境用トークンの寿命を決定 + const refreshTokenLifetime = + type === 'web' + ? this.refreshTokenLifetimeWeb + : this.refreshTokenLifetimeDefault; + const privateKey = getPrivateKey(this.configService); + + // ユーザーのロールを設定 + const role = this.getUserRole(user.role); + + const token = sign( + { + //ユーザーの属しているアカウントの管理者にユーザーが設定されていればadminをセットする + role: `${role} ${ + user.account.primary_admin_user_id === user.id || + user.account.secondary_admin_user_id === user.id + ? ADMIN_ROLES.ADMIN + : ADMIN_ROLES.STANDARD + }`, + tier: user.account.tier, + userId: idToken.sub, + }, + refreshTokenLifetime, + privateKey, + ); + + return token; } catch (e) { this.logger.error(`error=${e}`); - this.logger.log( - `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`, - ); + if (e instanceof Error) { + switch (e.constructor) { + case TierUnexpectedError: + throw new HttpException( + makeErrorResponse('E010206'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + case RoleUnexpectedError: + throw new HttpException( + makeErrorResponse('E010205'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + default: + break; + } + } throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); - } - // Tierのチェック - const minTier = 1; - const maxTier = 5; - const userTier = user.account.tier; - if (userTier < minTier || userTier > maxTier) { - this.logger.error( - `Tier from DB is unexpected value. tier=${user.account.tier}`, - ); + } finally { this.logger.log( `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`, ); - throw new HttpException( - makeErrorResponse('E010206'), - HttpStatus.INTERNAL_SERVER_ERROR, - ); } - // 要求された環境用トークンの寿命を決定 - const refreshTokenLifetime = - type === 'web' - ? this.refreshTokenLifetimeWeb - : this.refreshTokenLifetimeDefault; - const privateKey = getPrivateKey(this.configService); - - // ユーザーのロールを設定 - // 万一不正なRoleが登録されていた場合、そのままDBの値を使用すると不正なロールのリフレッシュトークンが発行されるため、 - // ロールの設定値はDBに保存したRoleの値を直接トークンに入れないように定数で設定する - // ※none/author/typist以外はロールに設定されない - let role = ''; - if (user.role === USER_ROLES.NONE) { - role = USER_ROLES.NONE; - } else if (user.role === USER_ROLES.AUTHOR) { - role = USER_ROLES.AUTHOR; - } else if (user.role === USER_ROLES.TYPIST) { - role = USER_ROLES.TYPIST; - } else { - this.logger.error(`Role from DB is unexpected value. role=${user.role}`); - this.logger.log( - `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`, - ); - throw new HttpException( - makeErrorResponse('E010205'), - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - - const token = sign( - { - //ユーザーの属しているアカウントの管理者にユーザーが設定されていればadminをセットする - role: `${role} ${ - user.account.primary_admin_user_id === user.id || - user.account.secondary_admin_user_id === user.id - ? ADMIN_ROLES.ADMIN - : ADMIN_ROLES.STANDARD - }`, - tier: user.account.tier, - userId: idToken.sub, - }, - refreshTokenLifetime, - privateKey, - ); - - this.logger.log( - `[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`, - ); - return token; } /** @@ -203,6 +197,137 @@ export class AuthService { ); return accessToken; } + + /** + * 代行操作用のリフレッシュトークンを生成します + * @param context + * @param delegateUserExternalId 代行操作者の外部認証サービスの識別子 + * @param originAccountId 代行操作対象アカウントのID + * @returns delegation refresh token + */ + async generateDelegationRefreshToken( + context: Context, + delegateUserExternalId: string, + originAccountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.generateDelegationRefreshToken.name} | params: { ` + + `delegateUserExternalId: ${delegateUserExternalId}, ` + + `originAccountId: ${originAccountId}, };`, + ); + + // ユーザー情報とユーザーが属しているアカウント情報を取得 + try { + const user = await this.usersRepository.findUserByExternalId( + delegateUserExternalId, + ); + + // 代行操作対象アカウントの管理者ユーザーを取得 + const adminUser = await this.usersRepository.findDelegateUser( + user.account_id, + originAccountId, + ); + + // 要求された環境用トークンの寿命を決定 + const refreshTokenLifetime = this.refreshTokenLifetimeWeb; + const privateKey = getPrivateKey(this.configService); + + // ユーザーのロールを設定 + const role = this.getUserRole(adminUser.role); + + const token = sign( + { + role: `${role} ${ADMIN_ROLES.ADMIN}`, + tier: TIERS.TIER5, + userId: adminUser.external_id, + delegateUserId: delegateUserExternalId, + }, + refreshTokenLifetime, + privateKey, + ); + + return token; + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + case AdminUserNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + case DelegationNotAllowedError: + throw new HttpException( + makeErrorResponse('E010503'), + HttpStatus.BAD_REQUEST, + ); + case RoleUnexpectedError: + throw new HttpException( + makeErrorResponse('E010205'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + default: + break; + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.generateDelegationRefreshToken.name}`, + ); + } + } + + /** + * 代行操作アクセストークンの更新 + * @param context + * @param refreshToken + * @returns delegation access token + */ + async generateDelegationAccessToken( + context: Context, + refreshToken: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.generateDelegationAccessToken.name}`, + ); + + const privateKey = getPrivateKey(this.configService); + const pubkey = getPublicKey(this.configService); + + const token = verify(refreshToken, pubkey); + if (isVerifyError(token)) { + this.logger.error(`${token.reason} | ${token.message}`); + this.logger.log( + `[OUT] [${context.trackingId}] ${this.generateDelegationAccessToken.name}`, + ); + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + + const accessToken = sign( + { + role: token.role, + tier: token.tier, + userId: token.userId, + delegateUserId: token.delegateUserId, + }, + this.accessTokenlifetime, + privateKey, + ); + + this.logger.log( + `[OUT] [${context.trackingId}] ${this.generateDelegationAccessToken.name}`, + ); + return accessToken; + } + /** * Gets id token * @param token @@ -340,6 +465,26 @@ export class AuthService { HttpStatus.UNAUTHORIZED, ); }; + /** + * トークンに設定するユーザーのロールを取得 + */ + getUserRole = (role: string): string => { + // ユーザーのロールを設定 + // 万一不正なRoleが登録されていた場合、そのままDBの値を使用すると不正なロールのリフレッシュトークンが発行されるため、 + // ロールの設定値はDBに保存したRoleの値を直接トークンに入れないように定数で設定する + // ※none/author/typist以外はロールに設定されない + if (role === USER_ROLES.NONE) { + return USER_ROLES.NONE; + } else if (role === USER_ROLES.AUTHOR) { + return USER_ROLES.AUTHOR; + } else if (role === USER_ROLES.TYPIST) { + return USER_ROLES.TYPIST; + } else { + throw new RoleUnexpectedError( + `Role from DB is unexpected value. role=${role}`, + ); + } + }; /** * 同意済み利用規約バージョンが最新かチェック diff --git a/dictation_server/src/features/auth/errors/types.ts b/dictation_server/src/features/auth/errors/types.ts new file mode 100644 index 0000000..5c12d36 --- /dev/null +++ b/dictation_server/src/features/auth/errors/types.ts @@ -0,0 +1,4 @@ +// Role文字列想定外エラー +export class RoleUnexpectedError extends Error {} +// Tier範囲想定外エラー +export class TierUnexpectedError extends Error {} diff --git a/dictation_server/src/features/auth/types/types.ts b/dictation_server/src/features/auth/types/types.ts index 7528fa7..1be9570 100644 --- a/dictation_server/src/features/auth/types/types.ts +++ b/dictation_server/src/features/auth/types/types.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsInt } from 'class-validator'; export class TokenRequest { @ApiProperty() @@ -28,6 +29,7 @@ export type TermsCheckInfo = { export class DelegationTokenRequest { @ApiProperty({ description: '代行操作対象のアカウントID' }) + @IsInt() delegatedAccountId: number; } export class DelegationTokenResponse { diff --git a/dictation_server/src/repositories/users/errors/types.ts b/dictation_server/src/repositories/users/errors/types.ts index faee0b1..32c1af0 100644 --- a/dictation_server/src/repositories/users/errors/types.ts +++ b/dictation_server/src/repositories/users/errors/types.ts @@ -12,3 +12,5 @@ export class EncryptionPasswordNeedError extends Error {} export class TermInfoNotFoundError extends Error {} // 利用規約バージョンパラメータ不在エラー export class UpdateTermsVersionNotSetError extends Error {} +// 代行操作不許可エラー +export class DelegationNotAllowedError extends Error {} diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index f579c3e..7f92ae3 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -14,6 +14,7 @@ import { EncryptionPasswordNeedError, TermInfoNotFoundError, UpdateTermsVersionNotSetError, + DelegationNotAllowedError, } from './errors/types'; import { LICENSE_ALLOCATED_STATUS, @@ -27,7 +28,11 @@ 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 } from '../accounts/errors/types'; +import { + AccountNotFoundError, + AdminUserNotFoundError, +} from '../accounts/errors/types'; +import { Account } from '../accounts/entity/account.entity'; @Injectable() export class UsersRepositoryService { @@ -533,4 +538,67 @@ export class UsersRepositoryService { await userRepo.update({ id: user.id }, user); }); } + + /** + * 代行操作対象のユーザー情報を取得する + * @param delegateAccountId 代行操作者のアカウントID + * @param originAccountId 代行操作対象のアカウントID + * @returns delegate accounts + */ + async findDelegateUser( + 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, + }, + }); + + 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, + }, + }); + + // 運用上、代行操作対象アカウントの管理者ユーザーがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!primaryUser) { + throw new Error(`Admin user is not found. id: ${originAccountId}`); + } + + return primaryUser; + }); + } }