From c1f370faaf9446bb73b2bfd8a48f25f366f2c46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Mon, 26 Feb 2024 05:13:43 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20766:=20API=20I/F=20&=20system?= =?UTF-8?q?=E6=A8=A9=E9=99=90Token=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3764: API I/F & system権限Token実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3764) - システムが発行したトークンの型定義を追加 - AuthGuardと同等の、システムが発行したTokenである `SystemAccessToken` を検証する `SystemAccessGuard` を追加 - API I/Fを実装 ## レビューポイント - バリデーターは適切か - システムが発行したトークンの型定義は適切か - API I/Fの型は問題ないか ## 動作確認状況 - ローカルでswagger UI上で確認 --- dictation_server/src/app.module.ts | 2 + .../guards/system/accessguards.module.ts | 10 + .../src/common/guards/system/accessguards.ts | 43 ++ dictation_server/src/common/token/types.ts | 14 + .../src/features/users/types/types.ts | 102 ++++ .../features/users/users.controller.spec.ts | 442 ++++++++++++++++++ .../src/features/users/users.controller.ts | 151 ++++++ 7 files changed, 764 insertions(+) create mode 100644 dictation_server/src/common/guards/system/accessguards.module.ts create mode 100644 dictation_server/src/common/guards/system/accessguards.ts diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 1157fa4..5d76aac 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -52,6 +52,7 @@ import { WorkflowsRepositoryModule } from './repositories/workflows/workflows.re import { TermsModule } from './features/terms/terms.module'; import { RedisModule } from './gateways/redis/redis.module'; import * as redisStore from 'cache-manager-redis-store'; +import { SystemAccessGuardsModule } from './common/guards/system/accessguards.module'; @Module({ imports: [ ServeStaticModule.forRootAsync({ @@ -133,6 +134,7 @@ import * as redisStore from 'cache-manager-redis-store'; NotificationhubModule, BlobstorageModule, AuthGuardsModule, + SystemAccessGuardsModule, SortCriteriaRepositoryModule, WorktypesRepositoryModule, TermsModule, diff --git a/dictation_server/src/common/guards/system/accessguards.module.ts b/dictation_server/src/common/guards/system/accessguards.module.ts new file mode 100644 index 0000000..43ac336 --- /dev/null +++ b/dictation_server/src/common/guards/system/accessguards.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SystemAccessGuard } from './accessguards'; + +@Module({ + imports: [ConfigModule], + controllers: [], + providers: [SystemAccessGuard], +}) +export class SystemAccessGuardsModule {} diff --git a/dictation_server/src/common/guards/system/accessguards.ts b/dictation_server/src/common/guards/system/accessguards.ts new file mode 100644 index 0000000..4aa11e2 --- /dev/null +++ b/dictation_server/src/common/guards/system/accessguards.ts @@ -0,0 +1,43 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { isVerifyError, decode, verify } from '../../jwt'; +import { Request } from 'express'; +import { retrieveAuthorizationToken } from '../../../common/http/helper'; +import { makeErrorResponse } from '../../../common/error/makeErrorResponse'; +import { SystemAccessToken } from '../../token/types'; +import { ConfigService } from '@nestjs/config'; +import { getPublicKey } from '../../jwt/jwt'; +/** + * システム間通信用のトークンを検証するガード + **/ +@Injectable() +export class SystemAccessGuard implements CanActivate { + constructor(private readonly configService: ConfigService) {} + + canActivate(context: ExecutionContext): boolean | Promise { + const pubkey = getPublicKey(this.configService); + const req = context.switchToHttp().getRequest(); + + const token = retrieveAuthorizationToken(req); + if (!token) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const payload = verify(token, pubkey); + if (isVerifyError(payload)) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + return true; + } +} diff --git a/dictation_server/src/common/token/types.ts b/dictation_server/src/common/token/types.ts index b602913..02f746d 100644 --- a/dictation_server/src/common/token/types.ts +++ b/dictation_server/src/common/token/types.ts @@ -36,6 +36,20 @@ export type AccessToken = { tier: number; }; +// システムの内部で発行し、外部に公開しないトークン +// システム間通信用(例: Azure Functions→AppService)に使用する +export type SystemAccessToken = { + /** + * トークンの発行者名(ログ記録用) + */ + systemName: string; + + /** + * 付加情報を 文字情報として格納できる + */ + context?: string; +}; + export type IDToken = { emails: string[]; nonce?: string | undefined; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 1c17c52..64896eb 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -1,11 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; import { + IsArray, IsBoolean, IsEmail, IsIn, IsInt, + IsNotEmpty, IsOptional, + IsString, MaxLength, + ValidateIf, + ValidateNested, } from 'class-validator'; import { TASK_LIST_SORTABLE_ATTRIBUTES, @@ -264,6 +269,103 @@ export class PostDeleteUserRequest { export class PostDeleteUserResponse {} +export class MultipleImportUser { + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @MaxLength(256) // AzureAdB2Cの仕様上、256文字まで[https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/user-profile-attributes] + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'メールアドレス' }) + @IsEmail({ blacklisted_chars: '*' }) + @IsNotEmpty() + email: string; + + @ApiProperty({ description: '0(none)/1(author)/2(typist)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1, 2]) + role: number; + + @ApiProperty({ required: false }) + @IsAuthorIdValid() + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + authorId?: string; + + @ApiProperty({ description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + autoRenew: number; + + @ApiProperty({ description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + notification: number; + + @ApiProperty({ required: false, description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + encryption?: number; + + @ApiProperty({ required: false }) + @IsPasswordvalid() + @IsNotEmpty() + @IsString() + @ValidateIf((o) => o.role === 1 && o.encryption === 1) // roleがauthorかつencryptionがtrueの場合のみバリデーションを実施 + encryptionPassword?: string; + + @ApiProperty({ required: false, description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + prompt?: number; +} + +export class PostMultipleImportsRequest { + @ApiProperty({ type: [MultipleImportUser] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MultipleImportUser) + users: MultipleImportUser[]; +} +export class PostMultipleImportsResponse {} + +export class MultipleImportErrors { + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'メールアドレス' }) + @IsEmail({ blacklisted_chars: '*' }) + @IsNotEmpty() + email: string; + + @ApiProperty({ description: 'エラーコード' }) + @IsString() + @IsNotEmpty() + errorCode: string; +} + +export class PostMultipleImportsCompleteRequest { + @ApiProperty({ description: 'アカウントID' }) + @Type(() => Number) + @IsInt() + accountId: number; + + @ApiProperty({ type: [MultipleImportErrors] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MultipleImportErrors) + errors: MultipleImportErrors[]; +} +export class PostMultipleImportsCompleteResponse {} + export class AllocateLicenseRequest { @ApiProperty({ description: 'ユーザーID' }) @Type(() => Number) diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index d060ac2..fa0bcd3 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -3,6 +3,12 @@ import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from '../auth/auth.service'; +import { + PostMultipleImportsCompleteRequest, + PostMultipleImportsRequest, +} from './types/types'; +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; describe('UsersController', () => { let controller: UsersController; @@ -32,4 +38,440 @@ describe('UsersController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation PostMultipleImportsRequest', () => { + it('role:noneの最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 0, + autoRenew: 0, + notification: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('role:authorの最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('emailがメールアドレスではない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge', + role: 0, + autoRenew: 0, + notification: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('AuthorなのにAuthorIDがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('Authorなのにencryptionがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('Authorなのにpromptがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('Authorでencryption:trueなのに、encryptionPasswordがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('Authorでencryption:trueでencryptionPasswordが正常であれば成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcd', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('encryptionPasswordが要件外(短い)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abc', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('encryptionPasswordが要件外(長い)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('encryptionPasswordが要件外(全角が含まれる)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcあいうえお', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('AuthorIDが要件外(小文字)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'author', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abc', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + }); + + describe('valdation PostMultipleImportsCompleteRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsCompleteRequest(); + request.accountId = 1; + request.errors = []; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('エラーが存在するリクエストが成功する', async () => { + const request = new PostMultipleImportsCompleteRequest(); + request.accountId = 1; + request.errors = [ + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ]; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('名前が足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('emailが足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('errorCodeが足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + email: 'hogehoge@example.com', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + + it('名前が空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: '', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('emailが空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + email: '', + errorCode: 'E1101', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + it('emailが空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + errors: [ + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: '', + }, + { + name: 'namae', + email: 'hogehoge@example.com', + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBeGreaterThan(0); + }); + }); }); diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 02f1c68..8d448d9 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -44,6 +44,10 @@ import { GetMyUserResponse, PostDeleteUserRequest, PostDeleteUserResponse, + PostMultipleImportsRequest, + PostMultipleImportsResponse, + PostMultipleImportsCompleteRequest, + PostMultipleImportsCompleteResponse, } from './types/types'; import { UsersService } from './users.service'; import { AuthService } from '../auth/auth.service'; @@ -57,6 +61,8 @@ import { ADMIN_ROLES, TIERS } from '../../constants'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log'; import { UserRoles } from '../../common/types/role'; +import { SystemAccessGuard } from '../../common/guards/system/accessguards'; +import { SystemAccessToken } from '../../common/token/types'; @ApiTags('users') @Controller('users') @@ -992,4 +998,149 @@ export class UsersController { await this.usersService.deleteUser(context, body.userId, now); return {}; } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostMultipleImportsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'multipleImports', + description: 'ユーザーを一括登録します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('multiple-imports') + async multipleImports( + @Body() body: PostMultipleImportsRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedToken = jwt.decode(accessToken, { json: true }); + if (!decodedToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, delegateUserId } = decodedToken as AccessToken; + const context = makeContext(userId, requestId, delegateUserId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: 処理を実装 + + return {}; + } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostMultipleImportsCompleteResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'multipleImportsComplate', + description: 'ユーザー一括登録の完了を通知します', + }) + @ApiBearerAuth() + @UseGuards(SystemAccessGuard) + @Post('multiple-imports/complete') + async multipleImportsComplate( + @Body() body: PostMultipleImportsCompleteRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedToken = jwt.decode(accessToken, { json: true }); + if (!decodedToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { systemName } = decodedToken as SystemAccessToken; + const context = makeContext(systemName, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // TODO: 処理を実装 + + return {}; + } }