Merged PR 56: API実装(アカウント登録)
## 概要 [Task1496: API実装(アカウント登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1496) - アカウント登録のAzureAD B2Cへのユーザー作成部分以外について実装 - migration SQLに足りない要素があった箇所を修正(default追加) - 各種環境変数を追加 - 秘密鍵/公開鍵を取得する方法を環境変数に変更(KeyVaultからWebAppsの機能で環境変数へ流し込む想定) ## レビューポイント - 実装方法として問題がありそうな箇所が存在しないか - 可読性の低い箇所が存在しないか - Moduleの分け方、つなげ方などは問題ないか - ラフスケッチと違い、Account作成と管理者ユーザー作成を同一トランザクションで行うよう修正したが問題ないか ## 動作確認状況 - メール送信以外はローカルで確認、メール送信部分は未確認
This commit is contained in:
parent
b4cd0208e6
commit
9f5252baf8
@ -32,6 +32,9 @@ RUN mkdir -p /tmp/gotools \
|
||||
&& mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \
|
||||
&& rm -rf /tmp/gotools
|
||||
|
||||
# Update NPM
|
||||
RUN npm install -g npm
|
||||
|
||||
# Install NestJS
|
||||
RUN npm i -g @nestjs/cli
|
||||
|
||||
|
||||
@ -8,6 +8,11 @@ DB_PASSWORD=omdsdbpass
|
||||
NO_COLOR=TRUE
|
||||
ACCESS_TOKEN_LIFETIME_WEB=1600000
|
||||
REFRESH_TOKEN_LIFETIME_WEB=86400000
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
|
||||
TENANT_NAME=adb2codmsdev
|
||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||
EMAIL_CONFIRM_LIFETIME=86400000
|
||||
JWT_PRIVATE_KEY=xxxxxxxxxxxxx
|
||||
JWT_PUBLIC_KEY=xxxxxxxxxxxxx
|
||||
SENDGRID_API_KEY=xxxxxxxxxxxxxxxx
|
||||
MAIL_FROM=xxxxx@xxxxx.xxxx
|
||||
@ -4,4 +4,7 @@ PORT=8081
|
||||
AZURE_TENANT_ID=xxxxxxxx
|
||||
AZURE_CLIENT_ID=xxxxxxxx
|
||||
AZURE_CLIENT_SECRET=xxxxxxxx
|
||||
KEY_VAULT_NAME=kv-odms-secret-dev
|
||||
KEY_VAULT_NAME=kv-odms-secret-dev
|
||||
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n"
|
||||
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
SENDGRID_API_KEY=xxxxxxxxxxxxxxxx
|
||||
@ -1,8 +1,8 @@
|
||||
local:
|
||||
dialect: mysql
|
||||
dir: /app/server/db/migrations
|
||||
dir: /app/dictation_server/db/migrations
|
||||
datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_ENDPOINT}:${DB_PORT})/${DB_NAME}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true
|
||||
ci:
|
||||
dialect: mysql
|
||||
dir: ./server/db/migrations
|
||||
dir: ./dictation_server/db/migrations
|
||||
datasource: DB_USERNAME:DB_PASS@tcp(WRITER_ENDPOINT:DB_PORT)/DB_NAME?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true
|
||||
|
||||
@ -4,9 +4,10 @@ CREATE TABLE IF NOT EXISTS `accounts` (
|
||||
`parent_account_id` BIGINT UNSIGNED COMMENT '親アカウントID',
|
||||
`tier` INT UNSIGNED NOT NULL COMMENT '商流における階層',
|
||||
`country` VARCHAR(16) NOT NULL COMMENT '国名(ISO 3166-1 alpha-2)',
|
||||
`delegation_permission` BOOLEAN NOT NULL COMMENT '上位階層からの代行操作を許可しているか',
|
||||
`loacked` BOOLEAN NOT NULL COMMENT 'アカウントがロック済みであるか',
|
||||
`delegation_permission` BOOLEAN NOT NULL DEFAULT 0 COMMENT '上位階層からの代行操作を許可しているか',
|
||||
`locked` BOOLEAN NOT NULL DEFAULT 0 COMMENT 'アカウントがロック済みであるか',
|
||||
`company_name` VARCHAR(255) NOT NULL COMMENT '会社名',
|
||||
`verified` BOOLEAN NOT NULL DEFAULT 0 COMMENT 'email認証が完了済みであるか',
|
||||
`primary_admin_user_id` BIGINT UNSIGNED COMMENT 'プライマリ管理者ユーザーID',
|
||||
`secondary_admin_user_id` BIGINT UNSIGNED COMMENT 'セカンダリ管理者ユーザーID',
|
||||
`deleted_at` TIMESTAMP COMMENT '削除時刻',
|
||||
|
||||
@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
||||
`role` VARCHAR(255) NOT NULL COMMENT '役職',
|
||||
`author_id` VARCHAR(255) COMMENT 'AuthorID',
|
||||
`accepted_terms_version` VARCHAR(255) NOT NULL COMMENT '同意済み利用規約バージョン',
|
||||
`email_verified` BOOLEAN NOT NULL DEFAULT 0 COMMENT 'email認証が完了済みであるか',
|
||||
`deleted_at` TIMESTAMP COMMENT '削除時刻',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
|
||||
2197
dictation_server/package-lock.json
generated
2197
dictation_server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,7 @@
|
||||
"@nestjs/serve-static": "^2.2.2",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"@openapitools/openapi-generator-cli": "^2.5.1",
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"axios": "^1.3.4",
|
||||
@ -83,7 +84,7 @@
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "^4.3.5"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { join } from 'path';
|
||||
import { LoggerMiddleware } from './common/loggerMiddleware';
|
||||
import { AuthModule } from './features/auth/auth.module';
|
||||
@ -15,6 +15,10 @@ 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';
|
||||
import { AccountsRepositoryModule } from './repositories/accounts/accounts.repository.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SendGridModule } from './gateways/sendgrid/sendgrid.module';
|
||||
import { UsersRepositoryModule } from './repositories/users/users.repository.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -30,20 +34,23 @@ import { UsersModule } from './features/users/users.module';
|
||||
AdB2cModule,
|
||||
AccountsModule,
|
||||
UsersModule,
|
||||
// TypeOrmModule.forRootAsync({
|
||||
// imports: [ConfigModule],
|
||||
// useFactory: async (configService: ConfigService) => ({
|
||||
// type: 'mysql',
|
||||
// host: configService.get('DB_ENDPOINT'),
|
||||
// port: configService.get('DB_PORT'),
|
||||
// username: configService.get('DB_USERNAME'),
|
||||
// password: configService.get('DB_PASSWORD'),
|
||||
// database: configService.get('DB_NAME'),
|
||||
// autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード
|
||||
// synchronize: false, // trueにすると自動的にmigrationが行われるため注意
|
||||
// }),
|
||||
// inject: [ConfigService],
|
||||
// }),
|
||||
SendGridModule,
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: configService.get('DB_ENDPOINT'),
|
||||
port: configService.get('DB_PORT'),
|
||||
username: configService.get('DB_USERNAME'),
|
||||
password: configService.get('DB_PASSWORD'),
|
||||
database: configService.get('DB_NAME'),
|
||||
autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード
|
||||
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
controllers: [
|
||||
HealthController,
|
||||
|
||||
29
dictation_server/src/constants/index.ts
Normal file
29
dictation_server/src/constants/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* OMDS東京
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIER_1 = 1;
|
||||
|
||||
/**
|
||||
* OMDS現地法人
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIER_2 = 2;
|
||||
|
||||
/**
|
||||
* 代理店
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIER_3 = 3;
|
||||
|
||||
/**
|
||||
* 販売店
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIER_4 = 4;
|
||||
|
||||
/**
|
||||
* エンドユーザー
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIER_5 = 5;
|
||||
@ -29,7 +29,27 @@ export class AccountsController {
|
||||
async createAccount(
|
||||
@Body() body: CreateAccountRequest,
|
||||
): Promise<CreateAccountResponse> {
|
||||
console.log(JSON.stringify(body));
|
||||
const {
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
adminMail,
|
||||
adminPassword,
|
||||
adminName,
|
||||
acceptedTermsVersion,
|
||||
} = body;
|
||||
const role = 'none';
|
||||
|
||||
const { accountId, userId } = await this.accountService.createAccount(
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
adminMail,
|
||||
adminPassword,
|
||||
adminName,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
|
||||
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
|
||||
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
|
||||
import { AccountsController } from './accounts.controller';
|
||||
import { AccountsService } from './accounts.service';
|
||||
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
SendGridModule,
|
||||
AdB2cModule,
|
||||
],
|
||||
controllers: [AccountsController],
|
||||
providers: [AccountsService],
|
||||
})
|
||||
|
||||
@ -1,4 +1,116 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
|
||||
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
import { TIER_5 } from '../../constants';
|
||||
import { makeErrorResponse } from 'src/common/error/makeErrorResponse';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsService {}
|
||||
export class AccountsService {
|
||||
constructor(
|
||||
private readonly accountRepository: AccountsRepositoryService,
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly sendgridService: SendGridService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* アカウント情報をDBに作成する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param [dealerAccountId]
|
||||
* @returns account
|
||||
*/
|
||||
async createAccount(
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | null,
|
||||
email: string,
|
||||
password: string,
|
||||
username: string,
|
||||
role: string,
|
||||
acceptedTermsVersion: string,
|
||||
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
|
||||
let externalUser: { sub: string };
|
||||
try {
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log('create externalUser failed');
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
let account: Account;
|
||||
let user: User;
|
||||
try {
|
||||
// アカウントと管理者をセットで作成
|
||||
const { newAccount, adminUser } =
|
||||
await this.accountRepository.createAccount(
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
TIER_5,
|
||||
externalUser.sub,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
} catch (e) {
|
||||
console.log('create account failed');
|
||||
console.log(
|
||||
`[NOT IMPLEMENT] [RECOVER] delete account: ${externalUser.sub}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// メールの送信元を取得
|
||||
const from = this.configService.get<string>('MAIL_FROM') ?? '';
|
||||
|
||||
// メールの内容を構成
|
||||
const { subject, text, html } =
|
||||
await this.sendgridService.createMailContentFromEmailConfirm(
|
||||
account.id,
|
||||
user.id,
|
||||
email,
|
||||
);
|
||||
|
||||
// メールを送信
|
||||
await this.sendgridService.sendMail(email, from, subject, text, html);
|
||||
} catch (e) {
|
||||
console.log('create user failed');
|
||||
console.log(`[NOT IMPLEMENT] [RECOVER] delete account: ${account.id}`);
|
||||
console.log(
|
||||
`[NOT IMPLEMENT] [RECOVER] delete externalUser: ${externalUser.sub}`,
|
||||
);
|
||||
console.log(`[NOT IMPLEMENT] [RECOVER] delete user: ${user.id}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
accountId: account.id,
|
||||
userId: user.id,
|
||||
externalUserId: user.external_id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsInt } from 'class-validator';
|
||||
import { IsEmail, IsInt, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateAccountRequest {
|
||||
@ApiProperty()
|
||||
@ -12,6 +12,7 @@ export class CreateAccountRequest {
|
||||
country: string;
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
dealerAccountId?: number;
|
||||
@ApiProperty()
|
||||
adminName: string;
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CryptoModule } from '../../gateways/crypto/crypto.module';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [CryptoModule],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
})
|
||||
|
||||
@ -11,6 +11,18 @@ export class AdB2cService {
|
||||
private readonly flowName =
|
||||
this.configService.get<string>('SIGNIN_FLOW_NAME');
|
||||
|
||||
async createUser(
|
||||
email: string,
|
||||
password: string,
|
||||
username: string,
|
||||
): Promise<{ sub: string }> {
|
||||
// XXX GraphAPIの呼び出しを行う
|
||||
console.log(email);
|
||||
console.log(password);
|
||||
console.log(username);
|
||||
return { sub: 'xxxxxxxx' };
|
||||
}
|
||||
|
||||
/**
|
||||
* ADB2Cのメタデータを取得する
|
||||
* @returns meta data
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { DefaultAzureCredential } from '@azure/identity';
|
||||
import { SecretClient } from '@azure/keyvault-secrets';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import jwkToPem from 'jwk-to-pem';
|
||||
@ -11,21 +10,7 @@ export class CryptoService {
|
||||
private readonly credential: DefaultAzureCredential;
|
||||
private readonly url: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
// DefaultAzureCredentialが内部で直接的に環境変数を読むので、明示的にセットする
|
||||
process.env.AZURE_TENANT_ID = configService.get('AZURE_TENANT_ID');
|
||||
process.env.AZURE_CLIENT_ID = configService.get('AZURE_CLIENT_ID');
|
||||
process.env.AZURE_CLIENT_SECRET = configService.get('AZURE_CLIENT_SECRET');
|
||||
|
||||
// If you're using MSI, DefaultAzureCredential should "just work".
|
||||
// Otherwise, DefaultAzureCredential expects the following three environment variables:
|
||||
// - AZURE_TENANT_ID: The tenant ID in Azure Active Directory
|
||||
// - AZURE_CLIENT_ID: The application (client) ID registered in the AAD tenant
|
||||
// - AZURE_CLIENT_SECRET: The client secret for the registered application
|
||||
this.credential = new DefaultAzureCredential();
|
||||
this.url =
|
||||
'https://' + configService.get('KEY_VAULT_NAME') + '.vault.azure.net';
|
||||
}
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* Gets private key
|
||||
@ -33,13 +18,13 @@ export class CryptoService {
|
||||
*/
|
||||
async getPrivateKey(): Promise<string> {
|
||||
try {
|
||||
const client = new SecretClient(this.url, this.credential);
|
||||
const secret = await client.getSecret('token-private-key');
|
||||
if (secret.value) {
|
||||
return secret.value;
|
||||
} else {
|
||||
throw new Error('private key was empty');
|
||||
const key = this.configService.get<string>('JWT_PRIVATE_KEY');
|
||||
if (key) {
|
||||
// 開発環境用に改行コードを置換する
|
||||
// 本番環境では\\nが含まれないため、置換が行われない想定
|
||||
return key.replace('\\n', '\n');
|
||||
}
|
||||
throw new Error(`JWT_PRIVATE_KEY not found.`);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
@ -54,13 +39,13 @@ export class CryptoService {
|
||||
*/
|
||||
async getPublicKey(): Promise<string> {
|
||||
try {
|
||||
const client = new SecretClient(this.url, this.credential);
|
||||
const secret = await client.getSecret('token-public-key');
|
||||
if (secret.value) {
|
||||
return secret.value;
|
||||
} else {
|
||||
throw new Error('public key was empty');
|
||||
const key = this.configService.get<string>('JWT_PUBLIC_KEY');
|
||||
if (key) {
|
||||
// 開発環境用に改行コードを置換する
|
||||
// 本番環境では\\nが含まれないため、置換が行われない想定
|
||||
return key.replace('\\n', '\n');
|
||||
}
|
||||
throw new Error(`JWT_PUBLIC_KEY not found.`);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
|
||||
10
dictation_server/src/gateways/sendgrid/sendgrid.module.ts
Normal file
10
dictation_server/src/gateways/sendgrid/sendgrid.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { SendGridService } from './sendgrid.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
exports: [SendGridService],
|
||||
providers: [SendGridService],
|
||||
})
|
||||
export class SendGridModule {}
|
||||
85
dictation_server/src/gateways/sendgrid/sendgrid.service.ts
Normal file
85
dictation_server/src/gateways/sendgrid/sendgrid.service.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { sign } from '../../common/jwt';
|
||||
import sendgrid from '@sendgrid/mail';
|
||||
|
||||
@Injectable()
|
||||
export class SendGridService {
|
||||
private readonly logger = new Logger(SendGridService.name);
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const key = this.configService.get<string>('SENDGRID_API_KEY');
|
||||
sendgrid.setApiKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Email認証用のメールコンテンツを作成する
|
||||
* @param accountId 認証対象のユーザーが所属するアカウントのID
|
||||
* @param userId 認証対象のユーザーのID
|
||||
* @param email 認証対象のユーザーのメールアドレス
|
||||
* @returns メールのサブジェクトとコンテンツ
|
||||
*/
|
||||
async createMailContentFromEmailConfirm(
|
||||
accountId: number,
|
||||
userId: number,
|
||||
email: string,
|
||||
): Promise<{ subject: string; text: string; html: string }> {
|
||||
const lifetime =
|
||||
this.configService.get<number>('EMAIL_CONFIRM_LIFETIME') ?? 0;
|
||||
const privateKey =
|
||||
this.configService.get<string>('JWT_PRIVATE_KEY')?.replace('\\n', '\n') ??
|
||||
'';
|
||||
const token = sign<{ accountId: number; userId: number; email: string }>(
|
||||
{
|
||||
accountId,
|
||||
userId,
|
||||
email,
|
||||
},
|
||||
lifetime,
|
||||
privateKey,
|
||||
);
|
||||
const domains = 'http://127.0.0.1/';
|
||||
const path = '';
|
||||
|
||||
return {
|
||||
subject: 'Verify your new account',
|
||||
text: `The verification URL. ${domains}${path}?verify=${token}`,
|
||||
html: `<p>The verification URL.<p><a href="${domains}${path}?verify=${token}">${domains}${path}?verify=${token}"</a>`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* メールを送信する
|
||||
* @param accountId アカウントID
|
||||
* @param userId ユーザーID
|
||||
* @returns user confirm token
|
||||
*/
|
||||
async sendMail(
|
||||
to: string,
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const res = await sendgrid
|
||||
.send({
|
||||
from: {
|
||||
email: from,
|
||||
},
|
||||
to: {
|
||||
email: to,
|
||||
},
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
})
|
||||
.then((v) => v[0]);
|
||||
this.logger.log(
|
||||
`status code: ${res.statusCode} body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(JSON.stringify(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Account } from './entity/account.entity';
|
||||
import { AccountsRepositoryService } from './accounts.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Account])],
|
||||
providers: [AccountsRepositoryService],
|
||||
exports: [AccountsRepositoryService],
|
||||
})
|
||||
export class AccountsRepositoryModule {}
|
||||
@ -0,0 +1,115 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, UpdateResult } from 'typeorm';
|
||||
import { User } from '../users/entity/user.entity';
|
||||
import { Account } from './entity/account.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
* 管理ユーザー無しでアカウントを作成する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param dealerAccountId
|
||||
* @param tier
|
||||
* @returns create
|
||||
*/
|
||||
async create(
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | null,
|
||||
tier: number,
|
||||
): Promise<Account> {
|
||||
const account = new Account();
|
||||
{
|
||||
account.parent_account_id = dealerAccountId;
|
||||
account.company_name = companyName;
|
||||
account.country = country;
|
||||
account.tier = tier;
|
||||
}
|
||||
|
||||
const createdEntity = await this.dataSource.transaction(
|
||||
async (entityManager) => {
|
||||
const repo = entityManager.getRepository(Account);
|
||||
const newAccount = repo.create(account);
|
||||
const persisted = await repo.save(newAccount);
|
||||
return persisted;
|
||||
},
|
||||
);
|
||||
return createdEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特定の情報でアカウントを更新する
|
||||
* @param account
|
||||
* @returns update
|
||||
*/
|
||||
async update(account: Account): Promise<UpdateResult> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const repo = entityManager.getRepository(Account);
|
||||
return await repo.update({ id: account.id }, account);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* プライマリ管理者とアカウントを同時に作成する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param dealerAccountId
|
||||
* @param tier
|
||||
* @param adminExternalUserId
|
||||
* @param adminUserRole
|
||||
* @param adminUserAcceptedTermsVersion
|
||||
* @returns account/admin user
|
||||
*/
|
||||
async createAccount(
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | null,
|
||||
tier: number,
|
||||
adminExternalUserId: string,
|
||||
adminUserRole: string,
|
||||
adminUserAcceptedTermsVersion: string,
|
||||
): Promise<{ newAccount: Account; adminUser: User }> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const account = new Account();
|
||||
{
|
||||
account.parent_account_id = dealerAccountId;
|
||||
account.company_name = companyName;
|
||||
account.country = country;
|
||||
account.tier = tier;
|
||||
}
|
||||
const accountsRepo = entityManager.getRepository(Account);
|
||||
const newAccount = accountsRepo.create(account);
|
||||
const persistedAccount = await accountsRepo.save(newAccount);
|
||||
|
||||
// 作成されたAccountのIDを使用してユーザーを作成
|
||||
const user = new User();
|
||||
{
|
||||
user.account_id = persistedAccount.id;
|
||||
user.external_id = adminExternalUserId;
|
||||
user.role = adminUserRole;
|
||||
user.accepted_terms_version = adminUserAcceptedTermsVersion;
|
||||
}
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const newUser = usersRepo.create(user);
|
||||
const persistedUser = await usersRepo.save(newUser);
|
||||
|
||||
// アカウントに管理者を設定して更新
|
||||
persistedAccount.primary_admin_user_id = persistedUser.id;
|
||||
|
||||
const result = await accountsRepo.update(
|
||||
{ id: persistedAccount.id },
|
||||
persistedAccount,
|
||||
);
|
||||
|
||||
// 想定外の更新が行われた場合はロールバックを行った上でエラー送出
|
||||
if (result.affected !== 1) {
|
||||
throw new Error(`invalid update. result.affected=${result.affected}`);
|
||||
}
|
||||
|
||||
return { newAccount: persistedAccount, adminUser: persistedUser };
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'accounts' })
|
||||
export class Account {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
parent_account_id?: number;
|
||||
|
||||
@Column()
|
||||
tier: number;
|
||||
|
||||
@Column()
|
||||
country: string;
|
||||
|
||||
@Column()
|
||||
delegation_permission: boolean;
|
||||
|
||||
@Column()
|
||||
locked: boolean;
|
||||
|
||||
@Column()
|
||||
company_name: string;
|
||||
|
||||
@Column()
|
||||
verified: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
primary_admin_user_id?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
secondary_admin_user_id?: number;
|
||||
|
||||
@Column('timestamp')
|
||||
deleted_at?: Date;
|
||||
|
||||
@Column()
|
||||
created_by: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@Column()
|
||||
updated_by: string;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'users' })
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
external_id: string;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
role: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
author_id?: string;
|
||||
|
||||
@Column()
|
||||
accepted_terms_version: string;
|
||||
|
||||
@Column()
|
||||
email_verified: boolean;
|
||||
|
||||
@Column('timestamp', { nullable: true })
|
||||
deleted_at?: Date;
|
||||
|
||||
@Column()
|
||||
created_by: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@Column()
|
||||
updated_by: string;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entity/user.entity';
|
||||
import { UsersRepositoryService } from './users.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
providers: [UsersRepositoryService],
|
||||
exports: [UsersRepositoryService],
|
||||
})
|
||||
export class UsersRepositoryModule {}
|
||||
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from './entity/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
async create(
|
||||
accountId: number,
|
||||
externalUserId: string,
|
||||
role: string,
|
||||
acceptedTermsVersion: string,
|
||||
): Promise<User> {
|
||||
const user = new User();
|
||||
{
|
||||
user.account_id = accountId;
|
||||
user.external_id = externalUserId;
|
||||
user.role = role;
|
||||
user.accepted_terms_version = acceptedTermsVersion;
|
||||
}
|
||||
|
||||
const createdEntity = await this.dataSource.transaction(
|
||||
async (entityManager) => {
|
||||
const repo = entityManager.getRepository(User);
|
||||
const newUser = repo.create(user);
|
||||
const persisted = await repo.save(newUser);
|
||||
return persisted;
|
||||
},
|
||||
);
|
||||
return createdEntity;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user