Merged PR 6: タスク 1362: API実装(I/F)

## 概要
[Task: 1362](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%202_2?workitem=1362)

- 以下のIFを実装
  - AzureADのidトークンを受け取り、アクセストークン・リフレッシュトークンを返却するAPI
  - リフレッシュトークンを受け取り、アクセストークンを返却するAPI
- エラー時のレスポンスを作成

## レビューポイント
- 各APIのリクエスト・レスポンスの型

## UIの変更
- なし

## 動作確認状況
- SwaggerUIでAPIを確認

## 補足
This commit is contained in:
斎藤 快斗 2023-02-24 06:01:33 +00:00
parent 3b4b3c59e7
commit c82d0363ac
13 changed files with 311 additions and 12 deletions

View File

@ -0,0 +1,2 @@
npx openapi-generator-cli version-manager set latest
npx openapi-generator-cli generate -g typescript-axios -i /app/dictation_server/src/api/odms/openapi.json -o /app/dictation_client/src/api/

View File

@ -2,6 +2,6 @@
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2, "spaces": 2,
"generator-cli": { "generator-cli": {
"version": "6.2.1" "version": "6.4.0"
} }
} }

View File

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
const SamplePage: React.FC = () => { const SamplePage: React.FC = () => (
return (<div>hello whorld!<br />Dictation App Service Site</div>) <div>
}; hello whorld!
<br />
Dictation App Service Site
</div>
);
export default SamplePage; export default SamplePage;

View File

@ -21,6 +21,12 @@ async function bootstrap(): Promise<void> {
const options = new DocumentBuilder() const options = new DocumentBuilder()
.setTitle('ODMSOpenAPI') .setTitle('ODMSOpenAPI')
.setVersion('1.0.0') .setVersion('1.0.0')
.addBearerAuth({
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
in: 'header',
})
.build(); .build();
const document = SwaggerModule.createDocument(app, options); const document = SwaggerModule.createDocument(app, options);
await fs.writeFile( await fs.writeFile(

View File

@ -12,10 +12,109 @@
} }
} }
} }
},
"/auth/token": {
"post": {
"operationId": "token",
"summary": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TokenRequest"
}
}
}
},
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TokenResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"tags": [
"auth"
]
}
},
"/auth/accessToken": {
"post": {
"operationId": "accessToken",
"summary": "",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AccessTokenResponse"
}
}
}
},
"401": {
"description": "認証エラー",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"tags": [
"auth"
],
"security": [
{
"bearer": []
}
]
}
} }
}, },
"info": { "info": {
"title": "OMDSOpenAPI", "title": "ODMSOpenAPI",
"description": "", "description": "",
"version": "1.0.0", "version": "1.0.0",
"contact": {} "contact": {}
@ -23,6 +122,72 @@
"tags": [], "tags": [],
"servers": [], "servers": [],
"components": { "components": {
"schemas": {} "securitySchemes": {
"bearer": {
"scheme": "bearer",
"bearerFormat": "JWT",
"type": "http",
"in": "header"
}
},
"schemas": {
"TokenRequest": {
"type": "object",
"properties": {
"idToken": {
"type": "string"
},
"type": {
"type": "string",
"description": "web or mobile or desktop"
}
},
"required": [
"idToken",
"type"
]
},
"TokenResponse": {
"type": "object",
"properties": {
"refreshToken": {
"type": "string"
},
"accessToken": {
"type": "string"
}
},
"required": [
"refreshToken",
"accessToken"
]
},
"ErrorResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"code": {
"type": "string"
}
},
"required": [
"message",
"code"
]
},
"AccessTokenResponse": {
"type": "object",
"properties": {
"accessToken": {
"type": "string"
}
},
"required": [
"accessToken"
]
}
}
} }
} }

View File

@ -1,11 +1,11 @@
import { MiddlewareConsumer, Module } from '@nestjs/common'; import { MiddlewareConsumer, Module } from '@nestjs/common';
import { HealthController } from './health.controller'; import { HealthController } from './health.controller';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { join } from 'path'; import { join } from 'path';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerMiddleware } from './common/loggerMiddleware'; import { LoggerMiddleware } from './common/loggerMiddleware';
import { AuthModule } from './features/auth/auth.module'; import { AuthModule } from './features/auth/auth.module';
import { AuthController } from './features/auth/auth.controller';
@Module({ @Module({
imports: [ imports: [
@ -32,7 +32,7 @@ import { AuthModule } from './features/auth/auth.module';
// inject: [ConfigService], // inject: [ConfigService],
// }), // }),
], ],
controllers: [HealthController], controllers: [HealthController, AuthController],
providers: [], providers: [],
}) })
export class AppModule { export class AppModule {

View File

@ -0,0 +1,4 @@
//TODO 仮のエラーコード作成
export const ErrorCodes = [
'E009999', // 汎用エラー
] as const;

View File

@ -0,0 +1,10 @@
import { errors } from './message';
import { ErrorCodeType, ErrorResponse } from './types/types';
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
const msg = errors[errorcode];
return {
code: errorcode,
message: msg,
};
};

View File

@ -0,0 +1,6 @@
import { Errors } from './types/types';
// エラーコードとメッセージ対応表
export const errors: Errors = {
E009999: 'Internal Server Error',
};

View File

@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { ErrorCodes } from '../code';
export class ErrorResponse {
@ApiProperty()
message: string;
@ApiProperty()
code: string;
}
export type ErrorCodeType = (typeof ErrorCodes)[number];
export type Errors = {
[P in ErrorCodeType]: string;
};

View File

@ -1,4 +1,66 @@
import { Controller } from '@nestjs/common'; import { Body, Controller, Headers, HttpStatus, Post } from '@nestjs/common';
import {
ApiResponse,
ApiOperation,
ApiBearerAuth,
ApiTags,
} from '@nestjs/swagger';
import { ErrorResponse } from '../../common/error/types/types';
import {
AccessTokenResponse,
TokenRequest,
TokenResponse,
} from './types/types';
@ApiTags('auth')
@Controller('auth') @Controller('auth')
export class AuthController {} export class AuthController {
@Post('token')
@ApiResponse({
status: HttpStatus.OK,
type: TokenResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'token' })
async token(@Body() body: TokenRequest): Promise<TokenResponse> {
console.log(body);
return {
accessToken: '',
refreshToken: '',
};
}
@Post('accessToken')
@ApiBearerAuth()
@ApiResponse({
status: HttpStatus.OK,
type: AccessTokenResponse,
description: '成功時のレスポンス',
})
@ApiResponse({
status: HttpStatus.UNAUTHORIZED,
description: '認証エラー',
type: ErrorResponse,
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: '想定外のサーバーエラー',
type: ErrorResponse,
})
@ApiOperation({ operationId: 'accessToken' })
async accessToken(@Headers() headers): Promise<AccessTokenResponse> {
console.log(headers['authorization']);
return { accessToken: '' };
}
}

View File

@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
export class TokenRequest {
@ApiProperty()
idToken: string;
@ApiProperty({ description: 'web or mobile or desktop' })
type: string;
}
export class TokenResponse {
@ApiProperty()
refreshToken: string;
@ApiProperty()
accessToken: string;
}
export class AccessTokenResponse {
@ApiProperty()
accessToken: string;
}
export class AccessTokenRequest {}

View File

@ -18,6 +18,12 @@ async function bootstrap() {
const options = new DocumentBuilder() const options = new DocumentBuilder()
.setTitle('ODMSOpenAPI') .setTitle('ODMSOpenAPI')
.setVersion('1.0.0') .setVersion('1.0.0')
.addBearerAuth({
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
in: 'header',
})
.build(); .build();
const document = SwaggerModule.createDocument(app, options); const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);