Merge branch 'develop' into ccb
This commit is contained in:
commit
a1b59de44d
63
data_migration_tools/package-lock.json
generated
Normal file
63
data_migration_tools/package-lock.json
generated
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "data_migration_tools",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"author": "",
|
||||
"description": "",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"packages": {},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.2.3",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "28.0.3",
|
||||
"license-checker": "^25.0.1",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"swagger-ui-express": "^4.5.0",
|
||||
"ts-jest": "28.0.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nest build && cp -r build dist",
|
||||
"build:exe": "nest build && cp -r build dist && sh ./buildTool.sh",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"prebuild": "rimraf dist",
|
||||
"start": "nest start",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"test": "jest",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:watch": "jest --watch"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
STAGE=local
|
||||
NO_COLOR=TRUE
|
||||
CORS=TRUE
|
||||
PORT=8280
|
||||
# 開発環境ではADB2Cが別テナントになる都合上、環境変数を分けている
|
||||
TENANT_NAME=adb2codmsdev
|
||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||
ADB2C_TENANT_ID=xxxxxxxx
|
||||
ADB2C_CLIENT_ID=xxxxxxxx
|
||||
ADB2C_CLIENT_SECRET=xxxxxxxx
|
||||
ADB2C_ORIGIN=https://zzzzzzzzzz
|
||||
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
|
||||
MAIL_FROM=xxxxx@xxxxx.xxxx
|
||||
NOTIFICATION_HUB_NAME=ntf-odms-dev
|
||||
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
|
||||
APP_DOMAIN=http://localhost:8081/
|
||||
STORAGE_TOKEN_EXPIRE_TIME=2
|
||||
STORAGE_ACCOUNT_NAME_US=saodmsusdev
|
||||
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
|
||||
STORAGE_ACCOUNT_NAME_EU=saodmseudev
|
||||
STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
|
||||
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
|
||||
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
|
||||
ACCESS_TOKEN_LIFETIME_WEB=7200
|
||||
REFRESH_TOKEN_LIFETIME_WEB=86400
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000
|
||||
EMAIL_CONFIRM_LIFETIME=86400
|
||||
REDIS_HOST=redis-cache
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=omdsredispass
|
||||
ADB2C_CACHE_TTL=86400
|
||||
521
data_migration_tools/server/package-lock.json
generated
521
data_migration_tools/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -41,10 +41,11 @@
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.0",
|
||||
"swagger-cli": "^4.0.4"
|
||||
"swagger-cli": "^4.0.4",
|
||||
"typeorm": "^0.3.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
|
||||
@ -1,16 +1,32 @@
|
||||
import { MiddlewareConsumer, Module } from "@nestjs/common";
|
||||
import { ServeStaticModule } from "@nestjs/serve-static";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { join } from "path";
|
||||
import { LoggerMiddleware } from "./common/loggerMiddleware";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { DeleteModule } from "./features/delete/delete.module";
|
||||
import { AdB2cModule } from "./gateways/adb2c/adb2c.module";
|
||||
import { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module";
|
||||
import { RegisterController } from "./features/register/register.controller";
|
||||
import { RegisterService } from "./features/register/register.service";
|
||||
import { RegisterModule } from "./features/register/register.module";
|
||||
import { AccountsRepositoryModule } from "./repositories/accounts/accounts.repository.module";
|
||||
import { UsersRepositoryModule } from "./repositories/users/users.repository.module";
|
||||
import { SortCriteriaRepositoryModule } from "./repositories/sort_criteria/sort_criteria.repository.module";
|
||||
import { LicensesRepositoryModule } from "./repositories/licenses/licenses.repository.module";
|
||||
import { WorktypesRepositoryModule } from "./repositories/worktypes/worktypes.repository.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";
|
||||
import { DeleteModule } from "./features/delete/delete.module";
|
||||
import { DeleteRepositoryModule } from "./repositories/delete/delete.repository.module";
|
||||
import { DeleteController } from "./features/delete/delete.controller";
|
||||
import { DeleteService } from "./features/delete/delete.service";
|
||||
import { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module";
|
||||
|
||||
import { TransferModule } from "./features/transfer/transfer.module";
|
||||
import { TransferController } from "./features/transfer/transfer.controller";
|
||||
import { TransferService } from "./features/transfer/transfer.service";
|
||||
@Module({
|
||||
imports: [
|
||||
ServeStaticModule.forRoot({
|
||||
@ -20,6 +36,19 @@ import { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module";
|
||||
envFilePath: [".env.local", ".env"],
|
||||
isGlobal: true,
|
||||
}),
|
||||
AdB2cModule,
|
||||
AccountsModule,
|
||||
UsersModule,
|
||||
TransferModule,
|
||||
RegisterModule,
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
LicensesRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
BlobstorageModule,
|
||||
DeleteModule,
|
||||
DeleteRepositoryModule,
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
@ -34,13 +63,21 @@ import { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module";
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
DeleteModule,
|
||||
AdB2cModule,
|
||||
BlobstorageModule,
|
||||
DeleteRepositoryModule,
|
||||
],
|
||||
controllers: [DeleteController],
|
||||
providers: [DeleteService],
|
||||
controllers: [
|
||||
RegisterController,
|
||||
AccountsController,
|
||||
UsersController,
|
||||
DeleteController,
|
||||
TransferController,
|
||||
],
|
||||
providers: [
|
||||
RegisterService,
|
||||
AccountsService,
|
||||
UsersService,
|
||||
DeleteService,
|
||||
TransferService,
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
|
||||
70
data_migration_tools/server/src/common/error/code.ts
Normal file
70
data_migration_tools/server/src/common/error/code.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
エラーコード作成方針
|
||||
E+6桁(数字)で構成する。
|
||||
- 1~2桁目の値は種類(業務エラー、システムエラー...)
|
||||
- 3~4桁目の値は原因箇所(トークン、DB、...)
|
||||
- 5~6桁目の値は任意の重複しない値
|
||||
ex)
|
||||
E00XXXX : システムエラー(通信エラーやDB接続失敗など)
|
||||
E01XXXX : 業務エラー
|
||||
EXX00XX : 内部エラー(内部プログラムのエラー)
|
||||
EXX01XX : トークンエラー(トークン認証関連)
|
||||
EXX02XX : DBエラー(DB関連)
|
||||
EXX03XX : ADB2Cエラー(DB関連)
|
||||
*/
|
||||
export const ErrorCodes = [
|
||||
'E009999', // 汎用エラー
|
||||
'E000101', // トークン形式不正エラー
|
||||
'E000102', // トークン有効期限切れエラー
|
||||
'E000103', // トークン非アクティブエラー
|
||||
'E000104', // トークン署名エラー
|
||||
'E000105', // トークン発行元エラー
|
||||
'E000106', // トークンアルゴリズムエラー
|
||||
'E000107', // トークン不足エラー
|
||||
'E000108', // トークン権限エラー
|
||||
'E000301', // ADB2Cへのリクエスト上限超過エラー
|
||||
'E000401', // IPアドレス未設定エラー
|
||||
'E000501', // リクエストID未設定エラー
|
||||
'E010001', // パラメータ形式不正エラー
|
||||
'E010201', // 未認証ユーザエラー
|
||||
'E010202', // 認証済ユーザエラー
|
||||
'E010203', // 管理ユーザ権限エラー
|
||||
'E010204', // ユーザ不在エラー
|
||||
'E010205', // DBのRoleが想定外の値エラー
|
||||
'E010206', // DBのTierが想定外の値エラー
|
||||
'E010207', // ユーザーのRole変更不可エラー
|
||||
'E010208', // ユーザーの暗号化パスワード不足エラー
|
||||
'E010209', // ユーザーの同意済み利用規約バージョンが最新でないエラー
|
||||
'E010301', // メールアドレス登録済みエラー
|
||||
'E010302', // authorId重複エラー
|
||||
'E010401', // PONumber重複エラー
|
||||
'E010501', // アカウント不在エラー
|
||||
'E010502', // アカウント情報変更不可エラー
|
||||
'E010503', // 代行操作不許可エラー
|
||||
'E010504', // アカウントロックエラー
|
||||
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||
'E010602', // タスク変更権限不足エラー
|
||||
'E010603', // タスク不在エラー
|
||||
'E010701', // Blobファイル不在エラー
|
||||
'E010801', // ライセンス不在エラー
|
||||
'E010802', // ライセンス取り込み済みエラー
|
||||
'E010803', // ライセンス発行済みエラー
|
||||
'E010804', // ライセンス不足エラー
|
||||
'E010805', // ライセンス有効期限切れエラー
|
||||
'E010806', // ライセンス割り当て不可エラー
|
||||
'E010807', // ライセンス割り当て解除済みエラー
|
||||
'E010808', // ライセンス注文キャンセル不可エラー
|
||||
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
|
||||
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
|
||||
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
|
||||
'E010812', // ライセンス未割当エラー
|
||||
'E010908', // タイピストグループ不在エラー
|
||||
'E010909', // タイピストグループ名重複エラー
|
||||
'E011001', // ワークタイプ重複エラー
|
||||
'E011002', // ワークタイプ登録上限超過エラー
|
||||
'E011003', // ワークタイプ不在エラー
|
||||
'E011004', // ワークタイプ使用中エラー
|
||||
'E012001', // テンプレートファイル不在エラー
|
||||
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
'E013002', // ワークフロー不在エラー
|
||||
] as const;
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
59
data_migration_tools/server/src/common/error/message.ts
Normal file
59
data_migration_tools/server/src/common/error/message.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Errors } from './types/types';
|
||||
|
||||
// エラーコードとメッセージ対応表
|
||||
export const errors: Errors = {
|
||||
E009999: 'Internal Server Error.',
|
||||
E000101: 'Token invalid format Error.',
|
||||
E000102: 'Token expired Error.',
|
||||
E000103: 'Token not before Error',
|
||||
E000104: 'Token invalid signature Error.',
|
||||
E000105: 'Token invalid issuer Error.',
|
||||
E000106: 'Token invalid algorithm Error.',
|
||||
E000107: 'Token is not exist Error.',
|
||||
E000108: 'Token authority failed Error.',
|
||||
E000301: 'ADB2C request limit exceeded Error',
|
||||
E000401: 'IP address not found Error.',
|
||||
E000501: 'Request ID not found Error.',
|
||||
E010001: 'Param invalid format Error.',
|
||||
E010201: 'Email not verified user Error.',
|
||||
E010202: 'Email already verified user Error.',
|
||||
E010203: 'Administrator Permissions Error.',
|
||||
E010204: 'User not Found Error.',
|
||||
E010205: 'Role from DB is unexpected value Error.',
|
||||
E010206: 'Tier from DB is unexpected value Error.',
|
||||
E010207: 'User role change not allowed Error.',
|
||||
E010208: 'User encryption password not found Error.',
|
||||
E010209: 'Accepted term not latest Error.',
|
||||
E010301: 'This email user already created Error',
|
||||
E010302: 'This AuthorId already used Error',
|
||||
E010401: 'This PoNumber already used Error',
|
||||
E010501: 'Account not Found Error.',
|
||||
E010502: 'Account information cannot be changed Error.',
|
||||
E010503: 'Delegation not allowed Error.',
|
||||
E010504: 'Account is locked Error.',
|
||||
E010601: 'Task is not Editable Error',
|
||||
E010602: 'No task edit permissions Error',
|
||||
E010603: 'Task not found Error.',
|
||||
E010701: 'File not found in Blob Storage Error.',
|
||||
E010801: 'License not exist Error',
|
||||
E010802: 'License already activated Error',
|
||||
E010803: 'License already issued Error',
|
||||
E010804: 'License shortage Error',
|
||||
E010805: 'License is expired Error',
|
||||
E010806: 'License is unavailable Error',
|
||||
E010807: 'License is already deallocated Error',
|
||||
E010808: 'Order cancel failed Error',
|
||||
E010809: 'Already license order status changed Error',
|
||||
E010810: 'Cancellation period expired error',
|
||||
E010811: 'Already license allocated Error',
|
||||
E010812: 'License not allocated Error',
|
||||
E010908: 'Typist Group not exist Error',
|
||||
E010909: 'Typist Group name already exist Error',
|
||||
E011001: 'This WorkTypeID already used Error',
|
||||
E011002: 'WorkTypeID create limit exceeded Error',
|
||||
E011003: 'WorkTypeID not found Error',
|
||||
E011004: 'WorkTypeID is in use Error',
|
||||
E012001: 'Template file not found Error',
|
||||
E013001: 'AuthorId and WorktypeId pair already exists Error',
|
||||
E013002: 'Workflow not found Error',
|
||||
};
|
||||
15
data_migration_tools/server/src/common/error/types/types.ts
Normal file
15
data_migration_tools/server/src/common/error/types/types.ts
Normal 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;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ErrorCodes } from '../code';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ErrorCodes } from "../code";
|
||||
|
||||
export class ErrorResponse {
|
||||
@ApiProperty()
|
||||
|
||||
32
data_migration_tools/server/src/common/log/context.ts
Normal file
32
data_migration_tools/server/src/common/log/context.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Request } from 'express';
|
||||
import { Context } from './types';
|
||||
|
||||
export const makeContext = (
|
||||
externalId: string,
|
||||
requestId: string,
|
||||
delegationId?: string,
|
||||
): Context => {
|
||||
return new Context(externalId, requestId, delegationId);
|
||||
};
|
||||
|
||||
// リクエストヘッダーからrequestIdを取得する
|
||||
export const retrieveRequestId = (req: Request): string | undefined => {
|
||||
return req.header('x-request-id');
|
||||
};
|
||||
|
||||
/**
|
||||
* リクエストのIPアドレスを取得します
|
||||
* @param {Request}
|
||||
* @return {string | undefined}
|
||||
*/
|
||||
export const retrieveIp = (req: Request): string | undefined => {
|
||||
// ローカル環境では直近の送信元IPを取得する
|
||||
if (process.env.STAGE === 'local') {
|
||||
return req.ip;
|
||||
}
|
||||
const ip = req.header('x-forwarded-for');
|
||||
if (typeof ip === 'string') {
|
||||
return ip;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
4
data_migration_tools/server/src/common/log/index.ts
Normal file
4
data_migration_tools/server/src/common/log/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Context } from "./types";
|
||||
import { makeContext, retrieveRequestId, retrieveIp } from "./context";
|
||||
|
||||
export { Context, makeContext, retrieveRequestId, retrieveIp };
|
||||
34
data_migration_tools/server/src/common/log/types.ts
Normal file
34
data_migration_tools/server/src/common/log/types.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export class Context {
|
||||
/**
|
||||
* APIの操作ユーザーを追跡するためのID
|
||||
*/
|
||||
trackingId: string;
|
||||
/**
|
||||
* APIの操作ユーザーのIPアドレス
|
||||
*/
|
||||
ip: string;
|
||||
/**
|
||||
* ユーザーの操作を一意に識別するためのID
|
||||
*/
|
||||
requestId: string;
|
||||
/**
|
||||
* APIの代行操作ユーザーを追跡するためのID
|
||||
*/
|
||||
delegationId?: string | undefined;
|
||||
|
||||
constructor(externalId: string, requestId: string, delegationId?: string) {
|
||||
this.trackingId = externalId;
|
||||
this.delegationId = delegationId;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
/**
|
||||
* ログにユーザーを特定する情報を出力する
|
||||
*/
|
||||
getTrackingId(): string {
|
||||
if (this.delegationId) {
|
||||
return `${this.requestId}_${this.trackingId} by ${this.delegationId}`;
|
||||
} else {
|
||||
return `${this.requestId}_${this.trackingId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
data_migration_tools/server/src/common/password/index.ts
Normal file
3
data_migration_tools/server/src/common/password/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { makePassword } from "./password";
|
||||
|
||||
export { makePassword };
|
||||
35
data_migration_tools/server/src/common/password/password.ts
Normal file
35
data_migration_tools/server/src/common/password/password.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export const makePassword = (): string => {
|
||||
// パスワードの文字数を決定
|
||||
const passLength = 8;
|
||||
|
||||
// パスワードに使用可能な文字を決定(今回はアルファベットの大文字と小文字 + 数字 + symbolsの記号)
|
||||
const lowerCase = "abcdefghijklmnopqrstuvwxyz";
|
||||
const upperCase = lowerCase.toLocaleUpperCase();
|
||||
const numbers = "0123456789";
|
||||
const symbols = "@#$%^&*\\-_+=[]{}|:',.?/`~\"();!";
|
||||
const chars = lowerCase + upperCase + numbers + symbols;
|
||||
|
||||
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!)から2種類以上組み合わせ
|
||||
const charaTypePattern =
|
||||
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
|
||||
|
||||
// autoGeneratedPasswordが以上の条件を満たせばvalidがtrueになる
|
||||
let valid = false;
|
||||
let autoGeneratedPassword: string = "";
|
||||
|
||||
while (!valid) {
|
||||
// パスワードをランダムに決定
|
||||
while (autoGeneratedPassword.length < passLength) {
|
||||
// 上で決定したcharsの中からランダムに1文字ずつ追加
|
||||
const index = Math.floor(Math.random() * chars.length);
|
||||
autoGeneratedPassword += chars[index];
|
||||
}
|
||||
|
||||
// パスワードが上で決定した条件をすべて満たしているかチェック
|
||||
// 条件を満たすまでループ
|
||||
valid =
|
||||
autoGeneratedPassword.length == passLength &&
|
||||
charaTypePattern.test(autoGeneratedPassword);
|
||||
}
|
||||
return autoGeneratedPassword;
|
||||
};
|
||||
143
data_migration_tools/server/src/common/repository/index.ts
Normal file
143
data_migration_tools/server/src/common/repository/index.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
ObjectLiteral,
|
||||
Repository,
|
||||
EntityTarget,
|
||||
UpdateResult,
|
||||
DeleteResult,
|
||||
UpdateQueryBuilder,
|
||||
Brackets,
|
||||
FindOptionsWhere,
|
||||
} from 'typeorm';
|
||||
import { Context } from '../log';
|
||||
|
||||
/**
|
||||
* VS Code上で型解析エラーが発生するため、typeorm内の型定義と同一の型定義をここに記述する
|
||||
*/
|
||||
type QueryDeepPartialEntity<T> = _QueryDeepPartialEntity<
|
||||
ObjectLiteral extends T ? unknown : T
|
||||
>;
|
||||
type _QueryDeepPartialEntity<T> = {
|
||||
[P in keyof T]?:
|
||||
| (T[P] extends Array<infer U>
|
||||
? Array<_QueryDeepPartialEntity<U>>
|
||||
: T[P] extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<_QueryDeepPartialEntity<U>>
|
||||
: _QueryDeepPartialEntity<T[P]>)
|
||||
| (() => string);
|
||||
};
|
||||
|
||||
interface InsertEntityOptions {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const insertEntity = async <T extends InsertEntityOptions & ObjectLiteral>(
|
||||
entity: EntityTarget<T>,
|
||||
repository: Repository<T>,
|
||||
value: QueryDeepPartialEntity<T>,
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<T> => {
|
||||
let query = repository.createQueryBuilder().insert().into(entity);
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
const result = await query.values(value).execute();
|
||||
// result.identifiers[0].idがnumber型でない場合はエラー
|
||||
if (typeof result.identifiers[0].id !== 'number') {
|
||||
throw new Error('Failed to insert entity');
|
||||
}
|
||||
const where: FindOptionsWhere<T> = { id: result.identifiers[0].id } as T;
|
||||
|
||||
// 結果をもとにセレクトする
|
||||
const inserted = await repository.findOne({
|
||||
where,
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (!inserted) {
|
||||
throw new Error('Failed to insert entity');
|
||||
}
|
||||
return inserted;
|
||||
};
|
||||
|
||||
const insertEntities = async <T extends InsertEntityOptions & ObjectLiteral>(
|
||||
entity: EntityTarget<T>,
|
||||
repository: Repository<T>,
|
||||
values: QueryDeepPartialEntity<T>[],
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<T[]> => {
|
||||
let query = repository.createQueryBuilder().insert().into(entity);
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
const result = await query.values(values).execute();
|
||||
|
||||
// 挿入するレコードが0で、結果も0であれば、からの配列を返す
|
||||
if (values.length === 0 && result.identifiers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 挿入するレコード数と挿入されたレコード数が一致しない場合はエラー
|
||||
if (result.identifiers.length !== values.length) {
|
||||
throw new Error('Failed to insert entities');
|
||||
}
|
||||
const where: FindOptionsWhere<T>[] = result.identifiers.map((i) => {
|
||||
// idがnumber型でない場合はエラー
|
||||
if (typeof i.id !== 'number') {
|
||||
throw new Error('Failed to insert entities');
|
||||
}
|
||||
return { id: i.id } as T;
|
||||
});
|
||||
|
||||
// 結果をもとにセレクトする
|
||||
const inserted = await repository.find({
|
||||
where,
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (!inserted) {
|
||||
throw new Error('Failed to insert entity');
|
||||
}
|
||||
return inserted;
|
||||
};
|
||||
|
||||
const updateEntity = async <T extends ObjectLiteral>(
|
||||
repository: Repository<T>,
|
||||
criteria:
|
||||
| string
|
||||
| ((qb: UpdateQueryBuilder<T>) => string)
|
||||
| Brackets
|
||||
| ObjectLiteral
|
||||
| ObjectLiteral[],
|
||||
values: QueryDeepPartialEntity<T>,
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<UpdateResult> => {
|
||||
let query = repository.createQueryBuilder().update();
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
return await query.set(values).where(criteria).execute();
|
||||
};
|
||||
|
||||
const deleteEntity = async <T extends ObjectLiteral>(
|
||||
repository: Repository<T>,
|
||||
criteria: string | Brackets | ObjectLiteral | ObjectLiteral[],
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<DeleteResult> => {
|
||||
let query = repository.createQueryBuilder().delete();
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
return await query.where(criteria).execute();
|
||||
};
|
||||
|
||||
export { insertEntity, insertEntities, updateEntity, deleteEntity };
|
||||
10
data_migration_tools/server/src/common/types/role/index.ts
Normal file
10
data_migration_tools/server/src/common/types/role/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ADMIN_ROLES, USER_ROLES } from '../../../constants';
|
||||
|
||||
/**
|
||||
* Token.roleに配置されうる文字列リテラル型
|
||||
*/
|
||||
export type Roles =
|
||||
| (typeof ADMIN_ROLES)[keyof typeof ADMIN_ROLES]
|
||||
| (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||
|
||||
export type UserRoles = (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||
27
data_migration_tools/server/src/common/types/sort/index.ts
Normal file
27
data_migration_tools/server/src/common/types/sort/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {
|
||||
TASK_LIST_SORTABLE_ATTRIBUTES,
|
||||
SORT_DIRECTIONS,
|
||||
} from '../../../constants';
|
||||
|
||||
export type TaskListSortableAttribute =
|
||||
(typeof TASK_LIST_SORTABLE_ATTRIBUTES)[number];
|
||||
|
||||
export type SortDirection = (typeof SORT_DIRECTIONS)[number];
|
||||
|
||||
export const isTaskListSortableAttribute = (
|
||||
arg: string,
|
||||
): arg is TaskListSortableAttribute => {
|
||||
const param = arg as TaskListSortableAttribute;
|
||||
if (TASK_LIST_SORTABLE_ATTRIBUTES.includes(param)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isSortDirection = (arg: string): arg is SortDirection => {
|
||||
const param = arg as SortDirection;
|
||||
if (SORT_DIRECTIONS.includes(param)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
11
data_migration_tools/server/src/common/types/sort/util.ts
Normal file
11
data_migration_tools/server/src/common/types/sort/util.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { SortDirection, TaskListSortableAttribute } from '.';
|
||||
|
||||
export const getDirection = (direction: SortDirection): SortDirection => {
|
||||
return direction;
|
||||
};
|
||||
|
||||
export const getTaskListSortableAttribute = (
|
||||
TaskListSortableAttribute: TaskListSortableAttribute,
|
||||
): TaskListSortableAttribute => {
|
||||
return TaskListSortableAttribute;
|
||||
};
|
||||
231
data_migration_tools/server/src/common/types/types.ts
Normal file
231
data_migration_tools/server/src/common/types/types.ts
Normal file
@ -0,0 +1,231 @@
|
||||
export class csvInputFile {
|
||||
type: string;
|
||||
account_id: string;
|
||||
parent_id: string;
|
||||
email: string;
|
||||
company_name: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
country: string;
|
||||
state: string;
|
||||
start_date: Date;
|
||||
expired_date: Date;
|
||||
user_email: string;
|
||||
author_id: string;
|
||||
recording_mode: string;
|
||||
wt1: string;
|
||||
wt2: string;
|
||||
wt3: string;
|
||||
wt4: string;
|
||||
wt5: string;
|
||||
wt6: string;
|
||||
wt7: string;
|
||||
wt8: string;
|
||||
wt9: string;
|
||||
wt10: string;
|
||||
wt11: string;
|
||||
wt12: string;
|
||||
wt13: string;
|
||||
wt14: string;
|
||||
wt15: string;
|
||||
wt16: string;
|
||||
wt17: string;
|
||||
wt18: string;
|
||||
wt19: string;
|
||||
wt20: string;
|
||||
}
|
||||
export class AccountsOutputFileStep1 {
|
||||
accountId: number;
|
||||
type: string;
|
||||
companyName: string;
|
||||
country: string;
|
||||
dealerAccountId?: number;
|
||||
adminName: string;
|
||||
adminMail: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export class AccountsOutputFile {
|
||||
accountId: number;
|
||||
type: number;
|
||||
companyName: string;
|
||||
country: string;
|
||||
dealerAccountId?: number;
|
||||
adminName: string;
|
||||
adminMail: string;
|
||||
userId: number;
|
||||
}
|
||||
export class AccountsInputFile {
|
||||
accountId: number;
|
||||
type: number;
|
||||
companyName: string;
|
||||
country: string;
|
||||
dealerAccountId?: number;
|
||||
adminName: string;
|
||||
adminMail: string;
|
||||
userId: number;
|
||||
}
|
||||
export class UsersOutputFile {
|
||||
accountId: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
role: string;
|
||||
authorId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export class UsersInputFile {
|
||||
accountId: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
role: string;
|
||||
authorId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export class LicensesOutputFile {
|
||||
expiry_date: string;
|
||||
account_id: number;
|
||||
type: string;
|
||||
status: string;
|
||||
allocated_user_id?: number;
|
||||
}
|
||||
export class LicensesInputFile {
|
||||
expiry_date: string;
|
||||
account_id: number;
|
||||
type: string;
|
||||
status: string;
|
||||
allocated_user_id?: number;
|
||||
}
|
||||
|
||||
export class WorktypesOutputFile {
|
||||
account_id: number;
|
||||
custom_worktype_id: string;
|
||||
}
|
||||
export class WorktypesInputFile {
|
||||
account_id: number;
|
||||
custom_worktype_id: string;
|
||||
}
|
||||
|
||||
export class CardLicensesInputFile {
|
||||
license_id: number;
|
||||
issue_id: number;
|
||||
card_license_key: string;
|
||||
activated_at?: string;
|
||||
created_at?: string;
|
||||
created_by?: string;
|
||||
updated_at?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export function isAccountsInputFileArray(obj: any): obj is AccountsInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isAccountsInputFile(item));
|
||||
}
|
||||
export function isAccountsInputFile(obj: any): obj is AccountsInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"accountId" in obj &&
|
||||
typeof obj.accountId === "number" &&
|
||||
"type" in obj &&
|
||||
typeof obj.type === "number" &&
|
||||
"companyName" in obj &&
|
||||
typeof obj.companyName === "string" &&
|
||||
"country" in obj &&
|
||||
typeof obj.country === "string" &&
|
||||
("dealerAccountId" in obj
|
||||
? typeof obj.dealerAccountId === "number"
|
||||
: true) &&
|
||||
"adminName" in obj &&
|
||||
typeof obj.adminName === "string" &&
|
||||
"adminMail" in obj &&
|
||||
typeof obj.adminMail === "string" &&
|
||||
"userId" in obj &&
|
||||
typeof obj.userId === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export function isUsersInputFileArray(obj: any): obj is UsersInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isUsersInputFile(item));
|
||||
}
|
||||
export function isUsersInputFile(obj: any): obj is UsersInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"accountId" in obj &&
|
||||
"userId" in obj &&
|
||||
"name" in obj &&
|
||||
"role" in obj &&
|
||||
"authorId" in obj &&
|
||||
"email" in obj &&
|
||||
typeof obj.accountId === "number" &&
|
||||
typeof obj.userId === "number" &&
|
||||
typeof obj.name === "string" &&
|
||||
typeof obj.role === "string" &&
|
||||
typeof obj.authorId === "string" &&
|
||||
typeof obj.email === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isLicensesInputFileArray(obj: any): obj is LicensesInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isLicensesInputFile(item));
|
||||
}
|
||||
export function isLicensesInputFile(obj: any): obj is LicensesInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"expiry_date" in obj &&
|
||||
"account_id" in obj &&
|
||||
"type" in obj &&
|
||||
"status" in obj &&
|
||||
typeof obj.expiry_date === "string" &&
|
||||
typeof obj.account_id === "number" &&
|
||||
typeof obj.type === "string" &&
|
||||
typeof obj.status === "string" &&
|
||||
(obj.allocated_user_id === null ||
|
||||
typeof obj.allocated_user_id === "number")
|
||||
);
|
||||
}
|
||||
|
||||
export function isWorktypesInputFileArray(
|
||||
obj: any
|
||||
): obj is WorktypesInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isWorktypesInputFile(item));
|
||||
}
|
||||
export function isWorktypesInputFile(obj: any): obj is WorktypesInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"account_id" in obj &&
|
||||
"custom_worktype_id" in obj &&
|
||||
typeof obj.account_id === "number" &&
|
||||
typeof obj.custom_worktype_id === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isCardLicensesInputFileArray(
|
||||
obj: any
|
||||
): obj is CardLicensesInputFile[] {
|
||||
return (
|
||||
Array.isArray(obj) && obj.every((item) => isCardLicensesInputFile(item))
|
||||
);
|
||||
}
|
||||
export function isCardLicensesInputFile(
|
||||
obj: any
|
||||
): obj is CardLicensesInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"license_id" in obj &&
|
||||
"issue_id" in obj &&
|
||||
"card_license_key" in obj &&
|
||||
typeof obj.license_id === "number" &&
|
||||
typeof obj.issue_id === "number" &&
|
||||
typeof obj.card_license_key === "string" &&
|
||||
(obj.activated_at === null || typeof obj.activated_at === "string") &&
|
||||
(obj.created_at === null || typeof obj.created_at === "string") &&
|
||||
(obj.created_by === null || typeof obj.created_by === "string") &&
|
||||
(obj.updated_at === null || typeof obj.updated_at === "string") &&
|
||||
(obj.updated_by === null || typeof obj.updated_by === "string")
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from "class-validator";
|
||||
|
||||
@ValidatorConstraint()
|
||||
export class IsAdminPassword implements ValidatorConstraintInterface {
|
||||
validate(value: string): boolean {
|
||||
// 8文字~64文字でなければ早期に不合格
|
||||
const minLength = 8;
|
||||
const maxLength = 64;
|
||||
if (value.length < minLength || value.length > maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!)から2種類以上組み合わせ
|
||||
const charaTypePattern =
|
||||
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
|
||||
return new RegExp(charaTypePattern).test(value);
|
||||
}
|
||||
defaultMessage(): string {
|
||||
return "Admin password rule not satisfied";
|
||||
}
|
||||
}
|
||||
|
||||
export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => {
|
||||
return (object: any, propertyName: string) => {
|
||||
registerDecorator({
|
||||
name: "IsAdminPasswordvalid",
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [],
|
||||
options: validationOptions,
|
||||
validator: IsAdminPassword,
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -1,3 +1,20 @@
|
||||
/**
|
||||
* 階層
|
||||
* @const {number}
|
||||
*/
|
||||
export const TIERS = {
|
||||
//OMDS東京
|
||||
TIER1: 1,
|
||||
//OMDS現地法人
|
||||
TIER2: 2,
|
||||
//代理店
|
||||
TIER3: 3,
|
||||
//販売店
|
||||
TIER4: 4,
|
||||
//エンドユーザー
|
||||
TIER5: 5,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 音声ファイルをEast USに保存する国リスト
|
||||
* @const {number}
|
||||
@ -52,6 +69,213 @@ export const BLOB_STORAGE_REGION_EU = [
|
||||
"GB",
|
||||
];
|
||||
|
||||
/**
|
||||
* 管理ロール
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const ADMIN_ROLES = {
|
||||
ADMIN: "admin",
|
||||
STANDARD: "standard",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ロール
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const USER_ROLES = {
|
||||
NONE: "none",
|
||||
AUTHOR: "author",
|
||||
TYPIST: "typist",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ロールのソート順
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const USER_ROLE_ORDERS = [
|
||||
USER_ROLES.AUTHOR,
|
||||
USER_ROLES.TYPIST,
|
||||
USER_ROLES.NONE,
|
||||
] as string[];
|
||||
|
||||
/**
|
||||
* ライセンス注文状態
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const LICENSE_ISSUE_STATUS = {
|
||||
ISSUE_REQUESTING: "Issue Requesting",
|
||||
ISSUED: "Issued",
|
||||
CANCELED: "Order Canceled",
|
||||
};
|
||||
|
||||
/**
|
||||
* ライセンス種別
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const LICENSE_TYPE = {
|
||||
TRIAL: "TRIAL",
|
||||
NORMAL: "NORMAL",
|
||||
CARD: "CARD",
|
||||
} as const;
|
||||
/**
|
||||
* ライセンス状態
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const LICENSE_ALLOCATED_STATUS = {
|
||||
UNALLOCATED: "Unallocated",
|
||||
ALLOCATED: "Allocated",
|
||||
REUSABLE: "Reusable",
|
||||
DELETED: "Deleted",
|
||||
} as const;
|
||||
/**
|
||||
* 切り替え元種別
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const SWITCH_FROM_TYPE = {
|
||||
NONE: "NONE",
|
||||
CARD: "CARD",
|
||||
TRIAL: "TRIAL",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ライセンスの期限切れが近いと見なす日数のしきい値
|
||||
* @const {number}
|
||||
*/
|
||||
export const LICENSE_EXPIRATION_THRESHOLD_DAYS = 14;
|
||||
|
||||
/**
|
||||
* ライセンスの有効期間
|
||||
* @const {number}
|
||||
*/
|
||||
export const LICENSE_EXPIRATION_DAYS = 365;
|
||||
|
||||
/**
|
||||
* タイムゾーンを加味したライセンスの有効期間(8時間)
|
||||
* @const {number}
|
||||
*/
|
||||
export const LICENSE_EXPIRATION_TIME_WITH_TIMEZONE = 8;
|
||||
|
||||
/**
|
||||
* カードライセンスの桁数
|
||||
* @const {number}
|
||||
*/
|
||||
export const CARD_LICENSE_LENGTH = 20;
|
||||
|
||||
/**
|
||||
* 音声ファイルに紐づくオプションアイテムの数
|
||||
* @const {string}
|
||||
*/
|
||||
export const OPTION_ITEM_NUM = 10;
|
||||
|
||||
/**
|
||||
* 文字起こしタスクのステータス
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const TASK_STATUS = {
|
||||
UPLOADED: "Uploaded",
|
||||
PENDING: "Pending",
|
||||
IN_PROGRESS: "InProgress",
|
||||
FINISHED: "Finished",
|
||||
BACKUP: "Backup",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* タスク一覧でソート可能な属性の一覧
|
||||
*/
|
||||
export const TASK_LIST_SORTABLE_ATTRIBUTES = [
|
||||
"JOB_NUMBER",
|
||||
"STATUS",
|
||||
"ENCRYPTION",
|
||||
"AUTHOR_ID",
|
||||
"WORK_TYPE",
|
||||
"FILE_NAME",
|
||||
"FILE_LENGTH",
|
||||
"FILE_SIZE",
|
||||
"RECORDING_STARTED_DATE",
|
||||
"RECORDING_FINISHED_DATE",
|
||||
"UPLOAD_DATE",
|
||||
"TRANSCRIPTION_STARTED_DATE",
|
||||
"TRANSCRIPTION_FINISHED_DATE",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* タスク一覧のソート条件(昇順・降順)
|
||||
*/
|
||||
export const SORT_DIRECTIONS = ["ASC", "DESC"] as const;
|
||||
|
||||
/**
|
||||
* 通知タグの最大個数
|
||||
* NotificationHubの仕様上タグ式のOR条件で使えるタグは20個まで
|
||||
* https://learn.microsoft.com/ja-jp/azure/notification-hubs/notification-hubs-tags-segment-push-message#tag-expressions
|
||||
*/
|
||||
export const TAG_MAX_COUNT = 20;
|
||||
|
||||
/**
|
||||
* 通知のプラットフォーム種別文字列
|
||||
*/
|
||||
export const PNS = {
|
||||
WNS: "wns",
|
||||
APNS: "apns",
|
||||
};
|
||||
|
||||
/**
|
||||
* ユーザーのライセンスの有効期限の状態
|
||||
*/
|
||||
export const USER_LICENSE_EXPIRY_STATUS = {
|
||||
NORMAL: "Normal",
|
||||
NO_LICENSE: "NoLicense",
|
||||
ALERT: "Alert",
|
||||
RENEW: "Renew",
|
||||
};
|
||||
|
||||
/**
|
||||
*トライアルライセンスの有効期限(日数)
|
||||
* @const {number}
|
||||
*/
|
||||
export const TRIAL_LICENSE_EXPIRATION_DAYS = 30;
|
||||
|
||||
/**
|
||||
* ライセンスの発行数
|
||||
* @const {number}
|
||||
*/
|
||||
export const TRIAL_LICENSE_ISSUE_NUM = 100;
|
||||
|
||||
/**
|
||||
* worktypeの最大登録数
|
||||
* @const {number}
|
||||
*/
|
||||
export const WORKTYPE_MAX_COUNT = 20;
|
||||
|
||||
/**
|
||||
* worktypeのDefault値の取りうる値
|
||||
**/
|
||||
export const OPTION_ITEM_VALUE_TYPE = {
|
||||
DEFAULT: "Default",
|
||||
BLANK: "Blank",
|
||||
LAST_INPUT: "LastInput",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* オプションアイテムのタイプ文字列と数値の対応
|
||||
**/
|
||||
export const OPTION_ITEM_VALUE_TYPE_NUMBER: {
|
||||
type: string;
|
||||
value: number;
|
||||
}[] = [
|
||||
{
|
||||
type: OPTION_ITEM_VALUE_TYPE.BLANK,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
type: OPTION_ITEM_VALUE_TYPE.DEFAULT,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
type: OPTION_ITEM_VALUE_TYPE.LAST_INPUT,
|
||||
value: 3,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* ADB2Cユーザのidentity.signInType
|
||||
* @const {string[]}
|
||||
@ -60,8 +284,123 @@ export const ADB2C_SIGN_IN_TYPE = {
|
||||
EMAILADDRESS: "emailAddress",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* MANUAL_RECOVERY_REQUIRED
|
||||
* @const {string}
|
||||
*/
|
||||
export const MANUAL_RECOVERY_REQUIRED = "[MANUAL_RECOVERY_REQUIRED]";
|
||||
|
||||
/**
|
||||
* 利用規約種別
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const TERM_TYPE = {
|
||||
EULA: "EULA",
|
||||
DPA: "DPA",
|
||||
PRIVACY_NOTICE: "PrivacyNotice",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 音声ファイルのフォーマット
|
||||
* @const {string}
|
||||
*/
|
||||
export const USER_AUDIO_FORMAT = "DS2(QP)";
|
||||
|
||||
/**
|
||||
* ユニットテスト実行をしている場合のNODE_ENVの値
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const NODE_ENV_TEST = "test";
|
||||
|
||||
/**
|
||||
* ユーザに対するライセンスの状態
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const USER_LICENSE_STATUS = {
|
||||
UNALLOCATED: "unallocated",
|
||||
ALLOCATED: "allocated",
|
||||
EXPIRED: "expired",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* typeの取りうる値(移行元CSV)
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const MIGRATION_TYPE = {
|
||||
ADMINISTRATOR: "Administrator",
|
||||
BC: "BC",
|
||||
COUNTRY: "Country",
|
||||
CUSTOMER: "Customer",
|
||||
DEALER: "Dealer",
|
||||
DISTRIBUTOR: "Distributor",
|
||||
USER: "USER",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 移行先の名称と移行元の値
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const COUNTRY_LIST = [
|
||||
{ value: "CA", label: "Canada" },
|
||||
{ value: "KY", label: "Cayman Islands" },
|
||||
{ value: "US", label: "United States" },
|
||||
{ value: "AU", label: "Australia" },
|
||||
{ value: "NZ", label: "New Zealand" },
|
||||
{ value: "AT", label: "Austria" },
|
||||
{ value: "BE", label: "Belgium" },
|
||||
{ value: "BG", label: "Bulgaria" },
|
||||
{ value: "HR", label: "Croatia" },
|
||||
{ value: "CY", label: "Cyprus" },
|
||||
{ value: "CZ", label: "Czech Republic" },
|
||||
{ value: "DK", label: "Denmark" },
|
||||
{ value: "EE", label: "Estonia" },
|
||||
{ value: "FI", label: "Finland" },
|
||||
{ value: "FR", label: "France" },
|
||||
{ value: "DE", label: "Germany" },
|
||||
{ value: "GR", label: "Greece" },
|
||||
{ value: "HU", label: "Hungary" },
|
||||
{ value: "IS", label: "Iceland" },
|
||||
{ value: "IE", label: "Ireland" },
|
||||
{ value: "IT", label: "Italy" },
|
||||
{ value: "LV", label: "Latvia" },
|
||||
{ value: "LI", label: "Liechtenstein" },
|
||||
{ value: "LT", label: "Lithuania" },
|
||||
{ value: "LU", label: "Luxembourg" },
|
||||
{ value: "MT", label: "Malta" },
|
||||
{ value: "NL", label: "Netherlands" },
|
||||
{ value: "NO", label: "Norway" },
|
||||
{ value: "PL", label: "Poland" },
|
||||
{ value: "PT", label: "Portugal" },
|
||||
{ value: "RO", label: "Romania" },
|
||||
{ value: "RS", label: "Serbia" },
|
||||
{ value: "SK", label: "Slovakia" },
|
||||
{ value: "SI", label: "Slovenia" },
|
||||
{ value: "ZA", label: "South Africa" },
|
||||
{ value: "ES", label: "Spain" },
|
||||
{ value: "SE", label: "Sweden" },
|
||||
{ value: "CH", label: "Switzerland" },
|
||||
{ value: "TR", label: "Turkey" },
|
||||
{ value: "GB", label: "United Kingdom" },
|
||||
];
|
||||
|
||||
/**
|
||||
* recording_modeの取りうる値(移行元CSV)
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const RECORDING_MODE = {
|
||||
DS2_QP: "DS2 (QP)",
|
||||
DS2_SP: "DS2 (SP)",
|
||||
DSS: "DSS",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* AutoIncrementの初期値
|
||||
* @const {number}
|
||||
*/
|
||||
export const AUTO_INCREMENT_START = 853211;
|
||||
|
||||
/**
|
||||
* 移行データ登録時のsleep間隔
|
||||
* @const {number}
|
||||
*/
|
||||
export const MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC = 13;
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { Controller, Logger } from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { AccountsService } from "./accounts.service";
|
||||
|
||||
@ApiTags("accounts")
|
||||
@Controller("accounts")
|
||||
export class AccountsController {
|
||||
private readonly logger = new Logger(AccountsController.name);
|
||||
constructor(
|
||||
private readonly accountService: AccountsService //private readonly cryptoService: CryptoService,
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UsersRepositoryModule } from "../../repositories/users/users.repository.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";
|
||||
import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
AdB2cModule,
|
||||
BlobstorageModule,
|
||||
],
|
||||
controllers: [AccountsController],
|
||||
providers: [AccountsService],
|
||||
})
|
||||
export class AccountsModule {}
|
||||
@ -0,0 +1,227 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
import { AccountsRepositoryService } from "../../repositories/accounts/accounts.repository.service";
|
||||
import {
|
||||
AdB2cService,
|
||||
ConflictError,
|
||||
isConflictError,
|
||||
} from "../../gateways/adb2c/adb2c.service";
|
||||
import { Account } from "../../repositories/accounts/entity/account.entity";
|
||||
import { User } from "../../repositories/users/entity/user.entity";
|
||||
import { MANUAL_RECOVERY_REQUIRED } from "../../constants";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
import { Context } from "../../common/log";
|
||||
import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service";
|
||||
|
||||
@Injectable()
|
||||
export class AccountsService {
|
||||
constructor(
|
||||
private readonly accountRepository: AccountsRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly blobStorageService: BlobstorageService
|
||||
) {}
|
||||
private readonly logger = new Logger(AccountsService.name);
|
||||
|
||||
/**
|
||||
* アカウント情報をDBに作成する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param [dealerAccountId]
|
||||
* @returns account
|
||||
*/
|
||||
async createAccount(
|
||||
context: Context,
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | undefined,
|
||||
email: string,
|
||||
password: string,
|
||||
username: string,
|
||||
role: string,
|
||||
acceptedEulaVersion: string,
|
||||
acceptedPrivacyNoticeVersion: string,
|
||||
acceptedDpaVersion: string,
|
||||
type: number,
|
||||
accountId: number,
|
||||
userId: number
|
||||
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.createAccount.name
|
||||
} | params: { ` +
|
||||
`dealerAccountId: ${dealerAccountId}, ` +
|
||||
`role: ${role}, ` +
|
||||
`acceptedEulaVersion: ${acceptedEulaVersion}, ` +
|
||||
`acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` +
|
||||
`acceptedDpaVersion: ${acceptedDpaVersion}, ` +
|
||||
`type: ${type}, ` +
|
||||
`accountId: ${accountId}, ` +
|
||||
`userId: ${userId} };`
|
||||
);
|
||||
try {
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
try {
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
context,
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] create externalUser failed`
|
||||
);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// メールアドレス重複エラー
|
||||
if (isConflictError(externalUser)) {
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] email conflict. externalUser: ${externalUser}`
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010301"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let account: Account;
|
||||
let user: User;
|
||||
try {
|
||||
// アカウントと管理者をセットで作成
|
||||
const { newAccount, adminUser } =
|
||||
await this.accountRepository.createAccount(
|
||||
context,
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
type,
|
||||
externalUser.sub,
|
||||
role,
|
||||
accountId,
|
||||
userId,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] adminUser.external_id: ${
|
||||
user.external_id
|
||||
}`
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(`[${context.getTrackingId()}] create account failed`);
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// 新規作成アカウント用のBlobコンテナを作成
|
||||
try {
|
||||
await this.blobStorageService.createContainer(
|
||||
context,
|
||||
account.id,
|
||||
country
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] create container failed`
|
||||
);
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
// DBのアカウントを削除
|
||||
await this.deleteAccount(account.id, user.id, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
accountId: account.id,
|
||||
userId: user.id,
|
||||
externalUserId: user.external_id,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createAccount.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// AdB2cのユーザーを削除
|
||||
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
||||
private async deleteAdB2cUser(
|
||||
externalUserId: string,
|
||||
context: Context
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.createAccount.name
|
||||
} | params: { ` + `externalUserId: ${externalUserId}};`
|
||||
);
|
||||
try {
|
||||
await this.adB2cService.deleteUser(externalUserId, context);
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] delete externalUser: ${externalUserId} | params: { ` +
|
||||
`externalUserId: ${externalUserId}, };`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteAdB2cUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DBのアカウントを削除
|
||||
private async deleteAccount(
|
||||
accountId: number,
|
||||
userId: number,
|
||||
context: Context
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteAccount.name
|
||||
} | params: { accountId: ${accountId}, userId: ${userId} };`
|
||||
);
|
||||
try {
|
||||
await this.accountRepository.deleteAccount(context, accountId, userId);
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] delete account: ${accountId}, user: ${userId}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete account: ${accountId}, user: ${userId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteAccount.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { Request } from "express";
|
||||
import { DeleteService } from "./delete.service";
|
||||
import { DeleteResponse } from "./types/types";
|
||||
import { makeContext } from "src/common/log";
|
||||
|
||||
@ApiTags("delete")
|
||||
@Controller("delete")
|
||||
@ -33,7 +34,9 @@ export class DeleteController {
|
||||
})
|
||||
@Post()
|
||||
async deleteData(): Promise<{}> {
|
||||
await this.deleteService.deleteData();
|
||||
const context = makeContext("tool", "delete");
|
||||
|
||||
await this.deleteService.deleteData(context);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { DeleteRepositoryService } from "../../repositories/delete/delete.reposi
|
||||
import { makeErrorResponse } from "../../common/errors/makeErrorResponse";
|
||||
import { AdB2cService } from "../../gateways/adb2c/adb2c.service";
|
||||
import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service";
|
||||
import { Context } from "../../common/log";
|
||||
|
||||
@Injectable()
|
||||
export class DeleteService {
|
||||
@ -11,27 +12,41 @@ export class DeleteService {
|
||||
private readonly deleteRepositoryService: DeleteRepositoryService,
|
||||
private readonly blobstorageService: BlobstorageService,
|
||||
private readonly adB2cService: AdB2cService
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* データを削除する
|
||||
* @returns data
|
||||
*/
|
||||
async deleteData(): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.deleteData.name}`);
|
||||
async deleteData(context: Context): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.deleteData.name}`
|
||||
);
|
||||
try {
|
||||
// BlobStorageからデータを削除する
|
||||
await this.blobstorageService.deleteContainers();
|
||||
await this.blobstorageService.deleteContainers(context);
|
||||
|
||||
// ADB2Cからユーザ情報を取得する
|
||||
const users = await this.adB2cService.getUsers();
|
||||
const externalIds = users.map((user) => user.id);
|
||||
await this.adB2cService.deleteUsers(externalIds);
|
||||
// 100件ずつのユーザー取得なのですべて削除するまでループする
|
||||
for (let i = 0; i < 500; i++) {
|
||||
// ADB2Cからユーザ情報を取得する
|
||||
const { users, hasNext } = await this.adB2cService.getUsers(context);
|
||||
|
||||
|
||||
const externalIds = users.map((user) => user.id);
|
||||
await this.adB2cService.deleteUsers(context, externalIds);
|
||||
|
||||
// 削除していないユーザーがいない場合はループを抜ける
|
||||
if (!hasNext) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// データベースからデータを削除する
|
||||
await this.deleteRepositoryService.deleteData();
|
||||
// AutoIncrementの値をリセットする
|
||||
await this.deleteRepositoryService.resetAutoIncrement();
|
||||
// 初期データを挿入する
|
||||
await this.deleteRepositoryService.insertInitData(context);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
|
||||
@ -0,0 +1,209 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Req,
|
||||
Logger,
|
||||
HttpException,
|
||||
} from "@nestjs/common";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
import fs from "fs";
|
||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { Request } from "express";
|
||||
import { RegisterRequest, RegisterResponse } from "./types/types";
|
||||
import { RegisterService } from "./register.service";
|
||||
import { AccountsService } from "../accounts/accounts.service";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { makeContext } from "../../common/log";
|
||||
import {
|
||||
isAccountsInputFileArray,
|
||||
isUsersInputFileArray,
|
||||
isLicensesInputFileArray,
|
||||
isWorktypesInputFileArray,
|
||||
isCardLicensesInputFileArray,
|
||||
} from "../../common/types/types";
|
||||
import { makePassword } from "../../common/password/password";
|
||||
import {
|
||||
USER_ROLES,
|
||||
MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC,
|
||||
} from "../../constants";
|
||||
|
||||
@ApiTags("register")
|
||||
@Controller("register")
|
||||
export class RegisterController {
|
||||
private readonly logger = new Logger(RegisterController.name);
|
||||
constructor(
|
||||
private readonly registerService: RegisterService,
|
||||
private readonly accountsService: AccountsService,
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: RegisterResponse,
|
||||
description: "成功時のレスポンス",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: "想定外のサーバーエラー",
|
||||
})
|
||||
@ApiOperation({ operationId: "dataRegist" })
|
||||
async dataRegist(
|
||||
@Body() body: RegisterRequest,
|
||||
@Req() req: Request
|
||||
): Promise<RegisterResponse> {
|
||||
const context = makeContext("iko", "register");
|
||||
|
||||
const inputFilePath = body.inputFilePath;
|
||||
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.dataRegist.name
|
||||
} | params: { inputFilePath: ${inputFilePath}};`
|
||||
);
|
||||
|
||||
try {
|
||||
// 読み込みファイルのフルパス
|
||||
const accouncsFileFullPath = inputFilePath + "accounts.json";
|
||||
const usersFileFullPath = inputFilePath + "users.json";
|
||||
const licensesFileFullPath = inputFilePath + "licenses.json";
|
||||
const worktypesFileFullPath = inputFilePath + "worktypes.json";
|
||||
const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json";
|
||||
|
||||
// ファイル存在チェックと読み込み
|
||||
if (
|
||||
!fs.existsSync(accouncsFileFullPath) ||
|
||||
!fs.existsSync(usersFileFullPath) ||
|
||||
!fs.existsSync(licensesFileFullPath) ||
|
||||
!fs.existsSync(worktypesFileFullPath) ||
|
||||
!fs.existsSync(cardLicensesFileFullPath)
|
||||
) {
|
||||
this.logger.error(`file not exists from ${inputFilePath}`);
|
||||
throw new Error(`file not exists from ${inputFilePath}`);
|
||||
}
|
||||
|
||||
// アカウントの登録用ファイル読み込み
|
||||
const accountsObject = JSON.parse(
|
||||
fs.readFileSync(accouncsFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(account)
|
||||
if (!isAccountsInputFileArray(accountsObject)) {
|
||||
throw new Error("input file is not accountsInputFiles");
|
||||
}
|
||||
|
||||
for (const accountsInputFile of accountsObject) {
|
||||
// ランダムなパスワードを生成する
|
||||
const ramdomPassword = makePassword();
|
||||
await this.accountsService.createAccount(
|
||||
context,
|
||||
accountsInputFile.companyName,
|
||||
accountsInputFile.country,
|
||||
accountsInputFile.dealerAccountId,
|
||||
accountsInputFile.adminMail,
|
||||
ramdomPassword,
|
||||
accountsInputFile.adminName,
|
||||
"none",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
accountsInputFile.type,
|
||||
accountsInputFile.accountId,
|
||||
accountsInputFile.userId
|
||||
);
|
||||
|
||||
// ratelimit対応のためsleepを行う
|
||||
await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC);
|
||||
}
|
||||
// const accountsInputFiles = accountsObject as AccountsInputFile[];
|
||||
|
||||
// ユーザの登録用ファイル読み込み
|
||||
const usersObject = JSON.parse(
|
||||
fs.readFileSync(usersFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(user)
|
||||
if (!isUsersInputFileArray(usersObject)) {
|
||||
throw new Error("input file is not usersInputFiles");
|
||||
}
|
||||
|
||||
for (const usersInputFile of usersObject) {
|
||||
this.logger.log(usersInputFile.name);
|
||||
await this.usersService.createUser(
|
||||
context,
|
||||
usersInputFile.name,
|
||||
usersInputFile.role === USER_ROLES.AUTHOR
|
||||
? USER_ROLES.AUTHOR
|
||||
: USER_ROLES.NONE,
|
||||
usersInputFile.email,
|
||||
true,
|
||||
true,
|
||||
usersInputFile.accountId,
|
||||
usersInputFile.userId,
|
||||
usersInputFile.authorId,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
);
|
||||
// ratelimit対応のためsleepを行う
|
||||
await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC);
|
||||
}
|
||||
|
||||
// ライセンスの登録用ファイル読み込み
|
||||
const licensesObject = JSON.parse(
|
||||
fs.readFileSync(licensesFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(license)
|
||||
if (!isLicensesInputFileArray(licensesObject)) {
|
||||
throw new Error("input file is not licensesInputFiles");
|
||||
}
|
||||
|
||||
// ワークタイプの登録用ファイル読み込み
|
||||
const worktypesObject = JSON.parse(
|
||||
fs.readFileSync(worktypesFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(Worktypes)
|
||||
if (!isWorktypesInputFileArray(worktypesObject)) {
|
||||
throw new Error("input file is not WorktypesInputFiles");
|
||||
}
|
||||
|
||||
// カードライセンスの登録用ファイル読み込み
|
||||
const cardLicensesObject = JSON.parse(
|
||||
fs.readFileSync(cardLicensesFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(cardLicenses)
|
||||
if (!isCardLicensesInputFileArray(cardLicensesObject)) {
|
||||
throw new Error("input file is not cardLicensesInputFiles");
|
||||
}
|
||||
|
||||
// ライセンス・ワークタイプ・カードライセンスの登録
|
||||
await this.registerService.registLicenseAndWorktypeData(
|
||||
context,
|
||||
licensesObject,
|
||||
worktypesObject,
|
||||
cardLicensesObject
|
||||
);
|
||||
|
||||
return {};
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.dataRegist.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { RegisterController } from "./register.controller";
|
||||
import { RegisterService } from "./register.service";
|
||||
import { AccountsService } from "../accounts/accounts.service";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { LicensesRepositoryModule } from "../../repositories/licenses/licenses.repository.module";
|
||||
import { WorktypesRepositoryModule } from "../../repositories/worktypes/worktypes.repository.module";
|
||||
import { UsersRepositoryModule } from "../../repositories/users/users.repository.module";
|
||||
import { AccountsRepositoryModule } from "../../repositories/accounts/accounts.repository.module";
|
||||
import { AdB2cModule } from "../../gateways/adb2c/adb2c.module";
|
||||
import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
LicensesRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
AdB2cModule,
|
||||
BlobstorageModule,
|
||||
],
|
||||
controllers: [RegisterController],
|
||||
providers: [RegisterService, AccountsService, UsersService],
|
||||
})
|
||||
export class RegisterModule {}
|
||||
@ -0,0 +1,68 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
import { Context } from "../../common/log";
|
||||
import {
|
||||
LicensesInputFile,
|
||||
WorktypesInputFile,
|
||||
CardLicensesInputFile,
|
||||
} from "../../common/types/types";
|
||||
import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service";
|
||||
import { WorktypesRepositoryService } from "../../repositories/worktypes/worktypes.repository.service";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
@Injectable()
|
||||
export class RegisterService {
|
||||
constructor(
|
||||
private readonly licensesRepository: LicensesRepositoryService,
|
||||
private readonly worktypesRepository: WorktypesRepositoryService
|
||||
) {}
|
||||
private readonly logger = new Logger(RegisterService.name);
|
||||
|
||||
/**
|
||||
* Regist Data
|
||||
* @param inputFilePath: string
|
||||
*/
|
||||
async registLicenseAndWorktypeData(
|
||||
context: Context,
|
||||
licensesInputFiles: LicensesInputFile[],
|
||||
worktypesInputFiles: WorktypesInputFile[],
|
||||
cardlicensesInputFiles: CardLicensesInputFile[]
|
||||
): Promise<void> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.registLicenseAndWorktypeData.name
|
||||
}`
|
||||
);
|
||||
|
||||
try {
|
||||
this.logger.log("Licenses register start");
|
||||
await this.licensesRepository.insertLicenses(context, licensesInputFiles);
|
||||
this.logger.log("Licenses register end");
|
||||
|
||||
this.logger.log("Worktypes register start");
|
||||
await this.worktypesRepository.createWorktype(
|
||||
context,
|
||||
worktypesInputFiles
|
||||
);
|
||||
this.logger.log("Worktypes register end");
|
||||
|
||||
this.logger.log("CardLicenses register start");
|
||||
await this.licensesRepository.insertCardLicenses(
|
||||
context,
|
||||
cardlicensesInputFiles
|
||||
);
|
||||
this.logger.log("CardLicenses register end");
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${
|
||||
this.registLicenseAndWorktypeData.name
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RegisterRequest {
|
||||
@ApiProperty()
|
||||
inputFilePath: string;
|
||||
|
||||
}
|
||||
|
||||
export class RegisterResponse {}
|
||||
|
||||
@ -0,0 +1,178 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Req,
|
||||
HttpException,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import fs from "fs";
|
||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { Request } from "express";
|
||||
import { transferRequest, transferResponse } from "./types/types";
|
||||
import { TransferService } from "./transfer.service";
|
||||
import { makeContext } from "../../common/log";
|
||||
import { csvInputFile } from "../../common/types/types";
|
||||
import { makeErrorResponse } from "src/common/errors/makeErrorResponse";
|
||||
import {
|
||||
COUNTRY_LIST,
|
||||
MIGRATION_TYPE,
|
||||
TIERS,
|
||||
WORKTYPE_MAX_COUNT,
|
||||
RECORDING_MODE,
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
USER_ROLES,
|
||||
AUTO_INCREMENT_START,
|
||||
} from "../../constants";
|
||||
@ApiTags("transfer")
|
||||
@Controller("transfer")
|
||||
export class TransferController {
|
||||
private readonly logger = new Logger(TransferController.name);
|
||||
constructor(private readonly transferService: TransferService) {}
|
||||
|
||||
@Post()
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: transferResponse,
|
||||
description: "成功時のレスポンス",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: "想定外のサーバーエラー",
|
||||
})
|
||||
@ApiOperation({ operationId: "dataRegist" })
|
||||
async dataRegist(
|
||||
@Body() body: transferRequest,
|
||||
@Req() req: Request
|
||||
): Promise<transferResponse> {
|
||||
const context = makeContext("iko", "transfer");
|
||||
|
||||
const inputFilePath = body.inputFilePath;
|
||||
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.dataRegist.name
|
||||
} | params: { inputFilePath: ${inputFilePath}};`
|
||||
);
|
||||
try {
|
||||
// 読み込みファイルのフルパス
|
||||
const csvFileFullPath = inputFilePath + ".csv";
|
||||
|
||||
// ファイル存在チェックと読み込み
|
||||
if (!fs.existsSync(csvFileFullPath)) {
|
||||
this.logger.error(`file not exists from ${inputFilePath}`);
|
||||
throw new Error(`file not exists from ${inputFilePath}`);
|
||||
}
|
||||
|
||||
// CSVファイルを全行読み込む
|
||||
const inputFile = fs.readFileSync(csvFileFullPath, "utf-8");
|
||||
|
||||
// レコードごとに分割
|
||||
const csvInputFileLines = inputFile.split("\n");
|
||||
|
||||
// ヘッダー行を削除
|
||||
csvInputFileLines.shift();
|
||||
|
||||
// 項目ごとに切り分ける
|
||||
let csvInputFile: csvInputFile[] = [];
|
||||
csvInputFileLines.forEach((line) => {
|
||||
const data = line.split(",");
|
||||
// ダブルクォーテーションは削除
|
||||
data.forEach((value, index) => {
|
||||
data[index] = value.replace(/"/g, "");
|
||||
});
|
||||
csvInputFile.push({
|
||||
type: data[0],
|
||||
account_id: data[1],
|
||||
parent_id: data[2],
|
||||
email: data[3],
|
||||
company_name: data[4],
|
||||
first_name: data[5],
|
||||
last_name: data[6],
|
||||
country: data[7],
|
||||
state: data[8],
|
||||
start_date: new Date(data[9]),
|
||||
expired_date: new Date(data[10]),
|
||||
user_email: data[11],
|
||||
author_id: data[12],
|
||||
recording_mode: data[13],
|
||||
wt1: data[14],
|
||||
wt2: data[15],
|
||||
wt3: data[16],
|
||||
wt4: data[17],
|
||||
wt5: data[18],
|
||||
wt6: data[19],
|
||||
wt7: data[20],
|
||||
wt8: data[21],
|
||||
wt9: data[22],
|
||||
wt10: data[23],
|
||||
wt11: data[24],
|
||||
wt12: data[25],
|
||||
wt13: data[26],
|
||||
wt14: data[27],
|
||||
wt15: data[28],
|
||||
wt16: data[29],
|
||||
wt17: data[30],
|
||||
wt18: data[31],
|
||||
wt19: data[32],
|
||||
wt20: data[33],
|
||||
});
|
||||
});
|
||||
|
||||
// 各データのバリデーションチェック
|
||||
await this.transferService.validateInputData(context, csvInputFile);
|
||||
|
||||
// account_idを通番に変換し、変換前account_id: 変換後accountId配列を作成する。
|
||||
const accountIdList = csvInputFile.map((line) => line.account_id);
|
||||
const accountIdListSet = new Set(accountIdList);
|
||||
const accountIdListArray = Array.from(accountIdListSet);
|
||||
const accountIdMap = new Map<string, number>();
|
||||
accountIdListArray.forEach((accountId, index) => {
|
||||
accountIdMap.set(accountId, index + AUTO_INCREMENT_START);
|
||||
});
|
||||
// CSVファイルの変換
|
||||
const transferResponse = await this.transferService.registInputData(
|
||||
context,
|
||||
csvInputFile,
|
||||
accountIdMap
|
||||
);
|
||||
|
||||
// countryを除いた階層の再配置
|
||||
const accountsOutputFileStep1Lines =
|
||||
transferResponse.accountsOutputFileStep1Lines;
|
||||
const accountsOutputFile = await this.transferService.relocateHierarchy(
|
||||
context,
|
||||
accountsOutputFileStep1Lines
|
||||
);
|
||||
// メールアドレスの重複を削除
|
||||
// デモライセンスの削除
|
||||
// いったんこのままコミットしてテストを実施する
|
||||
|
||||
// transferResponseを4つのJSONファイルの出力する(出力先はinputと同じにする)
|
||||
const outputFilePath = body.inputFilePath;
|
||||
const usersOutputFile = transferResponse.usersOutputFileLines;
|
||||
const licensesOutputFile = transferResponse.licensesOutputFileLines;
|
||||
const worktypesOutputFile = transferResponse.worktypesOutputFileLines;
|
||||
this.transferService.outputJsonFile(
|
||||
context,
|
||||
outputFilePath,
|
||||
accountsOutputFile,
|
||||
usersOutputFile,
|
||||
licensesOutputFile,
|
||||
worktypesOutputFile
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.dataRegist.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TransferController } from "./transfer.controller";
|
||||
import { TransferService } from "./transfer.service";
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [TransferController],
|
||||
providers: [TransferService],
|
||||
})
|
||||
export class TransferModule {}
|
||||
@ -0,0 +1,372 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
import { Context } from "../../common/log";
|
||||
import {
|
||||
AccountsOutputFileStep1,
|
||||
UsersOutputFile,
|
||||
LicensesOutputFile,
|
||||
WorktypesOutputFile,
|
||||
csvInputFile,
|
||||
AccountsOutputFile,
|
||||
} from "../../common/types/types";
|
||||
import {
|
||||
COUNTRY_LIST,
|
||||
MIGRATION_TYPE,
|
||||
TIERS,
|
||||
WORKTYPE_MAX_COUNT,
|
||||
RECORDING_MODE,
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
USER_ROLES,
|
||||
SWITCH_FROM_TYPE,
|
||||
} from "src/constants";
|
||||
import { registInputDataResponse } from "./types/types";
|
||||
import fs from "fs";
|
||||
|
||||
@Injectable()
|
||||
export class TransferService {
|
||||
constructor() {}
|
||||
private readonly logger = new Logger(TransferService.name);
|
||||
|
||||
/**
|
||||
* Regist Data
|
||||
* @param OutputFilePath: string
|
||||
* @param csvInputFile: csvInputFile[]
|
||||
*/
|
||||
async registInputData(
|
||||
context: Context,
|
||||
csvInputFile: csvInputFile[],
|
||||
accountIdMap: Map<string, number>
|
||||
): Promise<registInputDataResponse> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.registInputData.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
let accountsOutputFileStep1Lines: AccountsOutputFileStep1[] = [];
|
||||
let usersOutputFileLines: UsersOutputFile[] = [];
|
||||
let licensesOutputFileLines: LicensesOutputFile[] = [];
|
||||
let worktypesOutputFileLines: WorktypesOutputFile[] = [];
|
||||
|
||||
let userIdIndex = 0;
|
||||
// csvInputFileを一行読み込みする
|
||||
csvInputFile.forEach((line) => {
|
||||
// typeが"USER"以外の場合、アカウントデータの作成を行う
|
||||
if (line.type !== MIGRATION_TYPE.USER) {
|
||||
// userIdのインクリメント
|
||||
userIdIndex = userIdIndex + 1;
|
||||
// line.countryの値を読み込みCOUNTRY_LISTのlabelからvalueに変換する
|
||||
const country = COUNTRY_LIST.find(
|
||||
(country) => country.label === line.country
|
||||
)?.value;
|
||||
// adminNameの変換(last_name + " "+ first_name)
|
||||
const adminName = `${line.last_name} ${line.first_name}`;
|
||||
|
||||
// ランダムパスワードの生成(データ登録ツール側で行うのでやらない)
|
||||
// common/password/password.tsのmakePasswordを使用
|
||||
// const autoGeneratedPassword = makePassword();
|
||||
|
||||
// parentAccountIdの設定
|
||||
// parent_idが存在する場合、accountIdMapを参照し、accountIdに変換する
|
||||
let parentAccountId: number | null = null;
|
||||
if (line.parent_id) {
|
||||
parentAccountId = accountIdMap.get(line.parent_id);
|
||||
}
|
||||
// AccountsOutputFile配列にPush
|
||||
accountsOutputFileStep1Lines.push({
|
||||
// accountIdはaccountIdMapから取得する
|
||||
accountId: accountIdMap.get(line.account_id),
|
||||
type: line.type,
|
||||
companyName: line.company_name,
|
||||
country: country,
|
||||
dealerAccountId: parentAccountId,
|
||||
adminName: adminName,
|
||||
adminMail: line.email,
|
||||
userId: userIdIndex,
|
||||
});
|
||||
} else {
|
||||
// typeが"USER"の場合、ユーザデータの作成を行う
|
||||
// userIdのインクリメント
|
||||
userIdIndex = userIdIndex + 1;
|
||||
// nameの変換
|
||||
// もしline.last_nameとline.first_nameが存在しない場合、line.emailをnameにする
|
||||
// 存在する場合は、last_name + " " + first_name
|
||||
let name = line.email;
|
||||
if (line.last_name && line.first_name) {
|
||||
name = `${line.last_name} ${line.first_name}`;
|
||||
}
|
||||
// roleの変換
|
||||
// authorIdが設定されてる場合はauthor、されていない場合は移行しないので次の行に進む
|
||||
if (line.author_id) {
|
||||
usersOutputFileLines.push({
|
||||
accountId: accountIdMap.get(line.account_id),
|
||||
userId: userIdIndex,
|
||||
name: name,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
authorId: line.author_id,
|
||||
email: line.user_email,
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// ライセンスのデータの作成を行う
|
||||
// authorIdが設定されてる場合、statusは"allocated"、allocated_user_idは対象のユーザID
|
||||
// されていない場合、statusは"reusable"、allocated_user_idはnull
|
||||
licensesOutputFileLines.push({
|
||||
expiry_date: line.expired_date.toISOString(),
|
||||
account_id: accountIdMap.get(line.account_id),
|
||||
type: SWITCH_FROM_TYPE.NONE,
|
||||
status: line.author_id
|
||||
? LICENSE_ALLOCATED_STATUS.ALLOCATED
|
||||
: LICENSE_ALLOCATED_STATUS.REUSABLE,
|
||||
allocated_user_id: line.author_id ? userIdIndex : null,
|
||||
});
|
||||
// WorktypesOutputFileの作成
|
||||
// wt1~wt20まで読み込み、account単位で作成する
|
||||
// 作成したWorktypesOutputFileを配列にPush
|
||||
for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) {
|
||||
const wt = `wt${i}`;
|
||||
if (line[wt]) {
|
||||
// 既に存在する場合は、作成しない
|
||||
if (
|
||||
worktypesOutputFileLines.find(
|
||||
(worktype) =>
|
||||
worktype.account_id === accountIdMap.get(line.account_id) &&
|
||||
worktype.custom_worktype_id === line[wt].toString()
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// つぎの行に進む
|
||||
});
|
||||
return {
|
||||
accountsOutputFileStep1Lines,
|
||||
usersOutputFileLines,
|
||||
licensesOutputFileLines,
|
||||
worktypesOutputFileLines,
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.registInputData.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 階層の付け替えを行う
|
||||
* @param accountsOutputFileStep1: AccountsOutputFileStep1[]
|
||||
* @returns AccountsOutputFile[]
|
||||
*/
|
||||
async relocateHierarchy(
|
||||
context: Context,
|
||||
accountsOutputFileStep1: AccountsOutputFileStep1[]
|
||||
): Promise<AccountsOutputFile[]> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.relocateHierarchy.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
// dealerAccountIdを検索し、typeがCountryの場合
|
||||
accountsOutputFileStep1.forEach((account) => {
|
||||
if (account.type === MIGRATION_TYPE.COUNTRY) {
|
||||
console.log(account);
|
||||
// そのacccountIdをdealerAccountIdにもつアカウント(Distributor)を検索する
|
||||
const distributor = accountsOutputFileStep1.find(
|
||||
(distributor) => distributor.accountId === account.dealerAccountId
|
||||
);
|
||||
console.log(distributor);
|
||||
if (distributor) {
|
||||
// DistributorのdealerAccountIdをBC(Countryの親)に付け替える
|
||||
distributor.dealerAccountId = account.dealerAccountId;
|
||||
}
|
||||
}
|
||||
});
|
||||
// typeがCountryのアカウントを取り除く
|
||||
accountsOutputFileStep1 = accountsOutputFileStep1.filter(
|
||||
(account) => account.type !== MIGRATION_TYPE.COUNTRY
|
||||
);
|
||||
|
||||
// typeをtierに変換し、AccountsOutputFileに変換する
|
||||
let accountsOutputFile: AccountsOutputFile[] = [];
|
||||
accountsOutputFileStep1.forEach((account) => {
|
||||
let tier = 0;
|
||||
switch (account.type) {
|
||||
case MIGRATION_TYPE.ADMINISTRATOR:
|
||||
tier = TIERS.TIER1;
|
||||
break;
|
||||
case MIGRATION_TYPE.BC:
|
||||
tier = TIERS.TIER2;
|
||||
break;
|
||||
case MIGRATION_TYPE.DISTRIBUTOR:
|
||||
tier = TIERS.TIER3;
|
||||
break;
|
||||
case MIGRATION_TYPE.DEALER:
|
||||
tier = TIERS.TIER4;
|
||||
break;
|
||||
case MIGRATION_TYPE.CUSTOMER:
|
||||
tier = TIERS.TIER5;
|
||||
break;
|
||||
}
|
||||
accountsOutputFile.push({
|
||||
accountId: account.accountId,
|
||||
type: tier,
|
||||
companyName: account.companyName,
|
||||
country: account.country,
|
||||
dealerAccountId: account.dealerAccountId,
|
||||
adminName: account.adminName,
|
||||
adminMail: account.adminMail,
|
||||
userId: account.userId,
|
||||
});
|
||||
});
|
||||
return accountsOutputFile;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.relocateHierarchy.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSONファイルの出力
|
||||
* @param outputFilePath: string
|
||||
* @param accountsOutputFile: AccountsOutputFile[]
|
||||
* @param usersOutputFile: UsersOutputFile[]
|
||||
* @param licensesOutputFile: LicensesOutputFile[]
|
||||
* @param worktypesOutputFile: WorktypesOutputFile[]
|
||||
*/
|
||||
async outputJsonFile(
|
||||
context: Context,
|
||||
outputFilePath: string,
|
||||
accountsOutputFile: AccountsOutputFile[],
|
||||
usersOutputFile: UsersOutputFile[],
|
||||
licensesOutputFile: LicensesOutputFile[],
|
||||
worktypesOutputFile: WorktypesOutputFile[]
|
||||
): Promise<void> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.outputJsonFile.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
// JSONファイルの出力を行う
|
||||
// accountsOutputFile配列の出力
|
||||
const accountsOutputFileJson = JSON.stringify(accountsOutputFile);
|
||||
fs.writeFileSync(
|
||||
`${outputFilePath}_accounts.json`,
|
||||
accountsOutputFileJson
|
||||
);
|
||||
// usersOutputFile
|
||||
const usersOutputFileJson = JSON.stringify(usersOutputFile);
|
||||
fs.writeFileSync(`${outputFilePath}_users.json`, usersOutputFileJson);
|
||||
// licensesOutputFile
|
||||
const licensesOutputFileJson = JSON.stringify(licensesOutputFile);
|
||||
fs.writeFileSync(
|
||||
`${outputFilePath}_licenses.json`,
|
||||
licensesOutputFileJson
|
||||
);
|
||||
// worktypesOutputFile
|
||||
const worktypesOutputFileJson = JSON.stringify(worktypesOutputFile);
|
||||
fs.writeFileSync(
|
||||
`${outputFilePath}_worktypes.json`,
|
||||
worktypesOutputFileJson
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.outputJsonFile.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* データのバリデーションチェック
|
||||
* @param csvInputFile: csvInputFile[]
|
||||
*/
|
||||
async validateInputData(
|
||||
context: Context,
|
||||
csvInputFile: csvInputFile[]
|
||||
): Promise<void> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.validateInputData.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
// csvInputFileのバリデーションチェックを行う
|
||||
csvInputFile.forEach((line, index) => {
|
||||
// typeのバリデーションチェック
|
||||
if (
|
||||
line.type !== MIGRATION_TYPE.ADMINISTRATOR &&
|
||||
line.type !== MIGRATION_TYPE.BC &&
|
||||
line.type !== MIGRATION_TYPE.COUNTRY &&
|
||||
line.type !== MIGRATION_TYPE.DISTRIBUTOR &&
|
||||
line.type !== MIGRATION_TYPE.DEALER &&
|
||||
line.type !== MIGRATION_TYPE.CUSTOMER &&
|
||||
line.type !== MIGRATION_TYPE.USER
|
||||
) {
|
||||
throw new HttpException(
|
||||
`type is invalid. index=${index} type=${line.type}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
// countryのバリデーションチェック
|
||||
if (line.country) {
|
||||
if (!COUNTRY_LIST.find((country) => country.label === line.country)) {
|
||||
console.log(line.country);
|
||||
throw new HttpException(
|
||||
`country is invalid. index=${index} country=${line.country}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
// mailのバリデーションチェック
|
||||
// メールアドレスの形式が正しいかどうかのチェック
|
||||
const mailRegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (line.email) {
|
||||
if (!mailRegExp.test(line.email)) {
|
||||
throw new HttpException(
|
||||
`email is invalid. index=${index} email=${line.email}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
if (line.user_email) {
|
||||
if (!mailRegExp.test(line.user_email)) {
|
||||
throw new HttpException(
|
||||
`user_email is invalid. index=${index} user_email=${line.email}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
// recording_modeの値が存在する場合
|
||||
if (line.recording_mode) {
|
||||
// recording_modeのバリデーションチェック
|
||||
if (
|
||||
line.recording_mode !== RECORDING_MODE.DS2_QP &&
|
||||
line.recording_mode !== RECORDING_MODE.DS2_SP &&
|
||||
line.recording_mode !== RECORDING_MODE.DSS
|
||||
) {
|
||||
throw new HttpException(
|
||||
`recording_mode is invalid. index=${index} recording_mode=${line.recording_mode}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.validateInputData.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import {
|
||||
AccountsOutputFileStep1,
|
||||
LicensesOutputFile,
|
||||
UsersOutputFile,
|
||||
WorktypesOutputFile,
|
||||
} from "src/common/types/types";
|
||||
|
||||
export class transferRequest {
|
||||
@ApiProperty()
|
||||
inputFilePath: string;
|
||||
}
|
||||
|
||||
export class transferResponse {}
|
||||
|
||||
export class registInputDataResponse {
|
||||
@ApiProperty()
|
||||
accountsOutputFileStep1Lines: AccountsOutputFileStep1[];
|
||||
@ApiProperty()
|
||||
usersOutputFileLines: UsersOutputFile[];
|
||||
@ApiProperty()
|
||||
licensesOutputFileLines: LicensesOutputFile[];
|
||||
@ApiProperty()
|
||||
worktypesOutputFileLines: WorktypesOutputFile[];
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { Controller, Logger } from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { UsersService } from "./users.service";
|
||||
|
||||
@ApiTags("users")
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
private readonly logger = new Logger(UsersController.name);
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AdB2cModule } from "../../gateways/adb2c/adb2c.module";
|
||||
import { UsersRepositoryModule } from "../../repositories/users/users.repository.module";
|
||||
import { UsersController } from "./users.controller";
|
||||
import { UsersService } from "./users.service";
|
||||
|
||||
@Module({
|
||||
imports: [UsersRepositoryModule, AdB2cModule],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
306
data_migration_tools/server/src/features/users/users.service.ts
Normal file
306
data_migration_tools/server/src/features/users/users.service.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
import { makePassword } from "../../common/password/password";
|
||||
import {
|
||||
AdB2cService,
|
||||
ConflictError,
|
||||
isConflictError,
|
||||
} from "../../gateways/adb2c/adb2c.service";
|
||||
import {
|
||||
User as EntityUser,
|
||||
newUser,
|
||||
} from "../../repositories/users/entity/user.entity";
|
||||
import { UsersRepositoryService } from "../../repositories/users/users.repository.service";
|
||||
import { MANUAL_RECOVERY_REQUIRED, USER_ROLES } from "../../constants";
|
||||
import { Context } from "../../common/log";
|
||||
import { UserRoles } from "../../common/types/role";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private readonly logger = new Logger(UsersService.name);
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly adB2cService: AdB2cService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates user
|
||||
* @param context
|
||||
* @param name
|
||||
* @param role
|
||||
* @param email
|
||||
* @param autoRenew
|
||||
* @param notification
|
||||
* @param accountId
|
||||
* @param userid
|
||||
* @param [authorId]
|
||||
* @param [encryption]
|
||||
* @param [encryptionPassword]
|
||||
* @param [prompt]
|
||||
* @returns user
|
||||
*/
|
||||
async createUser(
|
||||
context: Context,
|
||||
name: string,
|
||||
role: UserRoles,
|
||||
email: string,
|
||||
autoRenew: boolean,
|
||||
notification: boolean,
|
||||
accountId: number,
|
||||
userid: number,
|
||||
authorId?: string | undefined,
|
||||
encryption?: boolean | undefined,
|
||||
encryptionPassword?: string | undefined,
|
||||
prompt?: boolean | undefined
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.createUser.name} | params: { ` +
|
||||
`role: ${role}, ` +
|
||||
`autoRenew: ${autoRenew}, ` +
|
||||
`notification: ${notification}, ` +
|
||||
`accountId: ${accountId}, ` +
|
||||
`userid: ${userid}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`encryption: ${encryption}, ` +
|
||||
`prompt: ${prompt} };`
|
||||
);
|
||||
|
||||
//authorIdが重複していないかチェックする
|
||||
if (authorId) {
|
||||
let isAuthorIdDuplicated = false;
|
||||
try {
|
||||
isAuthorIdDuplicated = await this.usersRepository.existsAuthorId(
|
||||
context,
|
||||
accountId,
|
||||
authorId
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
if (isAuthorIdDuplicated) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010302"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ランダムなパスワードを生成する
|
||||
const ramdomPassword = makePassword();
|
||||
|
||||
//Azure AD B2Cにユーザーを新規登録する
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
try {
|
||||
this.logger.log(`name=${name}`);
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
context,
|
||||
email,
|
||||
ramdomPassword,
|
||||
name
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] create externalUser failed`
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// メールアドレス重複エラー
|
||||
if (isConflictError(externalUser)) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010301"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
//Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する
|
||||
let newUser: EntityUser;
|
||||
|
||||
try {
|
||||
//roleに応じてユーザー情報を作成する
|
||||
const newUserInfo = this.createNewUserInfo(
|
||||
context,
|
||||
userid,
|
||||
role,
|
||||
accountId,
|
||||
externalUser.sub,
|
||||
autoRenew,
|
||||
notification,
|
||||
authorId,
|
||||
encryption,
|
||||
encryptionPassword,
|
||||
prompt
|
||||
);
|
||||
// ユーザ作成
|
||||
newUser = await this.usersRepository.createNormalUser(
|
||||
context,
|
||||
newUserInfo
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(`[${context.getTrackingId()}]create user failed`);
|
||||
//リカバリー処理
|
||||
//Azure AD B2Cに登録したユーザー情報を削除する
|
||||
await this.deleteB2cUser(externalUser.sub, context);
|
||||
|
||||
switch (e.code) {
|
||||
case "ER_DUP_ENTRY":
|
||||
//AuthorID重複エラー
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010302"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createUser.name}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Azure AD B2Cに登録したユーザー情報を削除する
|
||||
// TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
||||
private async deleteB2cUser(externalUserId: string, context: Context) {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteB2cUser.name
|
||||
} | params: { externalUserId: ${externalUserId} }`
|
||||
);
|
||||
try {
|
||||
await this.adB2cService.deleteUser(externalUserId, context);
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] delete externalUser: ${externalUserId}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteB2cUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DBに登録したユーザー情報を削除する
|
||||
private async deleteUser(userId: number, context: Context) {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteUser.name
|
||||
} | params: { userId: ${userId} }`
|
||||
);
|
||||
try {
|
||||
await this.usersRepository.deleteNormalUser(context, userId);
|
||||
this.logger.log(`[${context.getTrackingId()}] delete user: ${userId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete user: ${userId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// roleを受け取って、roleに応じたnewUserを作成して返却する
|
||||
private createNewUserInfo(
|
||||
context: Context,
|
||||
id: number,
|
||||
role: UserRoles,
|
||||
accountId: number,
|
||||
externalId: string,
|
||||
autoRenew: boolean,
|
||||
notification: boolean,
|
||||
authorId?: string | undefined,
|
||||
encryption?: boolean | undefined,
|
||||
encryptionPassword?: string | undefined,
|
||||
prompt?: boolean | undefined
|
||||
): newUser {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.createNewUserInfo.name
|
||||
} | params: { ` +
|
||||
`id: ${id}, ` +
|
||||
`role: ${role}, ` +
|
||||
`accountId: ${accountId}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`autoRenew: ${autoRenew}, ` +
|
||||
`notification: ${notification}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`encryption: ${encryption}, ` +
|
||||
`prompt: ${prompt} };`
|
||||
);
|
||||
try {
|
||||
switch (role) {
|
||||
case USER_ROLES.NONE:
|
||||
case USER_ROLES.TYPIST:
|
||||
return {
|
||||
id,
|
||||
account_id: accountId,
|
||||
external_id: externalId,
|
||||
auto_renew: autoRenew,
|
||||
notification,
|
||||
role,
|
||||
accepted_dpa_version: null,
|
||||
accepted_eula_version: null,
|
||||
accepted_privacy_notice_version: null,
|
||||
encryption: false,
|
||||
encryption_password: null,
|
||||
prompt: false,
|
||||
author_id: null,
|
||||
};
|
||||
case USER_ROLES.AUTHOR:
|
||||
return {
|
||||
id,
|
||||
account_id: accountId,
|
||||
external_id: externalId,
|
||||
auto_renew: autoRenew,
|
||||
notification,
|
||||
role,
|
||||
author_id: authorId ?? null,
|
||||
encryption: encryption ?? false,
|
||||
encryption_password: encryptionPassword ?? null,
|
||||
prompt: prompt ?? false,
|
||||
accepted_dpa_version: null,
|
||||
accepted_eula_version: null,
|
||||
accepted_privacy_notice_version: null,
|
||||
};
|
||||
default:
|
||||
//不正なroleが指定された場合はログを出力してエラーを返す
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] [NOT IMPLEMENT] [RECOVER] role: ${role}`
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
return e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createNewUserInfo.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,8 @@ import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { AdB2cResponse, AdB2cUser } from "./types/types";
|
||||
import { isPromiseRejectedResult } from "./utils/utils";
|
||||
import { Context } from "../../common/log";
|
||||
import { ADB2C_SIGN_IN_TYPE } from "../../constants";
|
||||
|
||||
export type ConflictError = {
|
||||
reason: "email";
|
||||
@ -27,9 +29,12 @@ export const isConflictError = (arg: unknown): arg is ConflictError => {
|
||||
@Injectable()
|
||||
export class AdB2cService {
|
||||
private readonly logger = new Logger(AdB2cService.name);
|
||||
private readonly tenantName: string;
|
||||
private graphClient: Client;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.tenantName = this.configService.getOrThrow<string>("TENANT_NAME");
|
||||
|
||||
// ADB2Cへの認証情報
|
||||
const credential = new ClientSecretCredential(
|
||||
this.configService.getOrThrow<string>("ADB2C_TENANT_ID"),
|
||||
@ -42,13 +47,70 @@ export class AdB2cService {
|
||||
|
||||
this.graphClient = Client.initWithMiddleware({ authProvider });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates user AzureADB2Cにユーザーを追加する
|
||||
* @param email 管理ユーザーのメールアドレス
|
||||
* @param password 管理ユーザーのパスワード
|
||||
* @param username 管理ユーザーの名前
|
||||
* @returns user
|
||||
*/
|
||||
async createUser(
|
||||
context: Context,
|
||||
email: string,
|
||||
password: string,
|
||||
username: string
|
||||
): Promise<{ sub: string } | ConflictError> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.createUser.name}`
|
||||
);
|
||||
try {
|
||||
// ユーザをADB2Cに登録
|
||||
const newUser = await this.graphClient.api("users/").post({
|
||||
accountEnabled: true,
|
||||
displayName: username,
|
||||
passwordPolicies: "DisableStrongPassword",
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password: password,
|
||||
},
|
||||
identities: [
|
||||
{
|
||||
signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: `${this.tenantName}.onmicrosoft.com`,
|
||||
issuerAssignedId: email,
|
||||
},
|
||||
],
|
||||
});
|
||||
return { sub: newUser.id };
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
if (e?.statusCode === 400 && e?.body) {
|
||||
const error = JSON.parse(e.body);
|
||||
|
||||
// エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す
|
||||
if (error?.details?.find((x) => x.code === "ObjectConflict")) {
|
||||
return { reason: "email", message: "ObjectConflict" };
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets users
|
||||
* @param externalIds
|
||||
* @returns users
|
||||
*/
|
||||
async getUsers(): Promise<AdB2cUser[]> {
|
||||
this.logger.log(`[IN] ${this.getUsers.name}`);
|
||||
async getUsers(
|
||||
context: Context
|
||||
): Promise<{ users: AdB2cUser[]; hasNext: boolean }> {
|
||||
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
|
||||
|
||||
try {
|
||||
const res: AdB2cResponse = await this.graphClient
|
||||
@ -57,7 +119,7 @@ export class AdB2cService {
|
||||
.filter(`creationType eq 'LocalAccount'`)
|
||||
.get();
|
||||
|
||||
return res.value;
|
||||
return { users: res.value, hasNext: !!res["@odata.nextLink"] };
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
const { statusCode } = e;
|
||||
@ -71,13 +133,53 @@ export class AdB2cService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を削除する
|
||||
* @param externalId 外部ユーザーID
|
||||
* @param context コンテキスト
|
||||
*/
|
||||
async deleteUser(externalId: string, context: Context): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteUser.name
|
||||
} | params: { externalId: ${externalId} };`
|
||||
);
|
||||
|
||||
try {
|
||||
// https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example
|
||||
await this.graphClient.api(`users/${externalId}`).delete();
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] [ADB2C DELETE] externalId: ${externalId}`
|
||||
);
|
||||
|
||||
// キャッシュからも削除する
|
||||
// 移行ツール特別対応:キャッシュ登録は行わないので削除も不要
|
||||
/*
|
||||
try {
|
||||
await this.redisService.del(context, makeADB2CKey(externalId));
|
||||
} catch (e) {
|
||||
// キャッシュからの削除に失敗しても、ADB2Cからの削除は成功しているため例外はスローしない
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
}
|
||||
*/
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を削除する(複数)
|
||||
* @param externalIds 外部ユーザーID
|
||||
*/
|
||||
async deleteUsers(externalIds: string[]): Promise<void> {
|
||||
async deleteUsers(context: Context, externalIds: string[]): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN]${this.deleteUsers.name} | params: { externalIds: ${externalIds} };`
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteUsers.name
|
||||
} | params: { externalIds: ${externalIds} };`
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import {
|
||||
ContainerClient,
|
||||
BlobServiceClient,
|
||||
StorageSharedKeyCredential,
|
||||
} from "@azure/storage-blob";
|
||||
import {
|
||||
BLOB_STORAGE_REGION_AU,
|
||||
BLOB_STORAGE_REGION_EU,
|
||||
BLOB_STORAGE_REGION_US,
|
||||
} from "../../constants";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { Context } from "../../common/log";
|
||||
@Injectable()
|
||||
export class BlobstorageService {
|
||||
private readonly logger = new Logger(BlobstorageService.name);
|
||||
@ -41,12 +47,52 @@ export class BlobstorageService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates container
|
||||
* @param context
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @returns container
|
||||
*/
|
||||
async createContainer(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.createContainer.name
|
||||
} | params: { ` + `accountId: ${accountId} };`
|
||||
);
|
||||
|
||||
// 国に応じたリージョンでコンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(
|
||||
context,
|
||||
accountId,
|
||||
country
|
||||
);
|
||||
|
||||
try {
|
||||
// コンテナ作成
|
||||
await containerClient.create();
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createContainer.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* すべてのコンテナを削除します。
|
||||
* @returns containers
|
||||
*/
|
||||
async deleteContainers(): Promise<void> {
|
||||
this.logger.log(`[IN] ${this.deleteContainers.name}`);
|
||||
async deleteContainers(context: Context): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.deleteContainers.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
for await (const container of this.blobServiceClientAU.listContainers({
|
||||
@ -80,4 +126,32 @@ export class BlobstorageService {
|
||||
this.logger.log(`[OUT] ${this.deleteContainers.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets container client
|
||||
* @param companyName
|
||||
* @returns container client
|
||||
*/
|
||||
private getContainerClient(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
country: string
|
||||
): ContainerClient {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.getContainerClient.name
|
||||
} | params: { ` + `accountId: ${accountId} };`
|
||||
);
|
||||
|
||||
const containerName = `account-${accountId}`;
|
||||
if (BLOB_STORAGE_REGION_US.includes(country)) {
|
||||
return this.blobServiceClientUS.getContainerClient(containerName);
|
||||
} else if (BLOB_STORAGE_REGION_AU.includes(country)) {
|
||||
return this.blobServiceClientAU.getContainerClient(containerName);
|
||||
} else if (BLOB_STORAGE_REGION_EU.includes(country)) {
|
||||
return this.blobServiceClientEU.getContainerClient(containerName);
|
||||
} else {
|
||||
throw new Error("invalid country");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,164 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
DataSource,
|
||||
} from 'typeorm';
|
||||
import { User } from '../users/entity/user.entity';
|
||||
import { Account } from './entity/account.entity';
|
||||
import {
|
||||
getDirection,
|
||||
getTaskListSortableAttribute,
|
||||
} from '../../common/types/sort/util';
|
||||
import { SortCriteria } from "../sort_criteria/entity/sort_criteria.entity";
|
||||
import {
|
||||
insertEntity,
|
||||
updateEntity,
|
||||
deleteEntity,
|
||||
} from '../../common/repository';
|
||||
import { Context } from '../../common/log';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsRepositoryService {
|
||||
// クエリログにコメントを出力するかどうか
|
||||
private readonly isCommentOut = process.env.STAGE !== "local";
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
* プライマリ管理者とアカウント、ソート条件を同時に作成する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param dealerAccountId
|
||||
* @param tier
|
||||
* @param adminExternalUserId
|
||||
* @param adminUserRole
|
||||
* @param accountId
|
||||
* @param userId
|
||||
* @param adminUserAcceptedEulaVersion
|
||||
* @param adminUserAcceptedPrivacyNoticeVersion
|
||||
* @param adminUserAcceptedDpaVersion
|
||||
* @returns account/admin user
|
||||
*/
|
||||
async createAccount(
|
||||
context: Context,
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | undefined,
|
||||
tier: number,
|
||||
adminExternalUserId: string,
|
||||
adminUserRole: string,
|
||||
accountId: number,
|
||||
userId: number,
|
||||
adminUserAcceptedEulaVersion?: string,
|
||||
adminUserAcceptedPrivacyNoticeVersion?: string,
|
||||
adminUserAcceptedDpaVersion?: string
|
||||
): Promise<{ newAccount: Account; adminUser: User }> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const account = new Account();
|
||||
{
|
||||
account.id = accountId;
|
||||
account.parent_account_id = dealerAccountId ?? null;
|
||||
account.company_name = companyName;
|
||||
account.country = country;
|
||||
account.tier = tier;
|
||||
}
|
||||
const accountsRepo = entityManager.getRepository(Account);
|
||||
const newAccount = accountsRepo.create(account);
|
||||
const persistedAccount = await insertEntity(
|
||||
Account,
|
||||
accountsRepo,
|
||||
newAccount,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
// 作成されたAccountのIDを使用してユーザーを作成
|
||||
const user = new User();
|
||||
{
|
||||
user.id = userId;
|
||||
user.account_id = persistedAccount.id;
|
||||
user.external_id = adminExternalUserId;
|
||||
user.role = adminUserRole;
|
||||
user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
|
||||
user.accepted_privacy_notice_version =
|
||||
adminUserAcceptedPrivacyNoticeVersion ?? null;
|
||||
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
|
||||
}
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const newUser = usersRepo.create(user);
|
||||
const persistedUser = await insertEntity(
|
||||
User,
|
||||
usersRepo,
|
||||
newUser,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
// アカウントに管理者を設定して更新
|
||||
persistedAccount.primary_admin_user_id = persistedUser.id;
|
||||
|
||||
const result = await updateEntity(
|
||||
accountsRepo,
|
||||
{ id: persistedAccount.id },
|
||||
persistedAccount,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
// 想定外の更新が行われた場合はロールバックを行った上でエラー送出
|
||||
if (result.affected !== 1) {
|
||||
throw new Error(`invalid update. result.affected=${result.affected}`);
|
||||
}
|
||||
|
||||
// ユーザーのタスクソート条件を作成
|
||||
const sortCriteria = new SortCriteria();
|
||||
{
|
||||
sortCriteria.parameter = getTaskListSortableAttribute("JOB_NUMBER");
|
||||
sortCriteria.direction = getDirection("ASC");
|
||||
sortCriteria.user_id = persistedUser.id;
|
||||
}
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
const newSortCriteria = sortCriteriaRepo.create(sortCriteria);
|
||||
await insertEntity(
|
||||
SortCriteria,
|
||||
sortCriteriaRepo,
|
||||
newSortCriteria,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
return { newAccount: persistedAccount, adminUser: persistedUser };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* プライマリ管理者とアカウント、ソート条件を同時に削除する
|
||||
* @param accountId
|
||||
* @returns delete
|
||||
*/
|
||||
async deleteAccount(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
userId: number
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const accountsRepo = entityManager.getRepository(Account);
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
// ソート条件を削除
|
||||
await deleteEntity(
|
||||
sortCriteriaRepo,
|
||||
{ user_id: userId },
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
// プライマリ管理者を削除
|
||||
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
|
||||
// アカウントを削除
|
||||
await deleteEntity(
|
||||
accountsRepo,
|
||||
{ id: accountId },
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import { bigintTransformer } from '../../../common/entity';
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'accounts' })
|
||||
export class Account {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
parent_account_id: number | null;
|
||||
|
||||
@Column()
|
||||
tier: number;
|
||||
|
||||
@Column()
|
||||
country: string;
|
||||
|
||||
@Column({ default: false })
|
||||
delegation_permission: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
locked: boolean;
|
||||
|
||||
@Column()
|
||||
company_name: string;
|
||||
|
||||
@Column({ default: false })
|
||||
verified: boolean;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
primary_admin_user_id: number | null;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
secondary_admin_user_id: number | null;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
active_worktype_id: number | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
deleted_at: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
created_by: string | null;
|
||||
|
||||
@CreateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
updated_by: string | null;
|
||||
|
||||
@UpdateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@OneToMany(() => User, (user) => user.id)
|
||||
user: User[] | null;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// アカウント未発見エラー
|
||||
export class AccountNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AccountNotFoundError';
|
||||
}
|
||||
}
|
||||
// ディーラーアカウント未存在エラー
|
||||
export class DealerAccountNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'DealerAccountNotFoundError';
|
||||
}
|
||||
}
|
||||
// 管理者ユーザ未存在エラー
|
||||
export class AdminUserNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AdminUserNotFoundError';
|
||||
}
|
||||
}
|
||||
// アカウントロックエラー
|
||||
export class AccountLockedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AccountLockedError';
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,15 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { DataSource } from "typeorm";
|
||||
import { logger } from "@azure/identity";
|
||||
import { Account } from "./entity/account.entity";
|
||||
import { AUTO_INCREMENT_START } from "../../constants";
|
||||
import { Term } from "./entity/term.entity";
|
||||
import { insertEntities } from "../../common/repository";
|
||||
import { Context } from "../../common/log";
|
||||
|
||||
@Injectable()
|
||||
export class DeleteRepositoryService {
|
||||
// クエリログにコメントを出力するかどうか
|
||||
private readonly isCommentOut = process.env.STAGE !== "local";
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
@ -54,4 +58,35 @@ export class DeleteRepositoryService {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初期データを挿入する
|
||||
* @returns data
|
||||
*/
|
||||
async insertInitData(context: Context): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const termRepo = entityManager.getRepository(Term);
|
||||
|
||||
// ワークフローのデータ作成
|
||||
const newTarmDpa = new Term();
|
||||
newTarmDpa.document_type = "DPA";
|
||||
newTarmDpa.version = "V0.1";
|
||||
const newTarmEula = new Term();
|
||||
newTarmEula.document_type = "EULA";
|
||||
newTarmEula.version = "V0.1";
|
||||
const newTarmPrivacyNotice = new Term();
|
||||
newTarmPrivacyNotice.document_type = "PrivacyNotice";
|
||||
newTarmPrivacyNotice.version = "V0.1";
|
||||
|
||||
const initTerms = [newTarmDpa, newTarmEula, newTarmPrivacyNotice];
|
||||
|
||||
await insertEntities(
|
||||
Term,
|
||||
termRepo,
|
||||
initTerms,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { bigintTransformer } from '../../../common/entity';
|
||||
|
||||
|
||||
@Entity({ name: 'licenses' })
|
||||
export class License {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
expiry_date: Date | null;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
status: string;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
allocated_user_id: number | null;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
order_id: number | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
deleted_at: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
delete_order_id: number | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
created_by: string | null;
|
||||
|
||||
@CreateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
})
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
updated_by: string | null;
|
||||
|
||||
@UpdateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
})
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
@Entity({ name: 'license_allocation_history' })
|
||||
export class LicenseAllocationHistory {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
user_id: number;
|
||||
|
||||
@Column()
|
||||
license_id: number;
|
||||
|
||||
@Column()
|
||||
is_allocated: boolean;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
executed_at: Date;
|
||||
|
||||
@Column()
|
||||
switch_from_type: string;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
deleted_at: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
created_by: string | null;
|
||||
|
||||
@CreateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
})
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
updated_by: string | null;
|
||||
|
||||
@UpdateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
})
|
||||
updated_at: Date;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Entity({ name: "card_licenses" })
|
||||
export class CardLicense {
|
||||
@PrimaryGeneratedColumn()
|
||||
license_id: number;
|
||||
|
||||
@Column()
|
||||
issue_id: number;
|
||||
|
||||
@Column()
|
||||
card_license_key: string;
|
||||
|
||||
@Column({ nullable: true, type: "datetime" })
|
||||
activated_at: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: "datetime" })
|
||||
created_by: string | null;
|
||||
|
||||
@CreateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: "datetime",
|
||||
})
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true, type: "datetime" })
|
||||
updated_by: string | null;
|
||||
|
||||
@UpdateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: "datetime",
|
||||
})
|
||||
updated_at: Date;
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
// POナンバーがすでに存在するエラー
|
||||
export class PoNumberAlreadyExistError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'PoNumberAlreadyExistError';
|
||||
}
|
||||
}
|
||||
|
||||
// 取り込むカードライセンスが存在しないエラー
|
||||
export class LicenseNotExistError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LicenseNotExistError';
|
||||
}
|
||||
}
|
||||
|
||||
// 取り込むライセンスが既に取り込み済みのエラー
|
||||
export class LicenseKeyAlreadyActivatedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LicenseKeyAlreadyActivatedError';
|
||||
}
|
||||
}
|
||||
|
||||
// 注文不在エラー
|
||||
export class OrderNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'OrderNotFoundError';
|
||||
}
|
||||
}
|
||||
// 注文発行済エラー
|
||||
export class AlreadyIssuedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AlreadyIssuedError';
|
||||
}
|
||||
}
|
||||
// ライセンス不足エラー
|
||||
export class LicensesShortageError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LicensesShortageError';
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス有効期限切れエラー
|
||||
export class LicenseExpiredError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LicenseExpiredError';
|
||||
}
|
||||
}
|
||||
// ライセンス割り当て不可エラー
|
||||
export class LicenseUnavailableError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LicenseUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス割り当て解除済みエラー
|
||||
export class LicenseAlreadyDeallocatedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LicenseAlreadyDeallocatedError';
|
||||
}
|
||||
}
|
||||
|
||||
// 注文キャンセル失敗エラー
|
||||
export class CancelOrderFailedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CancelOrderFailedError';
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
|
||||
export class AlreadyLicenseStatusChangedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AlreadyLicenseStatusChangedError';
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
|
||||
export class CancellationPeriodExpiredError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CancellationPeriodExpiredError';
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
|
||||
export class AlreadyLicenseAllocatedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AlreadyLicenseAllocatedError';
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス未割当エラー
|
||||
export class LicenseNotAllocatedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'LicenseNotAllocatedError';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import {
|
||||
CardLicense,
|
||||
License,
|
||||
LicenseAllocationHistory,
|
||||
} from "./entity/license.entity";
|
||||
import { LicensesRepositoryService } from "./licenses.repository.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([License, CardLicense, LicenseAllocationHistory]),
|
||||
],
|
||||
providers: [LicensesRepositoryService],
|
||||
exports: [LicensesRepositoryService],
|
||||
})
|
||||
export class LicensesRepositoryModule {}
|
||||
@ -0,0 +1,130 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { DataSource, In } from "typeorm";
|
||||
import {
|
||||
License,
|
||||
LicenseAllocationHistory,
|
||||
CardLicense,
|
||||
} from "./entity/license.entity";
|
||||
import { insertEntities } from "../../common/repository";
|
||||
import { Context } from "../../common/log";
|
||||
import {
|
||||
LicensesInputFile,
|
||||
CardLicensesInputFile,
|
||||
} from "../../common/types/types";
|
||||
|
||||
@Injectable()
|
||||
export class LicensesRepositoryService {
|
||||
//クエリログにコメントを出力するかどうか
|
||||
private readonly isCommentOut = process.env.STAGE !== "local";
|
||||
constructor(private dataSource: DataSource) {}
|
||||
private readonly logger = new Logger(LicensesRepositoryService.name);
|
||||
|
||||
/**
|
||||
* ライセンスを登録する
|
||||
* @context Context
|
||||
* @param licensesInputFiles
|
||||
*/
|
||||
async insertLicenses(
|
||||
context: Context,
|
||||
licensesInputFiles: LicensesInputFile[]
|
||||
): Promise<{}> {
|
||||
const nowDate = new Date();
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const licenseRepo = entityManager.getRepository(License);
|
||||
|
||||
let newLicenses: License[] = [];
|
||||
licensesInputFiles.forEach((licensesInputFile) => {
|
||||
const license = new License();
|
||||
license.account_id = licensesInputFile.account_id;
|
||||
license.status = licensesInputFile.status;
|
||||
license.type = licensesInputFile.type;
|
||||
license.expiry_date = (licensesInputFile.expiry_date) ? new Date(licensesInputFile.expiry_date) : null;
|
||||
if (licensesInputFile.allocated_user_id) {
|
||||
license.allocated_user_id = licensesInputFile.allocated_user_id;
|
||||
}
|
||||
newLicenses.push(license);
|
||||
});
|
||||
|
||||
// ライセンステーブルを登録
|
||||
const insertedlicenses = await insertEntities(
|
||||
License,
|
||||
licenseRepo,
|
||||
newLicenses,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
const licenseAllocationHistoryRepo = entityManager.getRepository(
|
||||
LicenseAllocationHistory
|
||||
);
|
||||
// ユーザに割り当てた場合はライセンス割り当て履歴にも登録
|
||||
let newLicenseAllocationHistories: LicenseAllocationHistory[] = [];
|
||||
insertedlicenses.forEach((insertedlicense) => {
|
||||
if (insertedlicense.allocated_user_id) {
|
||||
const licenseAllocationHistory = new LicenseAllocationHistory();
|
||||
licenseAllocationHistory.user_id = insertedlicense.allocated_user_id;
|
||||
licenseAllocationHistory.license_id = insertedlicense.id;
|
||||
licenseAllocationHistory.is_allocated = true;
|
||||
licenseAllocationHistory.account_id = insertedlicense.account_id;
|
||||
licenseAllocationHistory.switch_from_type = "NONE";
|
||||
licenseAllocationHistory.executed_at = insertedlicense.created_at;
|
||||
|
||||
newLicenseAllocationHistories.push(licenseAllocationHistory);
|
||||
}
|
||||
});
|
||||
|
||||
// ライセンス割り当てテーブルを登録
|
||||
await insertEntities(
|
||||
LicenseAllocationHistory,
|
||||
licenseAllocationHistoryRepo,
|
||||
newLicenseAllocationHistories,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* カードライセンスを登録する
|
||||
* @context Context
|
||||
* @param cardLicensesInputFiles
|
||||
*/
|
||||
async insertCardLicenses(
|
||||
context: Context,
|
||||
cardLicensesInputFiles: CardLicensesInputFile[]
|
||||
): Promise<{}> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const cardLicenseRepo = entityManager.getRepository(CardLicense);
|
||||
|
||||
|
||||
let newCardLicenses: CardLicense[] = [];
|
||||
cardLicensesInputFiles.forEach((cardLicensesInputFile) => {
|
||||
const cardLicense = new CardLicense();
|
||||
cardLicense.license_id = cardLicensesInputFile.license_id;
|
||||
cardLicense.issue_id = cardLicensesInputFile.issue_id;
|
||||
cardLicense.card_license_key = cardLicensesInputFile.card_license_key;
|
||||
cardLicense.activated_at = (cardLicensesInputFile.activated_at) ? new Date(cardLicensesInputFile.activated_at) : null;
|
||||
cardLicense.created_at = (cardLicensesInputFile.created_at) ? new Date(cardLicensesInputFile.created_at) : null;
|
||||
cardLicense.created_by = cardLicensesInputFile.created_by;
|
||||
cardLicense.updated_at = (cardLicensesInputFile.updated_at) ? new Date(cardLicensesInputFile.updated_at) : null;
|
||||
cardLicense.updated_by = cardLicensesInputFile.updated_by;
|
||||
|
||||
newCardLicenses.push(cardLicense);
|
||||
});
|
||||
|
||||
const query = cardLicenseRepo
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(CardLicense);
|
||||
if (this.isCommentOut) {
|
||||
query.comment(`${context.getTrackingId()}_${new Date().toUTCString()}`);
|
||||
}
|
||||
query.values(newCardLicenses).execute();
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'sort_criteria' })
|
||||
export class SortCriteria {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
user_id: number;
|
||||
|
||||
@Column()
|
||||
parameter: string;
|
||||
|
||||
@Column()
|
||||
direction: string;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SortCriteria } from './entity/sort_criteria.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([SortCriteria])],
|
||||
})
|
||||
export class SortCriteriaRepositoryModule {}
|
||||
@ -0,0 +1,97 @@
|
||||
import { Account } from '../../../repositories/accounts/entity/account.entity';
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} 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, type: 'varchar' })
|
||||
author_id: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_eula_version: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_privacy_notice_version: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_dpa_version: string | null;
|
||||
|
||||
@Column({ default: false })
|
||||
email_verified: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
auto_renew: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
notification: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
encryption: boolean;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
encryption_password: string | null;
|
||||
|
||||
@Column({ default: false })
|
||||
prompt: boolean;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
deleted_at: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
created_by: string | null;
|
||||
|
||||
@CreateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
updated_by: string | null;
|
||||
|
||||
@UpdateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.user, {
|
||||
createForeignKeyConstraints: false,
|
||||
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
account: Account | null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
export type newUser = Omit<
|
||||
User,
|
||||
| 'deleted_at'
|
||||
| 'created_at'
|
||||
| 'updated_at'
|
||||
| 'updated_by'
|
||||
| 'created_by'
|
||||
| 'account'
|
||||
| 'license'
|
||||
| 'userGroupMembers'
|
||||
| 'email_verified'
|
||||
>;
|
||||
@ -0,0 +1,56 @@
|
||||
// Email検証済みエラー
|
||||
export class EmailAlreadyVerifiedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'EmailAlreadyVerifiedError';
|
||||
}
|
||||
}
|
||||
// ユーザー未発見エラー
|
||||
export class UserNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'UserNotFoundError';
|
||||
}
|
||||
}
|
||||
// AuthorID重複エラー
|
||||
export class AuthorIdAlreadyExistsError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AuthorIdAlreadyExistsError';
|
||||
}
|
||||
}
|
||||
// 不正なRole変更エラー
|
||||
export class InvalidRoleChangeError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'InvalidRoleChangeError';
|
||||
}
|
||||
}
|
||||
// 暗号化パスワード不足エラー
|
||||
export class EncryptionPasswordNeedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'EncryptionPasswordNeedError';
|
||||
}
|
||||
}
|
||||
// 利用規約バージョン情報不在エラー
|
||||
export class TermInfoNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'TermInfoNotFoundError';
|
||||
}
|
||||
}
|
||||
// 利用規約バージョンパラメータ不在エラー
|
||||
export class UpdateTermsVersionNotSetError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'UpdateTermsVersionNotSetError';
|
||||
}
|
||||
}
|
||||
// 代行操作不許可エラー
|
||||
export class DelegationNotAllowedError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'DelegationNotAllowedError';
|
||||
}
|
||||
}
|
||||
@ -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,141 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User, newUser } from './entity/user.entity';
|
||||
import {
|
||||
DataSource,
|
||||
} from 'typeorm';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
import {
|
||||
getDirection,
|
||||
getTaskListSortableAttribute,
|
||||
} from '../../common/types/sort/util';
|
||||
import { Context } from '../../common/log';
|
||||
import {
|
||||
insertEntity,
|
||||
deleteEntity,
|
||||
} from '../../common/repository';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepositoryService {
|
||||
// クエリログにコメントを出力するかどうか
|
||||
private readonly isCommentOut = process.env.STAGE !== "local";
|
||||
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
* 一般ユーザーを作成する
|
||||
* @param user
|
||||
* @returns User
|
||||
*/
|
||||
async createNormalUser(context: Context, user: newUser): Promise<User> {
|
||||
const {
|
||||
id,
|
||||
account_id: accountId,
|
||||
external_id: externalUserId,
|
||||
role,
|
||||
auto_renew,
|
||||
notification,
|
||||
author_id,
|
||||
accepted_eula_version,
|
||||
accepted_dpa_version,
|
||||
encryption,
|
||||
encryption_password: encryptionPassword,
|
||||
prompt,
|
||||
} = user;
|
||||
const userEntity = new User();
|
||||
|
||||
userEntity.id = id;
|
||||
userEntity.role = role;
|
||||
userEntity.account_id = accountId;
|
||||
userEntity.external_id = externalUserId;
|
||||
userEntity.auto_renew = auto_renew;
|
||||
userEntity.notification = notification;
|
||||
userEntity.author_id = author_id;
|
||||
userEntity.accepted_eula_version = accepted_eula_version;
|
||||
userEntity.accepted_dpa_version = accepted_dpa_version;
|
||||
userEntity.encryption = encryption;
|
||||
userEntity.encryption_password = encryptionPassword;
|
||||
userEntity.prompt = prompt;
|
||||
userEntity.email_verified = true;
|
||||
|
||||
const createdEntity = await this.dataSource.transaction(
|
||||
async (entityManager) => {
|
||||
const repo = entityManager.getRepository(User);
|
||||
const newUser = repo.create(userEntity);
|
||||
const persisted = await insertEntity(
|
||||
User,
|
||||
repo,
|
||||
newUser,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
// ユーザーのタスクソート条件を作成
|
||||
const sortCriteria = new SortCriteria();
|
||||
{
|
||||
sortCriteria.parameter = getTaskListSortableAttribute("JOB_NUMBER");
|
||||
sortCriteria.direction = getDirection("ASC");
|
||||
sortCriteria.user_id = persisted.id;
|
||||
}
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
const newSortCriteria = sortCriteriaRepo.create(sortCriteria);
|
||||
await insertEntity(
|
||||
SortCriteria,
|
||||
sortCriteriaRepo,
|
||||
newSortCriteria,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
return persisted;
|
||||
}
|
||||
);
|
||||
return createdEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthorIdが既に存在するか確認する
|
||||
* @param user
|
||||
* @returns 存在する:true 存在しない:false
|
||||
*/
|
||||
async existsAuthorId(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
authorId: string
|
||||
): Promise<boolean> {
|
||||
const user = await this.dataSource.getRepository(User).findOne({
|
||||
where: [
|
||||
{
|
||||
account_id: accountId,
|
||||
author_id: authorId,
|
||||
},
|
||||
],
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserID指定のユーザーとソート条件を同時に削除する
|
||||
* @param userId
|
||||
* @returns delete
|
||||
*/
|
||||
async deleteNormalUser(context: Context, userId: number): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
// ソート条件を削除
|
||||
await deleteEntity(
|
||||
sortCriteriaRepo,
|
||||
{ user_id: userId },
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
// プライマリ管理者を削除
|
||||
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Worktype } from './worktype.entity';
|
||||
|
||||
@Entity({ name: 'option_items' })
|
||||
export class OptionItem {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@Column()
|
||||
worktype_id: number;
|
||||
@Column()
|
||||
item_label: string;
|
||||
@Column()
|
||||
default_value_type: string;
|
||||
@Column()
|
||||
initial_value: string;
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
created_by: string | null;
|
||||
@CreateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date | null;
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
updated_by: string | null;
|
||||
@UpdateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date | null;
|
||||
|
||||
@ManyToOne(() => Worktype, (worktype) => worktype.id)
|
||||
@JoinColumn({ name: 'worktype_id' })
|
||||
worktype: Worktype;
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'worktypes' })
|
||||
export class Worktype {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
custom_worktype_id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
description: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
deleted_at: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
created_by: string | null;
|
||||
|
||||
@CreateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
updated_by: string | null;
|
||||
|
||||
@UpdateDateColumn({
|
||||
default: () => "datetime('now', 'localtime')",
|
||||
type: 'datetime',
|
||||
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// WorktypeID重複エラー
|
||||
export class WorktypeIdAlreadyExistsError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'WorktypeIdAlreadyExistsError';
|
||||
}
|
||||
}
|
||||
// WorktypeID登録上限エラー
|
||||
export class WorktypeIdMaxCountError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'WorktypeIdMaxCountError';
|
||||
}
|
||||
}
|
||||
// WorktypeID不在エラー
|
||||
export class WorktypeIdNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'WorktypeIdNotFoundError';
|
||||
}
|
||||
}
|
||||
// WorktypeID使用中エラー
|
||||
export class WorktypeIdInUseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'WorktypeIdInUseError';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Worktype } from './entity/worktype.entity';
|
||||
import { WorktypesRepositoryService } from './worktypes.repository.service';
|
||||
import { OptionItem } from "./entity/option_item.entity";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Worktype, OptionItem])],
|
||||
providers: [WorktypesRepositoryService],
|
||||
exports: [WorktypesRepositoryService],
|
||||
})
|
||||
export class WorktypesRepositoryModule {}
|
||||
@ -0,0 +1,104 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { DataSource, Not } from "typeorm";
|
||||
import { Worktype } from "./entity/worktype.entity";
|
||||
import {
|
||||
OPTION_ITEM_NUM,
|
||||
OPTION_ITEM_VALUE_TYPE,
|
||||
WORKTYPE_MAX_COUNT,
|
||||
} from "../../constants";
|
||||
import {
|
||||
WorktypeIdAlreadyExistsError,
|
||||
WorktypeIdMaxCountError,
|
||||
} from "./errors/types";
|
||||
import { OptionItem } from "./entity/option_item.entity";
|
||||
import { insertEntities, insertEntity } from "../../common/repository";
|
||||
import { Context } from "../../common/log";
|
||||
import { WorktypesInputFile } from "../../common/types/types";
|
||||
|
||||
@Injectable()
|
||||
export class WorktypesRepositoryService {
|
||||
// クエリログにコメントを出力するかどうか
|
||||
private readonly isCommentOut = process.env.STAGE !== "local";
|
||||
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
* ワークタイプを作成する
|
||||
* @param accountId
|
||||
* @param worktypeId
|
||||
* @param [description]
|
||||
*/
|
||||
async createWorktype(
|
||||
context: Context,
|
||||
worktypesInputFiles: WorktypesInputFile[]
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||||
const optionItemRepo = entityManager.getRepository(OptionItem);
|
||||
|
||||
for (const worktypesInputFile of worktypesInputFiles) {
|
||||
const accountId = worktypesInputFile.account_id;
|
||||
const worktypeId = worktypesInputFile.custom_worktype_id;
|
||||
const description = null;
|
||||
|
||||
const duplicatedWorktype = await worktypeRepo.findOne({
|
||||
where: { account_id: accountId, custom_worktype_id: worktypeId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
|
||||
// ワークタイプIDが重複している場合はエラー
|
||||
if (duplicatedWorktype) {
|
||||
throw new WorktypeIdAlreadyExistsError(
|
||||
`WorktypeID is already exists. WorktypeID: ${worktypeId}`
|
||||
);
|
||||
}
|
||||
|
||||
const worktypeCount = await worktypeRepo.count({
|
||||
where: { account_id: accountId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
|
||||
// ワークタイプの登録数が上限に達している場合はエラー
|
||||
if (worktypeCount >= WORKTYPE_MAX_COUNT) {
|
||||
throw new WorktypeIdMaxCountError(
|
||||
`Number of worktype is exceeded the limit. MAX_COUNT: ${WORKTYPE_MAX_COUNT}, currentCount: ${worktypeCount}`
|
||||
);
|
||||
}
|
||||
|
||||
// ワークタイプを作成
|
||||
const worktype = await insertEntity(
|
||||
Worktype,
|
||||
worktypeRepo,
|
||||
{
|
||||
account_id: accountId,
|
||||
custom_worktype_id: worktypeId,
|
||||
description: description ?? null,
|
||||
},
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
|
||||
// ワークタイプに紐づくオプションアイテムを10件作成
|
||||
const newOptionItems = Array.from({ length: OPTION_ITEM_NUM }, () => {
|
||||
const optionItem = new OptionItem();
|
||||
optionItem.worktype_id = worktype.id;
|
||||
optionItem.item_label = "";
|
||||
optionItem.default_value_type = OPTION_ITEM_VALUE_TYPE.DEFAULT;
|
||||
optionItem.initial_value = "";
|
||||
|
||||
return optionItem;
|
||||
});
|
||||
|
||||
await insertEntities(
|
||||
OptionItem,
|
||||
optionItemRepo,
|
||||
newOptionItems,
|
||||
this.isCommentOut,
|
||||
context
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,26 @@ import {
|
||||
ValidationOptions,
|
||||
registerDecorator,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
PostUpdateUserRequest,
|
||||
SignupRequest,
|
||||
} from '../../features/users/types/types';
|
||||
import { USER_ROLES } from '../../constants';
|
||||
|
||||
// 大文字英数字とアンダースコアのみを許可するバリデータ
|
||||
@ValidatorConstraint({ name: 'IsAuthorId', async: false })
|
||||
export class IsAuthorId implements ValidatorConstraintInterface {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
const request = args.object as SignupRequest | PostUpdateUserRequest;
|
||||
// requestの存在チェック
|
||||
if (!request) {
|
||||
return false;
|
||||
}
|
||||
const { role } = request;
|
||||
// roleがauthor以外の場合はスキップする
|
||||
if (role !== USER_ROLES.AUTHOR) {
|
||||
return true;
|
||||
}
|
||||
return /^[A-Z0-9_]*$/.test(value);
|
||||
}
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user