Merged PR 54: API実装(I/F)

## 概要
[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が生成されることを確認
This commit is contained in:
湯本 開 2023-03-23 07:56:18 +00:00
parent 6fe1cc4d6d
commit dfd9abc1c3
16 changed files with 363 additions and 74 deletions

View File

@ -6,11 +6,7 @@
"operationId": "checkHealth", "operationId": "checkHealth",
"summary": "", "summary": "",
"parameters": [], "parameters": [],
"responses": { "responses": { "200": { "description": "" } }
"200": {
"description": ""
}
}
} }
}, },
"/auth/token": { "/auth/token": {
@ -22,9 +18,7 @@
"required": true, "required": true,
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": { "$ref": "#/components/schemas/TokenRequest" }
"$ref": "#/components/schemas/TokenRequest"
}
} }
} }
}, },
@ -33,9 +27,7 @@
"description": "成功時のレスポンス", "description": "成功時のレスポンス",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": { "$ref": "#/components/schemas/TokenResponse" }
"$ref": "#/components/schemas/TokenResponse"
}
} }
} }
}, },
@ -43,9 +35,7 @@
"description": "認証エラー", "description": "認証エラー",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }
"$ref": "#/components/schemas/ErrorResponse"
}
} }
} }
}, },
@ -53,16 +43,12 @@
"description": "想定外のサーバーエラー", "description": "想定外のサーバーエラー",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }
"$ref": "#/components/schemas/ErrorResponse"
}
} }
} }
} }
}, },
"tags": [ "tags": ["auth"]
"auth"
]
} }
}, },
"/auth/accessToken": { "/auth/accessToken": {
@ -75,9 +61,7 @@
"description": "成功時のレスポンス", "description": "成功時のレスポンス",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": { "$ref": "#/components/schemas/AccessTokenResponse" }
"$ref": "#/components/schemas/AccessTokenResponse"
}
} }
} }
}, },
@ -85,9 +69,7 @@
"description": "認証エラー", "description": "認証エラー",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }
"$ref": "#/components/schemas/ErrorResponse"
}
} }
} }
}, },
@ -95,21 +77,99 @@
"description": "想定外のサーバーエラー", "description": "想定外のサーバーエラー",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }
"$ref": "#/components/schemas/ErrorResponse"
}
} }
} }
} }
}, },
"tags": [ "tags": ["auth"],
"auth" "security": [{ "bearer": [] }]
], }
"security": [ },
{ "/accounts": {
"bearer": [] "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": { "TokenRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"idToken": { "idToken": { "type": "string" },
"type": "string"
},
"type": { "type": {
"type": "string", "type": "string",
"description": "web or mobile or desktop" "description": "web or mobile or desktop"
} }
}, },
"required": [ "required": ["idToken", "type"]
"idToken",
"type"
]
}, },
"TokenResponse": { "TokenResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"refreshToken": { "refreshToken": { "type": "string" },
"type": "string" "accessToken": { "type": "string" }
},
"accessToken": {
"type": "string"
}
}, },
"required": [ "required": ["refreshToken", "accessToken"]
"refreshToken",
"accessToken"
]
}, },
"ErrorResponse": { "ErrorResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"message": { "message": { "type": "string" },
"type": "string" "code": { "type": "string" }
},
"code": {
"type": "string"
}
}, },
"required": [ "required": ["message", "code"]
"message",
"code"
]
}, },
"AccessTokenResponse": { "AccessTokenResponse": {
"type": "object",
"properties": { "accessToken": { "type": "string" } },
"required": ["accessToken"]
},
"CreateAccountRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"accessToken": { "companyName": { "type": "string" },
"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": [ "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": {} }
} }
} }
} }

View File

@ -9,6 +9,12 @@ import { AuthController } from './features/auth/auth.controller';
import { AuthService } from './features/auth/auth.service'; import { AuthService } from './features/auth/auth.service';
import { CryptoModule } from './gateways/crypto/crypto.module'; import { CryptoModule } from './gateways/crypto/crypto.module';
import { AdB2cModule } from './gateways/adb2c/adb2c.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({ @Module({
imports: [ imports: [
@ -22,6 +28,8 @@ import { AdB2cModule } from './gateways/adb2c/adb2c.module';
AuthModule, AuthModule,
CryptoModule, CryptoModule,
AdB2cModule, AdB2cModule,
AccountsModule,
UsersModule,
// TypeOrmModule.forRootAsync({ // TypeOrmModule.forRootAsync({
// imports: [ConfigModule], // imports: [ConfigModule],
// useFactory: async (configService: ConfigService) => ({ // useFactory: async (configService: ConfigService) => ({
@ -37,8 +45,13 @@ import { AdB2cModule } from './gateways/adb2c/adb2c.module';
// inject: [ConfigService], // inject: [ConfigService],
// }), // }),
], ],
controllers: [HealthController, AuthController], controllers: [
providers: [AuthService], HealthController,
AuthController,
AccountsController,
UsersController,
],
providers: [AuthService, AccountsService, UsersService],
}) })
export class AppModule { export class AppModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {

View File

@ -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>(AccountsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -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<CreateAccountResponse> {
console.log(JSON.stringify(body));
return {};
}
}

View File

@ -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 {}

View File

@ -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>(AccountsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AccountsService {}

View File

@ -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 {}

View File

@ -1,5 +1,5 @@
import { Module } from '@nestjs/common'; 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 { CryptoModule } from '../../gateways/crypto/crypto.module';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';

View File

@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
export class ConfirmRequest {
@ApiProperty()
token: string;
}
export class ConfirmResponse {}

View File

@ -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>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -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<ConfirmResponse> {
console.log(JSON.stringify(body));
return {};
}
}

View File

@ -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 {}

View File

@ -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>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {}

View File

@ -3,9 +3,7 @@ import cookieParser from 'cookie-parser';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { LoggerMiddleware } from './common/loggerMiddleware';
import helmet from 'helmet'; import helmet from 'helmet';
import crypto from 'crypto';
const helmetDirectives = helmet.contentSecurityPolicy.getDefaultDirectives(); const helmetDirectives = helmet.contentSecurityPolicy.getDefaultDirectives();
helmetDirectives['connect-src'] = [ helmetDirectives['connect-src'] = [
"'self'", "'self'",