diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index b2bbf73..2761b0e 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AccountsController } from './accounts.controller'; import { AccountsService } from './accounts.service'; import { ConfigModule } from '@nestjs/config'; +import { AuthService } from '../auth/auth.service'; describe('AccountsController', () => { let controller: AccountsController; const mockAccountService = {}; + const mockAuthService = {}; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -16,10 +18,12 @@ describe('AccountsController', () => { }), ], controllers: [AccountsController], - providers: [AccountsService], + providers: [AccountsService, AuthService], }) .overrideProvider(AccountsService) .useValue(mockAccountService) + .overrideProvider(AuthService) + .useValue(mockAuthService) .compile(); controller = module.get(AccountsController); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 92206af..bb98487 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -8,6 +8,7 @@ import { UseGuards, Param, Query, + HttpException, } from '@nestjs/common'; import { ApiOperation, @@ -74,12 +75,15 @@ import { AccessToken } from '../../common/token'; import jwt from 'jsonwebtoken'; import { makeContext } from '../../common/log'; import { v4 as uuidv4 } from 'uuid'; +import { AuthService } from '../auth/auth.service'; +import { makeErrorResponse } from '../../common/error/makeErrorResponse'; @ApiTags('accounts') @Controller('accounts') export class AccountsController { constructor( private readonly accountService: AccountsService, //private readonly cryptoService: CryptoService, + private readonly authService: AuthService, ) {} @Post() @@ -1116,11 +1120,22 @@ export class AccountsController { async getAccountInfoMinimalAccess( @Body() body: GetAccountInfoMinimalAccessRequest, ): Promise { - const context = makeContext(uuidv4()); + // IDトークンの検証 + const idToken = await this.authService.getVerifiedIdToken(body.idToken); + const isVerified = await this.authService.isVerifiedUser(idToken); + if (!isVerified) { + throw new HttpException( + makeErrorResponse('E010201'), + HttpStatus.BAD_REQUEST, + ); + } - // TODO 仮実装。API実装タスクで本実装する。 - // const idToken = await this.authService.getVerifiedIdToken(body.idToken); - // await this.accountService.getAccountInfoMinimalAccess(context, idToken); - return; + const context = makeContext(idToken.sub); + + const tier = await this.accountService.getAccountInfoMinimalAccess( + context, + idToken.sub, + ); + return { tier }; } } diff --git a/dictation_server/src/features/accounts/accounts.module.ts b/dictation_server/src/features/accounts/accounts.module.ts index 4b43415..23cf65e 100644 --- a/dictation_server/src/features/accounts/accounts.module.ts +++ b/dictation_server/src/features/accounts/accounts.module.ts @@ -9,6 +9,7 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module'; import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktypes.repository.module'; +import { AuthService } from '../auth/auth.service'; @Module({ imports: [ @@ -22,6 +23,6 @@ import { WorktypesRepositoryModule } from '../../repositories/worktypes/worktype BlobstorageModule, ], controllers: [AccountsController], - providers: [AccountsService], + providers: [AccountsService, AuthService], }) export class AccountsModule {} diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index c7786d5..e5ec12d 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -5595,3 +5595,127 @@ describe('deleteAccountAndData', () => { expect(userRecord).toBe(null); }); }); +describe('getAccountInfoMinimalAccess', () => { + let source: DataSource = 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 () => { + await source.destroy(); + source = null; + }); + it('IDトークンのsub情報からアカウントの階層情報を取得できること(第五階層)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(5); + } + + const tier = await service.getAccountInfoMinimalAccess( + context, + admin.external_id, + ); + + //実行結果を確認 + expect(tier).toBe(5); + }); + it('IDトークンのSub情報からアカウントの階層情報を取得できること(第四階層)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第四階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(4); + } + + const tier = await service.getAccountInfoMinimalAccess( + context, + admin.external_id, + ); + + //実行結果を確認 + expect(tier).toBe(4); + }); + it('対象のユーザーが存在しない場合、400エラーとなること', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第四階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(4); + } + + try { + await service.getAccountInfoMinimalAccess(context, 'fail_external_id'); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010204')); + } else { + fail(); + } + } + }); + it('DBアクセスに失敗した場合、500エラーとなること', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + // 第四階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(admin.external_id); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account.tier).toBe(4); + } + + //DBアクセスに失敗するようにする + const usersRepositoryService = module.get( + UsersRepositoryService, + ); + usersRepositoryService.findUserByExternalId = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.getAccountInfoMinimalAccess(context, admin.external_id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 0587575..c2dacad 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -1843,4 +1843,61 @@ export class AccountsService { `[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`, ); } + + /** + * IDトークンのsubからアカウントの階層情報を取得します + * @param context + * @param externalId + * @returns account info minimal access + */ + async getAccountInfoMinimalAccess( + context: Context, + externalId: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getAccountInfoMinimalAccess.name} | params: { externalId: ${externalId} };`, + ); + + try { + const { account } = await this.usersRepository.findUserByExternalId( + externalId, + ); + if (!account) { + throw new AccountNotFoundError( + `Account not found. externalId: ${externalId}`, + ); + } + + return account.tier; + } catch (e) { + this.logger.error(`[${context.trackingId}] 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, + ); + 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.trackingId}] ${this.getAccountInfoMinimalAccess.name}`, + ); + } + } }