From dfd9abc1c3b60546c8d7779d9d66b9163e846704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Thu, 23 Mar 2023 07:56:18 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=2054:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88I/F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task1494: API実装(I/F)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1494) - POST /accounts のAPIを実装 - POST /users/confirm のAPIを実装 - 上記APIからopenapiを実装 ## レビューポイント - ラフスケッチ時から変更になった箇所があるが問題ないか - ディーラーIDは省略可能かつIDを指定するべきなのでnumber?に型を変更 - 管理者ユーザー用に同意済み利用規約バージョンを受け付けるようにした - reCAPTCHAを想定して事前にreCAPTCHA用トークンを受け付けるようにした ## 動作確認状況 - openapiが生成されることを確認 --- dictation_server/src/api/odms/openapi.json | 211 ++++++++++++------ dictation_server/src/app.module.ts | 17 +- .../accounts/accounts.controller.spec.ts | 18 ++ .../features/accounts/accounts.controller.ts | 36 +++ .../src/features/accounts/accounts.module.ts | 9 + .../accounts/accounts.service.spec.ts | 18 ++ .../src/features/accounts/accounts.service.ts | 4 + .../src/features/accounts/types/types.ts | 29 +++ .../src/features/auth/auth.module.ts | 2 +- .../src/features/users/types/types.ts | 8 + .../features/users/users.controller.spec.ts | 18 ++ .../src/features/users/users.controller.ts | 34 +++ .../src/features/users/users.module.ts | 9 + .../src/features/users/users.service.spec.ts | 18 ++ .../src/features/users/users.service.ts | 4 + dictation_server/src/main.ts | 2 - 16 files changed, 363 insertions(+), 74 deletions(-) create mode 100644 dictation_server/src/features/accounts/accounts.controller.spec.ts create mode 100644 dictation_server/src/features/accounts/accounts.controller.ts create mode 100644 dictation_server/src/features/accounts/accounts.module.ts create mode 100644 dictation_server/src/features/accounts/accounts.service.spec.ts create mode 100644 dictation_server/src/features/accounts/accounts.service.ts create mode 100644 dictation_server/src/features/accounts/types/types.ts create mode 100644 dictation_server/src/features/users/types/types.ts create mode 100644 dictation_server/src/features/users/users.controller.spec.ts create mode 100644 dictation_server/src/features/users/users.controller.ts create mode 100644 dictation_server/src/features/users/users.module.ts create mode 100644 dictation_server/src/features/users/users.service.spec.ts create mode 100644 dictation_server/src/features/users/users.service.ts diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index ce787bb..bb00d96 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -6,11 +6,7 @@ "operationId": "checkHealth", "summary": "", "parameters": [], - "responses": { - "200": { - "description": "" - } - } + "responses": { "200": { "description": "" } } } }, "/auth/token": { @@ -22,9 +18,7 @@ "required": true, "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/TokenRequest" - } + "schema": { "$ref": "#/components/schemas/TokenRequest" } } } }, @@ -33,9 +27,7 @@ "description": "成功時のレスポンス", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/TokenResponse" - } + "schema": { "$ref": "#/components/schemas/TokenResponse" } } } }, @@ -43,9 +35,7 @@ "description": "認証エラー", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, @@ -53,16 +43,12 @@ "description": "想定外のサーバーエラー", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, - "tags": [ - "auth" - ] + "tags": ["auth"] } }, "/auth/accessToken": { @@ -75,9 +61,7 @@ "description": "成功時のレスポンス", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessTokenResponse" - } + "schema": { "$ref": "#/components/schemas/AccessTokenResponse" } } } }, @@ -85,9 +69,7 @@ "description": "認証エラー", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, @@ -95,21 +77,99 @@ "description": "想定外のサーバーエラー", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } }, - "tags": [ - "auth" - ], - "security": [ - { - "bearer": [] + "tags": ["auth"], + "security": [{ "bearer": [] }] + } + }, + "/accounts": { + "post": { + "operationId": "createAccount", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateAccountRequest" } + } } - ] + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAccountResponse" + } + } + } + }, + "400": { + "description": "登録済みユーザーからの登録など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"] + } + }, + "/users/confirm": { + "post": { + "operationId": "confirmUser", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmResponse" } + } + } + }, + "400": { + "description": "不正なトークン", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"] } } }, @@ -134,60 +194,73 @@ "TokenRequest": { "type": "object", "properties": { - "idToken": { - "type": "string" - }, + "idToken": { "type": "string" }, "type": { "type": "string", "description": "web or mobile or desktop" } }, - "required": [ - "idToken", - "type" - ] + "required": ["idToken", "type"] }, "TokenResponse": { "type": "object", "properties": { - "refreshToken": { - "type": "string" - }, - "accessToken": { - "type": "string" - } + "refreshToken": { "type": "string" }, + "accessToken": { "type": "string" } }, - "required": [ - "refreshToken", - "accessToken" - ] + "required": ["refreshToken", "accessToken"] }, "ErrorResponse": { "type": "object", "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "message": { "type": "string" }, + "code": { "type": "string" } }, - "required": [ - "message", - "code" - ] + "required": ["message", "code"] }, "AccessTokenResponse": { + "type": "object", + "properties": { "accessToken": { "type": "string" } }, + "required": ["accessToken"] + }, + "CreateAccountRequest": { "type": "object", "properties": { - "accessToken": { - "type": "string" - } + "companyName": { "type": "string" }, + "country": { + "type": "string", + "description": "国名(ISO 3166-1 alpha-2)", + "minLength": 2, + "maxLength": 2 + }, + "dealerAccountId": { "type": "number", "nullable": true }, + "adminName": { "type": "string" }, + "adminMail": { "type": "string" }, + "adminPassword": { "type": "string" }, + "acceptedTermsVersion": { + "type": "string", + "description": "同意済み利用規約のバージョン" + }, + "token": { "type": "string", "description": "reCAPTCHA Token" } }, "required": [ - "accessToken" + "companyName", + "country", + "dealerAccountId", + "adminName", + "adminMail", + "adminPassword", + "acceptedTermsVersion", + "token" ] - } + }, + "CreateAccountResponse": { "type": "object", "properties": {} }, + "ConfirmRequest": { + "type": "object", + "properties": { "token": { "type": "string" } }, + "required": ["token"] + }, + "ConfirmResponse": { "type": "object", "properties": {} } } } -} \ No newline at end of file +} diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index a4da90b..e5c9997 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -9,6 +9,12 @@ import { AuthController } from './features/auth/auth.controller'; import { AuthService } from './features/auth/auth.service'; import { CryptoModule } from './gateways/crypto/crypto.module'; import { AdB2cModule } from './gateways/adb2c/adb2c.module'; +import { AccountsController } from './features/accounts/accounts.controller'; +import { AccountsService } from './features/accounts/accounts.service'; +import { AccountsModule } from './features/accounts/accounts.module'; +import { UsersController } from './features/users/users.controller'; +import { UsersService } from './features/users/users.service'; +import { UsersModule } from './features/users/users.module'; @Module({ imports: [ @@ -22,6 +28,8 @@ import { AdB2cModule } from './gateways/adb2c/adb2c.module'; AuthModule, CryptoModule, AdB2cModule, + AccountsModule, + UsersModule, // TypeOrmModule.forRootAsync({ // imports: [ConfigModule], // useFactory: async (configService: ConfigService) => ({ @@ -37,8 +45,13 @@ import { AdB2cModule } from './gateways/adb2c/adb2c.module'; // inject: [ConfigService], // }), ], - controllers: [HealthController, AuthController], - providers: [AuthService], + controllers: [ + HealthController, + AuthController, + AccountsController, + UsersController, + ], + providers: [AuthService, AccountsService, UsersService], }) export class AppModule { configure(consumer: MiddlewareConsumer) { diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts new file mode 100644 index 0000000..6308252 --- /dev/null +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AccountsController } from './accounts.controller'; + +describe('AccountsController', () => { + let controller: AccountsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AccountsController], + }).compile(); + + controller = module.get(AccountsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts new file mode 100644 index 0000000..e6aba51 --- /dev/null +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -0,0 +1,36 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ErrorResponse } from '../../common/error/types/types'; +import { AccountsService } from './accounts.service'; +import { CreateAccountRequest, CreateAccountResponse } from './types/types'; + +@ApiTags('accounts') +@Controller('accounts') +export class AccountsController { + constructor(private readonly accountService: AccountsService) {} + + @Post() + @ApiResponse({ + status: HttpStatus.OK, + type: CreateAccountResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '登録済みユーザーからの登録など', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'createAccount' }) + async createAccount( + @Body() body: CreateAccountRequest, + ): Promise { + console.log(JSON.stringify(body)); + + return {}; + } +} diff --git a/dictation_server/src/features/accounts/accounts.module.ts b/dictation_server/src/features/accounts/accounts.module.ts new file mode 100644 index 0000000..0e18141 --- /dev/null +++ b/dictation_server/src/features/accounts/accounts.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AccountsController } from './accounts.controller'; +import { AccountsService } from './accounts.service'; + +@Module({ + controllers: [AccountsController], + providers: [AccountsService], +}) +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 new file mode 100644 index 0000000..01304e2 --- /dev/null +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AccountsService } from './accounts.service'; + +describe('AccountsService', () => { + let service: AccountsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AccountsService], + }).compile(); + + service = module.get(AccountsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts new file mode 100644 index 0000000..83b76d6 --- /dev/null +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AccountsService {} diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts new file mode 100644 index 0000000..6981019 --- /dev/null +++ b/dictation_server/src/features/accounts/types/types.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsInt } from 'class-validator'; + +export class CreateAccountRequest { + @ApiProperty() + companyName: string; + @ApiProperty({ + description: '国名(ISO 3166-1 alpha-2)', + minLength: 2, + maxLength: 2, + }) + country: string; + @ApiProperty({ nullable: true }) + @IsInt() + dealerAccountId?: number; + @ApiProperty() + adminName: string; + @ApiProperty() + @IsEmail() + adminMail: string; + @ApiProperty() + adminPassword: string; + @ApiProperty({ description: '同意済み利用規約のバージョン' }) + acceptedTermsVersion: string; + @ApiProperty({ description: 'reCAPTCHA Token' }) + token: string; +} + +export class CreateAccountResponse {} diff --git a/dictation_server/src/features/auth/auth.module.ts b/dictation_server/src/features/auth/auth.module.ts index b50902d..a1a4135 100644 --- a/dictation_server/src/features/auth/auth.module.ts +++ b/dictation_server/src/features/auth/auth.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { AdB2cModule } from 'src/gateways/adb2c/adb2c.module'; +import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { CryptoModule } from '../../gateways/crypto/crypto.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts new file mode 100644 index 0000000..e638ba8 --- /dev/null +++ b/dictation_server/src/features/users/types/types.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ConfirmRequest { + @ApiProperty() + token: string; +} + +export class ConfirmResponse {} diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts new file mode 100644 index 0000000..3e27c39 --- /dev/null +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersController } from './users.controller'; + +describe('UsersController', () => { + let controller: UsersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + }).compile(); + + controller = module.get(UsersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts new file mode 100644 index 0000000..df9d7a5 --- /dev/null +++ b/dictation_server/src/features/users/users.controller.ts @@ -0,0 +1,34 @@ +import { Body, Controller, HttpStatus, Post } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ErrorResponse } from '../../common/error/types/types'; +import { ConfirmRequest, ConfirmResponse } from './types/types'; +import { UsersService } from './users.service'; + +@ApiTags('users') +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post('confirm') + @ApiResponse({ + status: HttpStatus.OK, + type: ConfirmResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なトークン', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'confirmUser' }) + async confirmUser(@Body() body: ConfirmRequest): Promise { + console.log(JSON.stringify(body)); + + return {}; + } +} diff --git a/dictation_server/src/features/users/users.module.ts b/dictation_server/src/features/users/users.module.ts new file mode 100644 index 0000000..440ef36 --- /dev/null +++ b/dictation_server/src/features/users/users.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +@Module({ + controllers: [UsersController], + providers: [UsersService], +}) +export class UsersModule {} diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts new file mode 100644 index 0000000..62815ba --- /dev/null +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UsersService } from './users.service'; + +describe('UsersService', () => { + let service: UsersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UsersService], + }).compile(); + + service = module.get(UsersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts new file mode 100644 index 0000000..ef0d82d --- /dev/null +++ b/dictation_server/src/features/users/users.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UsersService {} diff --git a/dictation_server/src/main.ts b/dictation_server/src/main.ts index 1611452..0dd1bb9 100644 --- a/dictation_server/src/main.ts +++ b/dictation_server/src/main.ts @@ -3,9 +3,7 @@ import cookieParser from 'cookie-parser'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; -import { LoggerMiddleware } from './common/loggerMiddleware'; import helmet from 'helmet'; -import crypto from 'crypto'; const helmetDirectives = helmet.contentSecurityPolicy.getDefaultDirectives(); helmetDirectives['connect-src'] = [ "'self'",