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:
湯本 開 2023-03-29 03:55:44 +00:00
parent b4cd0208e6
commit 9f5252baf8
25 changed files with 1807 additions and 1056 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 '削除時刻',

View File

@ -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 '作成時刻',

File diff suppressed because it is too large Load Diff

View File

@ -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": [

View File

@ -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,

View 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;

View File

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

View File

@ -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],
})

View File

@ -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,
};
}
}

View File

@ -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;

View File

@ -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],
})

View File

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

View File

@ -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;

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

View 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;
}
}
}

View File

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

View File

@ -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 };
});
}
}

View File

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

View File

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

View File

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

View File

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