Merge branch 'develop'

This commit is contained in:
SAITO-PC-3\saito.k 2024-02-22 15:45:00 +09:00
commit bc87bcd5cf
142 changed files with 9171 additions and 100 deletions

1
data_migration_tools/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/tool*

Binary file not shown.

View File

@ -0,0 +1,8 @@
# 移行ツールをビルドする
# docker ps
$clientContainerName = "client_devcontainer-client-1"
$serverContainerName = "server_devcontainer-server-1"
docker exec -t $clientContainerName sudo npm run build:local
docker exec -t $serverContainerName npm run build:exe

View File

@ -0,0 +1,2 @@
npx openapi-generator-cli version-manager set 7.1.0
npx openapi-generator-cli generate -g typescript-axios -i /app/data_migration_tools/server/src/api/odms/openapi.json -o /app/data_migration_tools/client/src/api/

View File

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.1.0"
}
}

View File

@ -1,8 +1,11 @@
import { Route, Routes } from "react-router-dom";
import TopPage from "./pages/topPage";
import DeletePage from "./pages/deletePage";
const AppRouter: React.FC = () => (
<Routes>
<Route path="/" element={<div />} />
<Route path="/" element={<TopPage />} />
<Route path="/delete" element={<DeletePage />} />
</Routes>
);

View File

@ -0,0 +1,4 @@
wwwroot/*.js
node_modules
typings
dist

View File

@ -0,0 +1 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm

View File

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@ -0,0 +1,9 @@
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts
configuration.ts
git_push.sh
index.ts

View File

@ -0,0 +1 @@
7.1.0

View File

@ -0,0 +1,146 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';
/**
*
* @export
* @interface ErrorResponse
*/
export interface ErrorResponse {
/**
*
* @type {string}
* @memberof ErrorResponse
*/
'message': string;
/**
*
* @type {string}
* @memberof ErrorResponse
*/
'code': string;
}
/**
* DeleteApi - axios parameter creator
* @export
*/
export const DeleteApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteData: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/delete`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* DeleteApi - functional programming interface
* @export
*/
export const DeleteApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = DeleteApiAxiosParamCreator(configuration)
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteData(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteData(options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['DeleteApi.deleteData']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
}
};
/**
* DeleteApi - factory interface
* @export
*/
export const DeleteApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = DeleteApiFp(configuration)
return {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteData(options?: any): AxiosPromise<object> {
return localVarFp.deleteData(options).then((request) => request(axios, basePath));
},
};
};
/**
* DeleteApi - object-oriented interface
* @export
* @class DeleteApi
* @extends {BaseAPI}
*/
export class DeleteApi extends BaseAPI {
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DeleteApi
*/
public deleteData(options?: AxiosRequestConfig) {
return DeleteApiFp(this.configuration).deleteData(options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -0,0 +1,86 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from './configuration';
// Some imports not used depending on template conditions
// @ts-ignore
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
export const BASE_PATH = "http://localhost".replace(/\/+$/, "");
/**
*
* @export
*/
export const COLLECTION_FORMATS = {
csv: ",",
ssv: " ",
tsv: "\t",
pipes: "|",
};
/**
*
* @export
* @interface RequestArgs
*/
export interface RequestArgs {
url: string;
options: AxiosRequestConfig;
}
/**
*
* @export
* @class BaseAPI
*/
export class BaseAPI {
protected configuration: Configuration | undefined;
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
if (configuration) {
this.configuration = configuration;
this.basePath = configuration.basePath ?? basePath;
}
}
};
/**
*
* @export
* @class RequiredError
* @extends {Error}
*/
export class RequiredError extends Error {
constructor(public field: string, msg?: string) {
super(msg);
this.name = "RequiredError"
}
}
interface ServerMap {
[key: string]: {
url: string,
description: string,
}[];
}
/**
*
* @export
*/
export const operationServerMap: ServerMap = {
}

View File

@ -0,0 +1,150 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from "./configuration";
import type { RequestArgs } from "./base";
import type { AxiosInstance, AxiosResponse } from 'axios';
import { RequiredError } from "./base";
/**
*
* @export
*/
export const DUMMY_BASE_URL = 'https://example.com'
/**
*
* @throws {RequiredError}
* @export
*/
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
}
}
/**
*
* @export
*/
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
? await configuration.apiKey(keyParamName)
: await configuration.apiKey;
object[keyParamName] = localVarApiKeyValue;
}
}
/**
*
* @export
*/
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
if (configuration && (configuration.username || configuration.password)) {
object["auth"] = { username: configuration.username, password: configuration.password };
}
}
/**
*
* @export
*/
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
object["Authorization"] = "Bearer " + accessToken;
}
}
/**
*
* @export
*/
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
? await configuration.accessToken(name, scopes)
: await configuration.accessToken;
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
}
}
function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
if (parameter == null) return;
if (typeof parameter === "object") {
if (Array.isArray(parameter)) {
(parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
}
else {
Object.keys(parameter).forEach(currentKey =>
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
);
}
}
else {
if (urlSearchParams.has(key)) {
urlSearchParams.append(key, parameter);
}
else {
urlSearchParams.set(key, parameter);
}
}
}
/**
*
* @export
*/
export const setSearchParams = function (url: URL, ...objects: any[]) {
const searchParams = new URLSearchParams(url.search);
setFlattenedQueryParams(searchParams, objects);
url.search = searchParams.toString();
}
/**
*
* @export
*/
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
const nonString = typeof value !== 'string';
const needsSerialization = nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
: nonString;
return needsSerialization
? JSON.stringify(value !== undefined ? value : {})
: (value || "");
}
/**
*
* @export
*/
export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash
}
/**
*
* @export
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url};
return axios.request<T, R>(axiosRequestArgs);
};
}

View File

@ -0,0 +1,110 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string;
serverIndex?: number;
baseOptions?: any;
formDataCtor?: new () => any;
}
export class Configuration {
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string;
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string;
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string;
/**
* override server index
*
* @type {number}
* @memberof Configuration
*/
serverIndex?: number;
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any;
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any;
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey;
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.basePath = param.basePath;
this.serverIndex = param.serverIndex;
this.baseOptions = param.baseOptions;
this.formDataCtor = param.formDataCtor;
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
}
}

View File

@ -0,0 +1,57 @@
#!/bin/sh
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
#
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
git_user_id=$1
git_repo_id=$2
release_note=$3
git_host=$4
if [ "$git_host" = "" ]; then
git_host="github.com"
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
fi
if [ "$git_user_id" = "" ]; then
git_user_id="GIT_USER_ID"
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
fi
if [ "$git_repo_id" = "" ]; then
git_repo_id="GIT_REPO_ID"
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
fi
if [ "$release_note" = "" ]; then
release_note="Minor update"
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
fi
# Initialize the local directory as a Git repository
git init
# Adds the files in the local repository and stages them for commit.
git add .
# Commits the tracked changes and prepares them to be pushed to a remote repository.
git commit -m "$release_note"
# Sets the new remote
git_remote=$(git remote)
if [ "$git_remote" = "" ]; then # git remote not defined
if [ "$GIT_TOKEN" = "" ]; then
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
else
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
fi
fi
git pull origin master
# Pushes (Forces) the changes in the local repository up to the remote repository
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
git push origin master 2>&1 | grep -v 'To https'

View File

@ -0,0 +1,18 @@
/* tslint:disable */
/* eslint-disable */
/**
* ODMSOpenAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export * from "./api";
export * from "./configuration";

View File

@ -1,7 +1,11 @@
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import auth from "features/auth/authSlice";
import ui from "features/ui/uiSlice";
export const store = configureStore({
reducer: {
auth,
ui,
},
});

View File

@ -0,0 +1,70 @@
/*
E+6
- 1~2...
- 3~4DB...
- 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;

View File

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

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

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

View File

@ -0,0 +1,17 @@
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
*/
export const errorCodes = [
"E009999", // 汎用エラー
] as const;

View File

@ -0,0 +1,3 @@
export * from "./code";
export * from "./types";
export * from "./utils";

View File

@ -0,0 +1,9 @@
import { errorCodes } from "./code";
export type ErrorObject = {
message: string;
code: ErrorCodeType;
statusCode?: number;
};
export type ErrorCodeType = (typeof errorCodes)[number];

View File

@ -0,0 +1,101 @@
import { AxiosError } from "axios";
import { isError } from "lodash";
import { ErrorResponse } from "../../api";
import { errorCodes } from "./code";
import { ErrorCodeType, ErrorObject } from "./types";
export const createErrorObject = (error: unknown): ErrorObject => {
// 最低限通常のエラーかを判定
// Error以外のものがthrowされた場合
// 基本的にないはずだがプログラム上あるので拾う
if (!isError(error)) {
return {
message: "not error type.",
code: "E009999",
};
}
// Axiosエラー 通信してのエラーであるかを判定
if (!isAxiosError(error)) {
return {
message: "not axios error.",
code: "E009999",
};
}
const errorResponse = error.response;
if (!errorResponse) {
return {
message: error.message,
code: "E009999",
statusCode: errorResponse,
};
}
const { data } = errorResponse;
// 想定しているエラーレスポンスの型か判定
if (!isErrorResponse(data)) {
return {
message: error.message,
code: "E009999",
statusCode: errorResponse.status,
};
}
const { message, code } = data;
// 想定しているエラーコードかを判定
if (!isErrorCode(code)) {
return {
message,
code: "E009999",
statusCode: errorResponse.status,
};
}
return {
message,
code,
statusCode: errorResponse.status,
};
};
const isAxiosError = (e: unknown): e is AxiosError => {
const error = e as AxiosError;
return error?.isAxiosError ?? false;
};
const isErrorResponse = (error: unknown): error is ErrorResponse => {
const errorResponse = error as ErrorResponse;
if (
errorResponse === undefined ||
errorResponse.message === undefined ||
errorResponse.code === undefined
) {
return false;
}
return true;
};
const isErrorCode = (errorCode: string): errorCode is ErrorCodeType =>
errorCodes.includes(errorCode as ErrorCodeType);
export const isErrorObject = (
data: unknown
): data is { error: ErrorObject } => {
if (
data &&
typeof data === "object" &&
"error" in data &&
typeof (data as { error: ErrorObject }).error === "object" &&
typeof (data as { error: ErrorObject }).error.message === "string" &&
typeof (data as { error: ErrorObject }).error.code === "string" &&
(typeof (data as { error: ErrorObject }).error.statusCode === "number" ||
(data as { error: ErrorObject }).error.statusCode === undefined)
) {
return true;
}
return false;
};

View File

@ -1,6 +1,6 @@
export const getBasePath = () => {
if (import.meta.env.VITE_STAGE === "local") {
return "http://localhost:8180";
return "http://localhost:8280";
}
return window.location.origin;
};

View File

@ -0,0 +1,15 @@
import { createSlice } from "@reduxjs/toolkit";
import type { AuthState } from "./state";
import { initialConfig } from "./utils";
const initialState: AuthState = {
configuration: initialConfig(),
};
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {},
});
export default authSlice.reducer;

View File

@ -0,0 +1,3 @@
export * from "./authSlice";
export * from "./state";
export * from "./utils";

View File

@ -0,0 +1,5 @@
import { ConfigurationParameters } from "../../api";
export interface AuthState {
configuration: ConfigurationParameters;
}

View File

@ -0,0 +1,13 @@
import { ConfigurationParameters } from "../../api";
// 初期状態のAPI Config
export const initialConfig = (): ConfigurationParameters => {
const config: ConfigurationParameters = {};
if (import.meta.env.VITE_STAGE === "local") {
config.basePath = "http://localhost:8280";
} else {
config.basePath = `${window.location.origin}/dictation/api`;
}
return config;
};

View File

@ -0,0 +1,25 @@
import { createSlice } from "@reduxjs/toolkit";
import { DeleteState } from "./state";
import { deleteDataAsync } from "./operations";
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
const initialState: DeleteState = {};
export const deleteSlice = createSlice({
name: "detete",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(deleteDataAsync.pending, () => {
/* Empty Object */
});
builder.addCase(deleteDataAsync.fulfilled, () => {
/* Empty Object */
});
builder.addCase(deleteDataAsync.rejected, () => {
/* Empty Object */
});
},
});
export default deleteSlice.reducer;

View File

@ -0,0 +1,3 @@
export * from "./state";
export * from "./deleteSlice";
export * from "./operations";

View File

@ -0,0 +1,47 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { openSnackbar } from "../ui/uiSlice";
import type { RootState } from "../../app/store";
import { ErrorObject, createErrorObject } from "../../common/errors";
import { DeleteApi } from "../../api/api";
import { Configuration } from "../../api/configuration";
export const deleteDataAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("delete/deleteDataAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const config = new Configuration(configuration);
const deleteApi = new DeleteApi(config);
try {
await deleteApi.deleteData();
thunkApi.dispatch(
openSnackbar({
level: "info",
message: "削除しました。",
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: "削除に失敗しました。",
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,2 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DeleteState {}

View File

@ -0,0 +1,2 @@
// 標準のスナックバー表示時間(ミリ秒)
export const DEFAULT_SNACKBAR_DURATION = 3000;

View File

@ -0,0 +1,5 @@
export * from "./constants";
export * from "./selectors";
export * from "./state";
export * from "./uiSlice";
export * from "./types";

View File

@ -0,0 +1,15 @@
import type { RootState } from "app/store";
import { SnackbarLevel } from "./types";
export const selectSnackber = (
state: RootState
): {
isOpen: boolean;
level: SnackbarLevel;
message: string;
duration?: number;
} => {
const { isOpen, level, message, duration } = state.ui;
return { isOpen, level, message, duration };
};

View File

@ -0,0 +1,8 @@
import { SnackbarLevel } from "./types";
export interface UIState {
isOpen: boolean;
level: SnackbarLevel;
message: string;
duration?: number;
}

View File

@ -0,0 +1 @@
export type SnackbarLevel = "info" | "error";

View File

@ -0,0 +1,38 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SnackbarLevel } from "./types";
import { UIState } from "./state";
import { DEFAULT_SNACKBAR_DURATION } from "./constants";
const initialState: UIState = {
isOpen: false,
level: "error",
message: "",
};
export const uiSlice = createSlice({
name: "ui",
initialState,
reducers: {
openSnackbar: (
state,
action: PayloadAction<{
level: SnackbarLevel;
message: string;
duration?: number;
}>
) => {
const { level, message, duration } = action.payload;
state.isOpen = true;
state.level = level;
state.message = message;
state.duration =
level === "error" ? undefined : duration ?? DEFAULT_SNACKBAR_DURATION;
},
closeSnackbar: (state) => {
state.isOpen = false;
},
},
});
export const { openSnackbar, closeSnackbar } = uiSlice.actions;
export default uiSlice.reducer;

View File

@ -0,0 +1,32 @@
import { AppDispatch } from "app/store";
import { deleteDataAsync } from "features/delete";
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
const DeletePage = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const onDelete = useCallback(() => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm("本当に削除しますか?")
) {
return;
}
dispatch(deleteDataAsync());
}, [dispatch]);
return (
<div>
<p></p>
<button type="button" onClick={onDelete}>
</button>
<br />
<Link to="/">return to TopPage</Link>
</div>
);
};
export default DeletePage;

View File

@ -0,0 +1,15 @@
import React from "react";
import { Link } from "react-router-dom";
const TopPage = (): JSX.Element => {
console.log("DeletePage");
return (
<div>
<Link to="/delete"></Link>
<br />
<Link to="/">return to TopPage</Link>
</div>
);
};
export default TopPage;

63
data_migration_tools/package-lock.json generated Normal file
View 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"
}
}

View File

@ -14,5 +14,11 @@ services:
- "8280"
environment:
- CHOKIDAR_USEPOLLING=true
networks:
- external
networks:
external:
name: omds_network
external: true
volumes:
data_migration_tools_server_node_modules:

View File

@ -0,0 +1,5 @@
DB_HOST=omds-mysql
DB_PORT=3306
DB_NAME=omds
DB_USERNAME=omdsdbuser
DB_PASSWORD=omdsdbpass

View File

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

View File

@ -0,0 +1,12 @@
if [ ! -d "../tool" ]; then
mkdir "../tool"
echo "フォルダが作成されました。"
else
rm -f ../tool/ODMS_DataTool.exe
fi
unzip ../baseNode.zip -d ../tool
ncc build dist/main.js -o dist_single -a
npx -y postject ../tool/ODMS_DataTool.exe NODE_JS_CODE dist_single/index.js --sentinel-fuse NODE_JS_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite
cp -r build ../tool
cp .env ../tool
cp .env.local.example ../tool

File diff suppressed because it is too large Load Diff

View File

@ -19,10 +19,13 @@
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"apigen": "ts-node src/api/generate.ts && prettier --write \"src/api/odms/*.json\""
},
"dependencies": {
"@azure/identity": "^4.0.1",
"@azure/storage-blob": "^12.14.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/common": "^9.3.9",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.3.9",
@ -30,6 +33,7 @@
"@nestjs/serve-static": "^3.0.1",
"@nestjs/swagger": "^6.2.1",
"@nestjs/testing": "^9.3.9",
"@nestjs/typeorm": "^10.0.2",
"@openapitools/openapi-generator-cli": "^2.5.2",
"@vercel/ncc": "^0.36.1",
"axios": "^1.3.4",
@ -37,9 +41,11 @@
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"multer": "^1.4.5-lts.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",

View File

@ -0,0 +1,25 @@
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "../app.module";
import { promises as fs } from "fs";
import { NestFactory } from "@nestjs/core";
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, {
preview: true,
});
const options = new DocumentBuilder()
.setTitle("ODMSOpenAPI")
.setVersion("1.0.0")
.addBearerAuth({
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
})
.build();
const document = SwaggerModule.createDocument(app, options);
await fs.writeFile(
"src/api/odms/openapi.json",
JSON.stringify(document, null, 0)
);
}
bootstrap();

View File

@ -0,0 +1,56 @@
{
"openapi": "3.0.0",
"paths": {
"/delete": {
"post": {
"operationId": "deleteData",
"summary": "",
"description": "すべてのデータを削除します",
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/DeleteResponse" }
}
}
},
"500": {
"description": "想定外のサーバーエラー",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
}
}
}
},
"tags": ["delete"]
}
}
},
"info": {
"title": "ODMSOpenAPI",
"description": "",
"version": "1.0.0",
"contact": {}
},
"tags": [],
"servers": [],
"components": {
"securitySchemes": {
"bearer": { "scheme": "bearer", "bearerFormat": "JWT", "type": "http" }
},
"schemas": {
"DeleteResponse": { "type": "object", "properties": {} },
"ErrorResponse": {
"type": "object",
"properties": {
"message": { "type": "string" },
"code": { "type": "string" }
},
"required": ["message", "code"]
}
}
}
}

View File

@ -1,8 +1,29 @@
import { MiddlewareConsumer, Module } from "@nestjs/common";
import { ServeStaticModule } from "@nestjs/serve-static";
import { ConfigModule } from "@nestjs/config";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { join } from "path";
import { LoggerMiddleware } from "./common/loggerMiddleware";
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";
@Module({
imports: [
@ -13,9 +34,35 @@ import { LoggerMiddleware } from "./common/loggerMiddleware";
envFilePath: [".env.local", ".env"],
isGlobal: true,
}),
AdB2cModule,
AccountsModule,
UsersModule,
RegisterModule,
AccountsRepositoryModule,
UsersRepositoryModule,
SortCriteriaRepositoryModule,
LicensesRepositoryModule,
WorktypesRepositoryModule,
BlobstorageModule,
DeleteModule,
DeleteRepositoryModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
type: "mysql",
host: configService.get("DB_HOST"),
port: configService.get("DB_PORT"),
username: configService.get("DB_USERNAME"),
password: configService.get("DB_PASSWORD"),
database: configService.get("DB_NAME"),
autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
}),
inject: [ConfigService],
}),
],
controllers: [],
providers: [],
controllers: [RegisterController, AccountsController, UsersController, DeleteController],
providers: [RegisterService, AccountsService, UsersService, DeleteService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {

View File

@ -0,0 +1,61 @@
import { bigintTransformer } from '.';
describe('bigintTransformer', () => {
describe('to', () => {
it('number型を整数を表す文字列に変換できる', () => {
expect(bigintTransformer.to(0)).toBe('0');
expect(bigintTransformer.to(1)).toBe('1');
expect(bigintTransformer.to(1234567890)).toBe('1234567890');
expect(bigintTransformer.to(9007199254740991)).toBe('9007199254740991');
expect(bigintTransformer.to(-1)).toBe('-1');
});
it('少数点以下がある場合はエラーとなる', () => {
expect(() => bigintTransformer.to(1.1)).toThrowError(
'1.1 is not integer.',
);
});
it('Number.MAX_SAFE_INTEGERを超える値を変換しようとするとエラーになる', () => {
expect(() => bigintTransformer.to(9007199254740992)).toThrowError(
'value is greater than 9007199254740991.',
);
expect(() => bigintTransformer.to(9223372036854775807)).toThrowError(
'value is greater than 9007199254740991.',
);
});
});
describe('from', () => {
it('bigint型の文字列をnumber型に変換できる', () => {
expect(bigintTransformer.from('0')).toBe(0);
expect(bigintTransformer.from('1')).toBe(1);
expect(bigintTransformer.from('1234567890')).toBe(1234567890);
expect(bigintTransformer.from('-1')).toBe(-1);
});
it('Number.MAX_SAFE_INTEGERを超える値を変換しようとするとエラーになる', () => {
expect(() => bigintTransformer.from('9007199254740992')).toThrowError(
'9007199254740992 is greater than 9007199254740991.',
);
expect(() => bigintTransformer.from('9223372036854775807')).toThrowError(
'9223372036854775807 is greater than 9007199254740991.',
);
});
it('number型の場合はそのまま返す', () => {
expect(bigintTransformer.from(0)).toBe(0);
expect(bigintTransformer.from(1)).toBe(1);
expect(bigintTransformer.from(1234567890)).toBe(1234567890);
expect(bigintTransformer.from(-1)).toBe(-1);
});
it('nullの場合はそのまま返す', () => {
expect(bigintTransformer.from(null)).toBe(null);
});
it('number型に変換できない場合はエラーとなる', () => {
expect(() => bigintTransformer.from('a')).toThrowError('a is not int.');
expect(() => bigintTransformer.from('')).toThrowError(' is not int.');
expect(() => bigintTransformer.from(undefined)).toThrowError(
'undefined is not string.',
);
expect(() => bigintTransformer.from({})).toThrowError(
'[object Object] is not string.',
);
});
});
});

View File

@ -0,0 +1,57 @@
import { ValueTransformer } from 'typeorm';
// DBのbigint型をnumber型に変換するためのtransformer
// DBのBigInt型をそのまま扱うと、JSのNumber型の最大値を超えると誤差が発生するため、本来はNumber型に変換すべきではないが、
// 影響範囲を最小限に抑えるため、Number型に変換する。使用するのはAutoIncrementされるIDのみの想定のため、
// Number.MAX_SAFE_INTEGERより大きい値は現実的には発生しない想定で変換する。
export const bigintTransformer: ValueTransformer = {
from: (value: any): number | null => {
// valueがnullであればそのまま返す
if (value === null) {
return value;
}
// valueがnumber型かどうかを判定
// 利用DBによってはbigint型であってもnumber型で返ってくる場合があるため、number型の場合はそのまま返す(sqliteの場合)
if (typeof value === 'number') {
return value;
}
// valueが文字列かどうかを判定
if (typeof value !== 'string') {
throw new Error(`${value} is not string.`);
}
// 数値に変換可能な文字列かどうかを判定
if (Number.isNaN(parseInt(value))) {
throw new Error(`${value} is not int.`);
}
// 文字列ならbigintに変換
// valueが整数でない場合は値が丸められてしまうが、TypeORMのEntityの定義上、整数を表す文字列以外はありえないため、少数点は考慮しない
const bigIntValue = BigInt(value);
// bigIntValueがNumber.MAX_SAFE_INTEGERより大きいかどうかを判定
if (bigIntValue > Number.MAX_SAFE_INTEGER) {
throw new Error(`${value} is greater than ${Number.MAX_SAFE_INTEGER}.`);
}
// number型で表現できる整数であればnumber型に変換して返す
return Number(bigIntValue);
},
to: (value: any): string | null | undefined => {
// valueがnullまたはundefinedであればそのまま返す
if (value === null || value === undefined) {
return value;
}
// valueがnumber型かどうかを判定
if (typeof value !== 'number') {
throw new Error(`${value} is not number.`);
}
// valueがNumber.MAX_SAFE_INTEGERより大きいかどうかを判定
if (value > Number.MAX_SAFE_INTEGER) {
throw new Error(`value is greater than ${Number.MAX_SAFE_INTEGER}.`);
}
// valueが整数かどうかを判定
if (!Number.isInteger(value)) {
throw new Error(`${value} is not integer.`);
}
return value.toString();
},
};

View File

@ -0,0 +1,70 @@
/*
E+6
- 1~2...
- 3~4DB...
- 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;

View File

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

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

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

View File

@ -0,0 +1,17 @@
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
*/
export const ErrorCodes = [
"E009999", // 汎用エラー
] as const;

View File

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

View File

@ -0,0 +1,6 @@
import { Errors } from "./types/types";
// エラーコードとメッセージ対応表
export const errors: Errors = {
E009999: "Internal Server Error.",
};

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

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

View File

@ -0,0 +1,4 @@
import { Context } from "./types";
import { makeContext, retrieveRequestId, retrieveIp } from "./context";
export { Context, makeContext, retrieveRequestId, retrieveIp };

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

View File

@ -0,0 +1,3 @@
import { makePassword } from "./password";
export { makePassword };

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

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

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

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

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

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

View File

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

View File

@ -0,0 +1,406 @@
/**
*
* @const {number}
*/
export const TIERS = {
//OMDS東京
TIER1: 1,
//OMDS現地法人
TIER2: 2,
//代理店
TIER3: 3,
//販売店
TIER4: 4,
//エンドユーザー
TIER5: 5,
} as const;
/**
* East USに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_US = ["CA", "KY", "US"];
/**
* Australia Eastに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_AU = ["AU", "NZ"];
/**
* North Europeに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_EU = [
"AT",
"BE",
"BG",
"HR",
"CY",
"CZ",
"DK",
"EE",
"FI",
"FR",
"DE",
"GR",
"HU",
"IS",
"IE",
"IT",
"LV",
"LI",
"LT",
"LU",
"MT",
"NL",
"NO",
"PL",
"PT",
"RO",
"RS",
"SK",
"SI",
"ZA",
"ES",
"SE",
"CH",
"TR",
"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[]}
*/
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: "U.S.A." },
{ 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;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,30 @@
import { Test, TestingModule } from "@nestjs/testing";
import { ConfigModule } from "@nestjs/config";
import { DeleteService } from "./delete.service";
import { DeleteController } from "./delete.controller";
describe("DeleteController", () => {
let controller: DeleteController;
const mockTemplatesService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: [".env.test", ".env"],
isGlobal: true,
}),
],
controllers: [DeleteController],
providers: [DeleteService],
})
.overrideProvider(DeleteService)
.useValue(mockTemplatesService)
.compile();
controller = module.get<DeleteController>(DeleteController);
});
it("should be defined", () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,39 @@
import {
Controller,
HttpException,
HttpStatus,
Logger,
Post,
Req,
} from "@nestjs/common";
import { ErrorResponse } from "../../common/errors/types/types";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Request } from "express";
import { DeleteService } from "./delete.service";
import { DeleteResponse } from "./types/types";
@ApiTags("delete")
@Controller("delete")
export class DeleteController {
constructor(private readonly deleteService: DeleteService) {}
@ApiResponse({
status: HttpStatus.OK,
type: DeleteResponse,
description: "成功時のレスポンス",
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: "想定外のサーバーエラー",
type: ErrorResponse,
})
@ApiOperation({
operationId: "deleteData",
description: "すべてのデータを削除します",
})
@Post()
async deleteData(): Promise<{}> {
await this.deleteService.deleteData();
return {};
}
}

View File

@ -0,0 +1,13 @@
import { Module } from "@nestjs/common";
import { DeleteRepositoryModule } from "../../repositories/delete/delete.repository.module";
import { DeleteController } from "./delete.controller";
import { DeleteService } from "./delete.service";
import { AdB2cModule } from "../../gateways/adb2c/adb2c.module";
import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module";
@Module({
imports: [DeleteRepositoryModule, AdB2cModule, BlobstorageModule],
providers: [DeleteService],
controllers: [DeleteController],
})
export class DeleteModule {}

View File

@ -0,0 +1,29 @@
import { DataSource } from "typeorm";
import { ConfigModule } from "@nestjs/config";
import { DeleteService } from "./delete.service";
import { Test, TestingModule } from "@nestjs/testing";
describe("DeleteController", () => {
let service: DeleteService;
const mockTemplatesService = {};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: [".env.test", ".env"],
isGlobal: true,
}),
],
providers: [DeleteService],
})
.overrideProvider(DeleteService)
.useValue(mockTemplatesService)
.compile();
service = module.get<DeleteService>(DeleteService);
});
it("should be defined", () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,54 @@
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { DeleteRepositoryService } from "../../repositories/delete/delete.repository.service";
import { makeErrorResponse } from "../../common/errors/makeErrorResponse";
import { AdB2cService } from "../../gateways/adb2c/adb2c.service";
import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service";
@Injectable()
export class DeleteService {
private readonly logger = new Logger(DeleteService.name);
constructor(
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}`);
try {
// BlobStorageからデータを削除する
await this.blobstorageService.deleteContainers();
// ADB2Cからユーザ情報を取得する
const users = await this.adB2cService.getUsers();
const externalIds = users.map((user) => user.id);
await this.adB2cService.deleteUsers(externalIds);
// データベースからデータを削除する
await this.deleteRepositoryService.deleteData();
// AutoIncrementの値をリセットする
await this.deleteRepositoryService.resetAutoIncrement();
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally {
this.logger.log(`[OUT] ${this.deleteData.name}`);
}
}
}

View File

@ -0,0 +1 @@
import { DataSource } from "typeorm";

View File

@ -0,0 +1 @@
export class DeleteResponse {}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
export class RegisterRequest {
@ApiProperty()
inputFilePath: string;
}
export class RegisterResponse {}

View File

@ -0,0 +1,174 @@
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 "../../../src/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(",");
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をつの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}`
);
}
}
}

View File

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

View File

@ -0,0 +1,357 @@
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) {
// そのacccountIdをdealerAccountIdにもつアカウント(Distributor)を検索する
const distributor = accountsOutputFileStep1.find(
(distributor) =>
account.type === MIGRATION_TYPE.DISTRIBUTOR &&
distributor.dealerAccountId === account.accountId
);
// DistributorのdealerAccountIdをBCCountryの親に付け替える
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.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 (!COUNTRY_LIST.find((country) => country.label === line.country)) {
throw new HttpException(
`country is invalid. index=${index} country=${line.country}`,
HttpStatus.BAD_REQUEST
);
}
// mailのバリデーションチェック
// メールアドレスの形式が正しいかどうかのチェック
const mailRegExp = new RegExp(
/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$/
);
if (!mailRegExp.test(line.email)) {
throw new HttpException(
`email is invalid. index=${index} email=${line.email}`,
HttpStatus.BAD_REQUEST
);
}
// 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 &&
line.recording_mode !== null
) {
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}`
);
}
}
}

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AdB2cService } from "./adb2c.service";
@Module({
imports: [ConfigModule],
exports: [AdB2cService],
providers: [AdB2cService],
})
export class AdB2cModule {}

View File

@ -0,0 +1,218 @@
import { ClientSecretCredential } from "@azure/identity";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
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";
message: string;
};
export class Adb2cTooManyRequestsError extends Error {}
export const isConflictError = (arg: unknown): arg is ConflictError => {
const value = arg as ConflictError;
if (value.message === undefined) {
return false;
}
if (value.reason === "email") {
return true;
}
return false;
};
@Injectable()
export class AdB2cService {
private readonly logger = new Logger(AdB2cService.name);
private readonly tenantName: string;
private readonly flowName: string;
private readonly ttl: number;
private graphClient: Client;
constructor(private readonly configService: ConfigService) {
this.tenantName = this.configService.getOrThrow<string>("TENANT_NAME");
this.flowName = this.configService.getOrThrow<string>("SIGNIN_FLOW_NAME");
this.ttl = this.configService.getOrThrow<number>("ADB2C_CACHE_TTL");
// ADB2Cへの認証情報
const credential = new ClientSecretCredential(
this.configService.getOrThrow<string>("ADB2C_TENANT_ID"),
this.configService.getOrThrow<string>("ADB2C_CLIENT_ID"),
this.configService.getOrThrow<string>("ADB2C_CLIENT_SECRET")
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["https://graph.microsoft.com/.default"],
});
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}`);
try {
const res: AdB2cResponse = await this.graphClient
.api(`users/`)
.select(["id", "displayName", "identities"])
.filter(`creationType eq 'LocalAccount'`)
.get();
return res.value;
} catch (e) {
this.logger.error(`error=${e}`);
const { statusCode } = e;
if (statusCode === 429) {
throw new Adb2cTooManyRequestsError();
}
throw e;
} finally {
this.logger.log(`[OUT] ${this.getUsers.name}`);
}
}
/**
* 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> {
this.logger.log(
`[IN]${this.deleteUsers.name} | params: { externalIds: ${externalIds} };`
);
try {
// 複数ユーザーを一括削除する方法がないため、1人ずつで削除を行う
const results = await Promise.allSettled(
externalIds.map(async (externalId) => {
await this.graphClient.api(`users/${externalId}`).delete();
await new Promise((resolve) => setTimeout(resolve, 15)); // 15ms待つ
this.logger.log(`[[ADB2C DELETE] externalId: ${externalId}`);
})
);
// 失敗したプロミスのエラーをログに記録
results.forEach((result, index) => {
// statusがrejectedでない場合は、エラーが発生していないためログに記録しない
if (result.status !== "rejected") {
return;
}
const failedId = externalIds[index];
if (isPromiseRejectedResult(result)) {
const error = result.reason.toString();
this.logger.error(`Failed to delete user ${failedId}: ${error}`);
} else {
this.logger.error(`Failed to delete user ${failedId}`);
}
});
} catch (e) {
this.logger.error(`error=${e}`);
throw e;
} finally {
this.logger.log(`[OUT] ${this.deleteUsers.name}`);
}
}
}

View File

@ -0,0 +1,15 @@
export type AdB2cResponse = {
'@odata.context': string;
value: AdB2cUser[];
};
export type AdB2cUser = {
id: string;
displayName: string;
identities?: UserIdentity[];
};
export type UserIdentity = {
signInType: string;
issuer: string;
issuerAssignedId: string;
};

View File

@ -0,0 +1,22 @@
import { ADB2C_SIGN_IN_TYPE } from '../../../constants';
import { AdB2cUser } from '../types/types';
export const isPromiseRejectedResult = (
data: unknown,
): data is PromiseRejectedResult => {
return (
data !== null &&
typeof data === 'object' &&
'status' in data &&
'reason' in data
);
};
// 生のAdB2cUserのレスポンスから表示名とメールアドレスを取得する
export const getUserNameAndMailAddress = (user: AdB2cUser) => {
const { displayName, identities } = user;
const emailAddress = identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
)?.issuerAssignedId;
return { displayName, emailAddress };
};

Some files were not shown because too many files have changed in this diff Show More