diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 6cdd041..00d1c62 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -87,4 +87,5 @@ export const ErrorCodes = [ 'E017003', // 親アカウント変更不可エラー(リージョンが同一でない) 'E017004', // 親アカウント変更不可エラー(国が同一でない) 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) + 'E019001', // パートナーアカウント取得不可エラー(階層構造が不正) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 6d3855d..123dc5c 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -77,4 +77,5 @@ export const errors: Errors = { E017003: 'Parent account switch failed Error: region mismatch', E017004: 'Parent account switch failed Error: country mismatch', E018001: 'Partner account delete failed Error: not satisfied conditions', + E019001: 'Partner account get failed Error: hierarchy mismatch', }; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index db02299..0cbee34 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2559,13 +2559,8 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO: 仮実装 - /*await this.accountService.getPartnerUsers( - context, - targetAccountId, - ); - */ - //仮の返却値 + await this.accountService.getPartnerUsers(context, userId, targetAccountId); + return { users: [] }; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 7cd6f4a..066eb0b 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -161,7 +161,7 @@ describe('createAccount', () => { }, }); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -6261,7 +6261,7 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -9447,3 +9447,191 @@ describe('deletePartnerAccount', () => { } }); }); +describe('getPartnerUsers', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('パートナーアカウント情報が取得できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + const context = makeContext(tier3Admin.external_id, 'requestId'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // パートナーアカウント情報の取得 + const partnerUsers = await service.getPartnerUsers( + context, + tier3Admin.external_id, + tier4Account.id, + ); + expect(partnerUsers).toEqual([ + { + id: tier4Admin.id, + name: 'adb2c' + tier4Admin.external_id, + email: 'mail@example.com', + isPrimaryAdmin: true, + }, + { + id: typist.id, + name: 'adb2c' + typist.external_id, + email: 'mail@example.com', + isPrimaryAdmin: false, + }, + ]); + }); + + it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 第3階層のアカウント作成 + const { admin: tier3Admin } = await makeTestAccount(source, { tier: 3 }); + const { account: tier3Parent } = await makeTestAccount(source, { tier: 3 }); + // 第4階層のアカウント作成 + const { account: tier4Account } = await makeTestAccount( + source, + { + parent_account_id: tier3Parent.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + try { + // パートナーアカウント情報の取得 + await service.getPartnerUsers( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E019001')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index b4a943d..484dbf0 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -37,6 +37,7 @@ import { Author, Partner, GetCompanyNameResponse, + PartnerUser, } from './types/types'; import { DateWithZeroTime, @@ -2981,4 +2982,112 @@ export class AccountsService { ); } } + /** + * 指定したアカウントIDのユーザー情報を取得します + * @param context + * @param targetAccountId 取得対象パートナーのアカウントID + * @returns PartnerUser[] + */ + async getPartnerUsers( + context: Context, + externalId: string, + targetAccountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.getPartnerUsers.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId},};`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: myAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (myAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + // 指定したアカウントIDの情報を取得する + const targetAccount = await this.accountRepository.findAccountById( + context, + targetAccountId, + ); + + // 実行者のアカウントが対象アカウントの親アカウントであるか確認する。 + if (myAccount.id !== targetAccount.parent_account_id) { + throw new HierarchyMismatchError( + `Invalid hierarchy relation. accountId: ${targetAccountId}`, + ); + } + // 対象アカウントのユーザ一覧を取得する + const users = await this.usersRepository.findUsersByAccountId( + context, + targetAccountId, + ); + + //ADB2Cからユーザー情報を取得する + const externalIds = users.map((x) => x.external_id); + const adb2cUsers = await this.adB2cService.getUsers(context, externalIds); + + // ユーザー情報をマージする + const partnerUsers = users.map((user) => { + const adb2cUser = adb2cUsers.find( + (adb2c) => user.external_id === adb2c.id, + ); + if (!adb2cUser) { + throw new Error( + `adb2c user not found. externalId: ${user.external_id}`, + ); + } + const { displayName, emailAddress } = + getUserNameAndMailAddress(adb2cUser); + if (!emailAddress) { + throw new Error( + `adb2c user mail not found. externalId: ${user.external_id}`, + ); + } + return { + id: user.id, + name: displayName, + email: emailAddress, + isPrimaryAdmin: targetAccount.primary_admin_user_id === user.id, + }; + }); + + return partnerUsers; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E019001'), + 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.getPartnerUsers.name}`, + ); + } + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 03c2954..2363279 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -15,7 +15,6 @@ import { Max, IsString, IsNotEmpty, - IsBoolean, } from 'class-validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator'; diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index e319156..01b0ff9 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -190,7 +190,26 @@ export class UsersRepositoryService { } return user; } + /** + * アカウントIDをもとにユーザー一覧を取得します。 + * @param context + * @param accountId 検索対象のアカウントID + * @returns users[] + */ + async findUsersByAccountId( + context: Context, + accountId: number, + ): Promise { + const users = await this.dataSource.getRepository(User).find({ + where: { + email_verified: true, + account_id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + return users; + } /** * AuthorIDをもとにユーザーを取得します。 * AuthorIDがセットされていない場合や、ユーザーが存在しない場合はエラーを返します。