Merge branch 'develop'
This commit is contained in:
commit
bc87bcd5cf
1
data_migration_tools/.gitignore
vendored
Normal file
1
data_migration_tools/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/tool*
|
||||
BIN
data_migration_tools/baseNode.zip
Normal file
BIN
data_migration_tools/baseNode.zip
Normal file
Binary file not shown.
8
data_migration_tools/buildTool.ps1
Normal file
8
data_migration_tools/buildTool.ps1
Normal 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
|
||||
2
data_migration_tools/client/codegen.sh
Normal file
2
data_migration_tools/client/codegen.sh
Normal 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/
|
||||
7
data_migration_tools/client/openapitools.json
Normal file
7
data_migration_tools/client/openapitools.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "7.1.0"
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
4
data_migration_tools/client/src/api/.gitignore
vendored
Normal file
4
data_migration_tools/client/src/api/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
wwwroot/*.js
|
||||
node_modules
|
||||
typings
|
||||
dist
|
||||
1
data_migration_tools/client/src/api/.npmignore
Normal file
1
data_migration_tools/client/src/api/.npmignore
Normal file
@ -0,0 +1 @@
|
||||
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
||||
@ -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
|
||||
@ -0,0 +1,9 @@
|
||||
.gitignore
|
||||
.npmignore
|
||||
.openapi-generator-ignore
|
||||
api.ts
|
||||
base.ts
|
||||
common.ts
|
||||
configuration.ts
|
||||
git_push.sh
|
||||
index.ts
|
||||
@ -0,0 +1 @@
|
||||
7.1.0
|
||||
146
data_migration_tools/client/src/api/api.ts
Normal file
146
data_migration_tools/client/src/api/api.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
86
data_migration_tools/client/src/api/base.ts
Normal file
86
data_migration_tools/client/src/api/base.ts
Normal 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 = {
|
||||
}
|
||||
150
data_migration_tools/client/src/api/common.ts
Normal file
150
data_migration_tools/client/src/api/common.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
110
data_migration_tools/client/src/api/configuration.ts
Normal file
110
data_migration_tools/client/src/api/configuration.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
57
data_migration_tools/client/src/api/git_push.sh
Normal file
57
data_migration_tools/client/src/api/git_push.sh
Normal 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'
|
||||
18
data_migration_tools/client/src/api/index.ts
Normal file
18
data_migration_tools/client/src/api/index.ts
Normal 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";
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
70
data_migration_tools/client/src/common/error/code.ts
Normal file
70
data_migration_tools/client/src/common/error/code.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
エラーコード作成方針
|
||||
E+6桁(数字)で構成する。
|
||||
- 1~2桁目の値は種類(業務エラー、システムエラー...)
|
||||
- 3~4桁目の値は原因箇所(トークン、DB、...)
|
||||
- 5~6桁目の値は任意の重複しない値
|
||||
ex)
|
||||
E00XXXX : システムエラー(通信エラーやDB接続失敗など)
|
||||
E01XXXX : 業務エラー
|
||||
EXX00XX : 内部エラー(内部プログラムのエラー)
|
||||
EXX01XX : トークンエラー(トークン認証関連)
|
||||
EXX02XX : DBエラー(DB関連)
|
||||
EXX03XX : ADB2Cエラー(DB関連)
|
||||
*/
|
||||
export const ErrorCodes = [
|
||||
'E009999', // 汎用エラー
|
||||
'E000101', // トークン形式不正エラー
|
||||
'E000102', // トークン有効期限切れエラー
|
||||
'E000103', // トークン非アクティブエラー
|
||||
'E000104', // トークン署名エラー
|
||||
'E000105', // トークン発行元エラー
|
||||
'E000106', // トークンアルゴリズムエラー
|
||||
'E000107', // トークン不足エラー
|
||||
'E000108', // トークン権限エラー
|
||||
'E000301', // ADB2Cへのリクエスト上限超過エラー
|
||||
'E000401', // IPアドレス未設定エラー
|
||||
'E000501', // リクエストID未設定エラー
|
||||
'E010001', // パラメータ形式不正エラー
|
||||
'E010201', // 未認証ユーザエラー
|
||||
'E010202', // 認証済ユーザエラー
|
||||
'E010203', // 管理ユーザ権限エラー
|
||||
'E010204', // ユーザ不在エラー
|
||||
'E010205', // DBのRoleが想定外の値エラー
|
||||
'E010206', // DBのTierが想定外の値エラー
|
||||
'E010207', // ユーザーのRole変更不可エラー
|
||||
'E010208', // ユーザーの暗号化パスワード不足エラー
|
||||
'E010209', // ユーザーの同意済み利用規約バージョンが最新でないエラー
|
||||
'E010301', // メールアドレス登録済みエラー
|
||||
'E010302', // authorId重複エラー
|
||||
'E010401', // PONumber重複エラー
|
||||
'E010501', // アカウント不在エラー
|
||||
'E010502', // アカウント情報変更不可エラー
|
||||
'E010503', // 代行操作不許可エラー
|
||||
'E010504', // アカウントロックエラー
|
||||
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||
'E010602', // タスク変更権限不足エラー
|
||||
'E010603', // タスク不在エラー
|
||||
'E010701', // Blobファイル不在エラー
|
||||
'E010801', // ライセンス不在エラー
|
||||
'E010802', // ライセンス取り込み済みエラー
|
||||
'E010803', // ライセンス発行済みエラー
|
||||
'E010804', // ライセンス不足エラー
|
||||
'E010805', // ライセンス有効期限切れエラー
|
||||
'E010806', // ライセンス割り当て不可エラー
|
||||
'E010807', // ライセンス割り当て解除済みエラー
|
||||
'E010808', // ライセンス注文キャンセル不可エラー
|
||||
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
|
||||
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
|
||||
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
|
||||
'E010812', // ライセンス未割当エラー
|
||||
'E010908', // タイピストグループ不在エラー
|
||||
'E010909', // タイピストグループ名重複エラー
|
||||
'E011001', // ワークタイプ重複エラー
|
||||
'E011002', // ワークタイプ登録上限超過エラー
|
||||
'E011003', // ワークタイプ不在エラー
|
||||
'E011004', // ワークタイプ使用中エラー
|
||||
'E012001', // テンプレートファイル不在エラー
|
||||
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
'E013002', // ワークフロー不在エラー
|
||||
] as const;
|
||||
@ -0,0 +1,10 @@
|
||||
import { errors } from './message';
|
||||
import { ErrorCodeType, ErrorResponse } from './types/types';
|
||||
|
||||
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
|
||||
const msg = errors[errorcode];
|
||||
return {
|
||||
code: errorcode,
|
||||
message: msg,
|
||||
};
|
||||
};
|
||||
59
data_migration_tools/client/src/common/error/message.ts
Normal file
59
data_migration_tools/client/src/common/error/message.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Errors } from './types/types';
|
||||
|
||||
// エラーコードとメッセージ対応表
|
||||
export const errors: Errors = {
|
||||
E009999: 'Internal Server Error.',
|
||||
E000101: 'Token invalid format Error.',
|
||||
E000102: 'Token expired Error.',
|
||||
E000103: 'Token not before Error',
|
||||
E000104: 'Token invalid signature Error.',
|
||||
E000105: 'Token invalid issuer Error.',
|
||||
E000106: 'Token invalid algorithm Error.',
|
||||
E000107: 'Token is not exist Error.',
|
||||
E000108: 'Token authority failed Error.',
|
||||
E000301: 'ADB2C request limit exceeded Error',
|
||||
E000401: 'IP address not found Error.',
|
||||
E000501: 'Request ID not found Error.',
|
||||
E010001: 'Param invalid format Error.',
|
||||
E010201: 'Email not verified user Error.',
|
||||
E010202: 'Email already verified user Error.',
|
||||
E010203: 'Administrator Permissions Error.',
|
||||
E010204: 'User not Found Error.',
|
||||
E010205: 'Role from DB is unexpected value Error.',
|
||||
E010206: 'Tier from DB is unexpected value Error.',
|
||||
E010207: 'User role change not allowed Error.',
|
||||
E010208: 'User encryption password not found Error.',
|
||||
E010209: 'Accepted term not latest Error.',
|
||||
E010301: 'This email user already created Error',
|
||||
E010302: 'This AuthorId already used Error',
|
||||
E010401: 'This PoNumber already used Error',
|
||||
E010501: 'Account not Found Error.',
|
||||
E010502: 'Account information cannot be changed Error.',
|
||||
E010503: 'Delegation not allowed Error.',
|
||||
E010504: 'Account is locked Error.',
|
||||
E010601: 'Task is not Editable Error',
|
||||
E010602: 'No task edit permissions Error',
|
||||
E010603: 'Task not found Error.',
|
||||
E010701: 'File not found in Blob Storage Error.',
|
||||
E010801: 'License not exist Error',
|
||||
E010802: 'License already activated Error',
|
||||
E010803: 'License already issued Error',
|
||||
E010804: 'License shortage Error',
|
||||
E010805: 'License is expired Error',
|
||||
E010806: 'License is unavailable Error',
|
||||
E010807: 'License is already deallocated Error',
|
||||
E010808: 'Order cancel failed Error',
|
||||
E010809: 'Already license order status changed Error',
|
||||
E010810: 'Cancellation period expired error',
|
||||
E010811: 'Already license allocated Error',
|
||||
E010812: 'License not allocated Error',
|
||||
E010908: 'Typist Group not exist Error',
|
||||
E010909: 'Typist Group name already exist Error',
|
||||
E011001: 'This WorkTypeID already used Error',
|
||||
E011002: 'WorkTypeID create limit exceeded Error',
|
||||
E011003: 'WorkTypeID not found Error',
|
||||
E011004: 'WorkTypeID is in use Error',
|
||||
E012001: 'Template file not found Error',
|
||||
E013001: 'AuthorId and WorktypeId pair already exists Error',
|
||||
E013002: 'Workflow not found Error',
|
||||
};
|
||||
15
data_migration_tools/client/src/common/error/types/types.ts
Normal file
15
data_migration_tools/client/src/common/error/types/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ErrorCodes } from '../code';
|
||||
|
||||
export class ErrorResponse {
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
@ApiProperty()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export type ErrorCodeType = (typeof ErrorCodes)[number];
|
||||
|
||||
export type Errors = {
|
||||
[P in ErrorCodeType]: string;
|
||||
};
|
||||
17
data_migration_tools/client/src/common/errors/code.ts
Normal file
17
data_migration_tools/client/src/common/errors/code.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
エラーコード作成方針
|
||||
E+6桁(数字)で構成する。
|
||||
- 1~2桁目の値は種類(業務エラー、システムエラー...)
|
||||
- 3~4桁目の値は原因箇所(トークン、DB、...)
|
||||
- 5~6桁目の値は任意の重複しない値
|
||||
ex)
|
||||
E00XXXX : システムエラー(通信エラーやDB接続失敗など)
|
||||
E01XXXX : 業務エラー
|
||||
EXX00XX : 内部エラー(内部プログラムのエラー)
|
||||
EXX01XX : トークンエラー(トークン認証関連)
|
||||
EXX02XX : DBエラー(DB関連)
|
||||
EXX03XX : ADB2Cエラー(DB関連)
|
||||
*/
|
||||
export const errorCodes = [
|
||||
"E009999", // 汎用エラー
|
||||
] as const;
|
||||
3
data_migration_tools/client/src/common/errors/index.ts
Normal file
3
data_migration_tools/client/src/common/errors/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./code";
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
9
data_migration_tools/client/src/common/errors/types.ts
Normal file
9
data_migration_tools/client/src/common/errors/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { errorCodes } from "./code";
|
||||
|
||||
export type ErrorObject = {
|
||||
message: string;
|
||||
code: ErrorCodeType;
|
||||
statusCode?: number;
|
||||
};
|
||||
|
||||
export type ErrorCodeType = (typeof errorCodes)[number];
|
||||
101
data_migration_tools/client/src/common/errors/utils.ts
Normal file
101
data_migration_tools/client/src/common/errors/utils.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
|
||||
15
data_migration_tools/client/src/features/auth/authSlice.ts
Normal file
15
data_migration_tools/client/src/features/auth/authSlice.ts
Normal 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;
|
||||
3
data_migration_tools/client/src/features/auth/index.ts
Normal file
3
data_migration_tools/client/src/features/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./authSlice";
|
||||
export * from "./state";
|
||||
export * from "./utils";
|
||||
5
data_migration_tools/client/src/features/auth/state.ts
Normal file
5
data_migration_tools/client/src/features/auth/state.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { ConfigurationParameters } from "../../api";
|
||||
|
||||
export interface AuthState {
|
||||
configuration: ConfigurationParameters;
|
||||
}
|
||||
13
data_migration_tools/client/src/features/auth/utils.ts
Normal file
13
data_migration_tools/client/src/features/auth/utils.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
3
data_migration_tools/client/src/features/delete/index.ts
Normal file
3
data_migration_tools/client/src/features/delete/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./state";
|
||||
export * from "./deleteSlice";
|
||||
export * from "./operations";
|
||||
@ -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 });
|
||||
}
|
||||
});
|
||||
2
data_migration_tools/client/src/features/delete/state.ts
Normal file
2
data_migration_tools/client/src/features/delete/state.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface DeleteState {}
|
||||
2
data_migration_tools/client/src/features/ui/constants.ts
Normal file
2
data_migration_tools/client/src/features/ui/constants.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// 標準のスナックバー表示時間(ミリ秒)
|
||||
export const DEFAULT_SNACKBAR_DURATION = 3000;
|
||||
5
data_migration_tools/client/src/features/ui/index.ts
Normal file
5
data_migration_tools/client/src/features/ui/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./constants";
|
||||
export * from "./selectors";
|
||||
export * from "./state";
|
||||
export * from "./uiSlice";
|
||||
export * from "./types";
|
||||
15
data_migration_tools/client/src/features/ui/selectors.ts
Normal file
15
data_migration_tools/client/src/features/ui/selectors.ts
Normal 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 };
|
||||
};
|
||||
8
data_migration_tools/client/src/features/ui/state.ts
Normal file
8
data_migration_tools/client/src/features/ui/state.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { SnackbarLevel } from "./types";
|
||||
|
||||
export interface UIState {
|
||||
isOpen: boolean;
|
||||
level: SnackbarLevel;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
1
data_migration_tools/client/src/features/ui/types.ts
Normal file
1
data_migration_tools/client/src/features/ui/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type SnackbarLevel = "info" | "error";
|
||||
38
data_migration_tools/client/src/features/ui/uiSlice.ts
Normal file
38
data_migration_tools/client/src/features/ui/uiSlice.ts
Normal 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;
|
||||
32
data_migration_tools/client/src/pages/deletePage/index.tsx
Normal file
32
data_migration_tools/client/src/pages/deletePage/index.tsx
Normal 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;
|
||||
15
data_migration_tools/client/src/pages/topPage/index.tsx
Normal file
15
data_migration_tools/client/src/pages/topPage/index.tsx
Normal 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
63
data_migration_tools/package-lock.json
generated
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "data_migration_tools",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"author": "",
|
||||
"description": "",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"packages": {},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.2.3",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "28.0.3",
|
||||
"license-checker": "^25.0.1",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"swagger-ui-express": "^4.5.0",
|
||||
"ts-jest": "28.0.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nest build && cp -r build dist",
|
||||
"build:exe": "nest build && cp -r build dist && sh ./buildTool.sh",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"prebuild": "rimraf dist",
|
||||
"start": "nest start",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"test": "jest",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:watch": "jest --watch"
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
DB_HOST=omds-mysql
|
||||
DB_PORT=3306
|
||||
DB_NAME=omds
|
||||
DB_USERNAME=omdsdbuser
|
||||
DB_PASSWORD=omdsdbpass
|
||||
@ -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
|
||||
12
data_migration_tools/server/buildTool.sh
Normal file
12
data_migration_tools/server/buildTool.sh
Normal 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
|
||||
1979
data_migration_tools/server/package-lock.json
generated
1979
data_migration_tools/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
25
data_migration_tools/server/src/api/generate.ts
Normal file
25
data_migration_tools/server/src/api/generate.ts
Normal 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();
|
||||
56
data_migration_tools/server/src/api/odms/openapi.json
Normal file
56
data_migration_tools/server/src/api/odms/openapi.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
57
data_migration_tools/server/src/common/entity/index.ts
Normal file
57
data_migration_tools/server/src/common/entity/index.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
70
data_migration_tools/server/src/common/error/code.ts
Normal file
70
data_migration_tools/server/src/common/error/code.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
エラーコード作成方針
|
||||
E+6桁(数字)で構成する。
|
||||
- 1~2桁目の値は種類(業務エラー、システムエラー...)
|
||||
- 3~4桁目の値は原因箇所(トークン、DB、...)
|
||||
- 5~6桁目の値は任意の重複しない値
|
||||
ex)
|
||||
E00XXXX : システムエラー(通信エラーやDB接続失敗など)
|
||||
E01XXXX : 業務エラー
|
||||
EXX00XX : 内部エラー(内部プログラムのエラー)
|
||||
EXX01XX : トークンエラー(トークン認証関連)
|
||||
EXX02XX : DBエラー(DB関連)
|
||||
EXX03XX : ADB2Cエラー(DB関連)
|
||||
*/
|
||||
export const ErrorCodes = [
|
||||
'E009999', // 汎用エラー
|
||||
'E000101', // トークン形式不正エラー
|
||||
'E000102', // トークン有効期限切れエラー
|
||||
'E000103', // トークン非アクティブエラー
|
||||
'E000104', // トークン署名エラー
|
||||
'E000105', // トークン発行元エラー
|
||||
'E000106', // トークンアルゴリズムエラー
|
||||
'E000107', // トークン不足エラー
|
||||
'E000108', // トークン権限エラー
|
||||
'E000301', // ADB2Cへのリクエスト上限超過エラー
|
||||
'E000401', // IPアドレス未設定エラー
|
||||
'E000501', // リクエストID未設定エラー
|
||||
'E010001', // パラメータ形式不正エラー
|
||||
'E010201', // 未認証ユーザエラー
|
||||
'E010202', // 認証済ユーザエラー
|
||||
'E010203', // 管理ユーザ権限エラー
|
||||
'E010204', // ユーザ不在エラー
|
||||
'E010205', // DBのRoleが想定外の値エラー
|
||||
'E010206', // DBのTierが想定外の値エラー
|
||||
'E010207', // ユーザーのRole変更不可エラー
|
||||
'E010208', // ユーザーの暗号化パスワード不足エラー
|
||||
'E010209', // ユーザーの同意済み利用規約バージョンが最新でないエラー
|
||||
'E010301', // メールアドレス登録済みエラー
|
||||
'E010302', // authorId重複エラー
|
||||
'E010401', // PONumber重複エラー
|
||||
'E010501', // アカウント不在エラー
|
||||
'E010502', // アカウント情報変更不可エラー
|
||||
'E010503', // 代行操作不許可エラー
|
||||
'E010504', // アカウントロックエラー
|
||||
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||
'E010602', // タスク変更権限不足エラー
|
||||
'E010603', // タスク不在エラー
|
||||
'E010701', // Blobファイル不在エラー
|
||||
'E010801', // ライセンス不在エラー
|
||||
'E010802', // ライセンス取り込み済みエラー
|
||||
'E010803', // ライセンス発行済みエラー
|
||||
'E010804', // ライセンス不足エラー
|
||||
'E010805', // ライセンス有効期限切れエラー
|
||||
'E010806', // ライセンス割り当て不可エラー
|
||||
'E010807', // ライセンス割り当て解除済みエラー
|
||||
'E010808', // ライセンス注文キャンセル不可エラー
|
||||
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
|
||||
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
|
||||
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
|
||||
'E010812', // ライセンス未割当エラー
|
||||
'E010908', // タイピストグループ不在エラー
|
||||
'E010909', // タイピストグループ名重複エラー
|
||||
'E011001', // ワークタイプ重複エラー
|
||||
'E011002', // ワークタイプ登録上限超過エラー
|
||||
'E011003', // ワークタイプ不在エラー
|
||||
'E011004', // ワークタイプ使用中エラー
|
||||
'E012001', // テンプレートファイル不在エラー
|
||||
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
'E013002', // ワークフロー不在エラー
|
||||
] as const;
|
||||
@ -0,0 +1,10 @@
|
||||
import { errors } from './message';
|
||||
import { ErrorCodeType, ErrorResponse } from './types/types';
|
||||
|
||||
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
|
||||
const msg = errors[errorcode];
|
||||
return {
|
||||
code: errorcode,
|
||||
message: msg,
|
||||
};
|
||||
};
|
||||
59
data_migration_tools/server/src/common/error/message.ts
Normal file
59
data_migration_tools/server/src/common/error/message.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Errors } from './types/types';
|
||||
|
||||
// エラーコードとメッセージ対応表
|
||||
export const errors: Errors = {
|
||||
E009999: 'Internal Server Error.',
|
||||
E000101: 'Token invalid format Error.',
|
||||
E000102: 'Token expired Error.',
|
||||
E000103: 'Token not before Error',
|
||||
E000104: 'Token invalid signature Error.',
|
||||
E000105: 'Token invalid issuer Error.',
|
||||
E000106: 'Token invalid algorithm Error.',
|
||||
E000107: 'Token is not exist Error.',
|
||||
E000108: 'Token authority failed Error.',
|
||||
E000301: 'ADB2C request limit exceeded Error',
|
||||
E000401: 'IP address not found Error.',
|
||||
E000501: 'Request ID not found Error.',
|
||||
E010001: 'Param invalid format Error.',
|
||||
E010201: 'Email not verified user Error.',
|
||||
E010202: 'Email already verified user Error.',
|
||||
E010203: 'Administrator Permissions Error.',
|
||||
E010204: 'User not Found Error.',
|
||||
E010205: 'Role from DB is unexpected value Error.',
|
||||
E010206: 'Tier from DB is unexpected value Error.',
|
||||
E010207: 'User role change not allowed Error.',
|
||||
E010208: 'User encryption password not found Error.',
|
||||
E010209: 'Accepted term not latest Error.',
|
||||
E010301: 'This email user already created Error',
|
||||
E010302: 'This AuthorId already used Error',
|
||||
E010401: 'This PoNumber already used Error',
|
||||
E010501: 'Account not Found Error.',
|
||||
E010502: 'Account information cannot be changed Error.',
|
||||
E010503: 'Delegation not allowed Error.',
|
||||
E010504: 'Account is locked Error.',
|
||||
E010601: 'Task is not Editable Error',
|
||||
E010602: 'No task edit permissions Error',
|
||||
E010603: 'Task not found Error.',
|
||||
E010701: 'File not found in Blob Storage Error.',
|
||||
E010801: 'License not exist Error',
|
||||
E010802: 'License already activated Error',
|
||||
E010803: 'License already issued Error',
|
||||
E010804: 'License shortage Error',
|
||||
E010805: 'License is expired Error',
|
||||
E010806: 'License is unavailable Error',
|
||||
E010807: 'License is already deallocated Error',
|
||||
E010808: 'Order cancel failed Error',
|
||||
E010809: 'Already license order status changed Error',
|
||||
E010810: 'Cancellation period expired error',
|
||||
E010811: 'Already license allocated Error',
|
||||
E010812: 'License not allocated Error',
|
||||
E010908: 'Typist Group not exist Error',
|
||||
E010909: 'Typist Group name already exist Error',
|
||||
E011001: 'This WorkTypeID already used Error',
|
||||
E011002: 'WorkTypeID create limit exceeded Error',
|
||||
E011003: 'WorkTypeID not found Error',
|
||||
E011004: 'WorkTypeID is in use Error',
|
||||
E012001: 'Template file not found Error',
|
||||
E013001: 'AuthorId and WorktypeId pair already exists Error',
|
||||
E013002: 'Workflow not found Error',
|
||||
};
|
||||
15
data_migration_tools/server/src/common/error/types/types.ts
Normal file
15
data_migration_tools/server/src/common/error/types/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ErrorCodes } from '../code';
|
||||
|
||||
export class ErrorResponse {
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
@ApiProperty()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export type ErrorCodeType = (typeof ErrorCodes)[number];
|
||||
|
||||
export type Errors = {
|
||||
[P in ErrorCodeType]: string;
|
||||
};
|
||||
17
data_migration_tools/server/src/common/errors/code.ts
Normal file
17
data_migration_tools/server/src/common/errors/code.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
エラーコード作成方針
|
||||
E+6桁(数字)で構成する。
|
||||
- 1~2桁目の値は種類(業務エラー、システムエラー...)
|
||||
- 3~4桁目の値は原因箇所(トークン、DB、...)
|
||||
- 5~6桁目の値は任意の重複しない値
|
||||
ex)
|
||||
E00XXXX : システムエラー(通信エラーやDB接続失敗など)
|
||||
E01XXXX : 業務エラー
|
||||
EXX00XX : 内部エラー(内部プログラムのエラー)
|
||||
EXX01XX : トークンエラー(トークン認証関連)
|
||||
EXX02XX : DBエラー(DB関連)
|
||||
EXX03XX : ADB2Cエラー(DB関連)
|
||||
*/
|
||||
export const ErrorCodes = [
|
||||
"E009999", // 汎用エラー
|
||||
] as const;
|
||||
@ -0,0 +1,10 @@
|
||||
import { errors } from "./message";
|
||||
import { ErrorCodeType, ErrorResponse } from "./types/types";
|
||||
|
||||
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
|
||||
const msg = errors[errorcode];
|
||||
return {
|
||||
code: errorcode,
|
||||
message: msg,
|
||||
};
|
||||
};
|
||||
6
data_migration_tools/server/src/common/errors/message.ts
Normal file
6
data_migration_tools/server/src/common/errors/message.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Errors } from "./types/types";
|
||||
|
||||
// エラーコードとメッセージ対応表
|
||||
export const errors: Errors = {
|
||||
E009999: "Internal Server Error.",
|
||||
};
|
||||
15
data_migration_tools/server/src/common/errors/types/types.ts
Normal file
15
data_migration_tools/server/src/common/errors/types/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ErrorCodes } from "../code";
|
||||
|
||||
export class ErrorResponse {
|
||||
@ApiProperty()
|
||||
message: string;
|
||||
@ApiProperty()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export type ErrorCodeType = (typeof ErrorCodes)[number];
|
||||
|
||||
export type Errors = {
|
||||
[P in ErrorCodeType]: string;
|
||||
};
|
||||
32
data_migration_tools/server/src/common/log/context.ts
Normal file
32
data_migration_tools/server/src/common/log/context.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Request } from 'express';
|
||||
import { Context } from './types';
|
||||
|
||||
export const makeContext = (
|
||||
externalId: string,
|
||||
requestId: string,
|
||||
delegationId?: string,
|
||||
): Context => {
|
||||
return new Context(externalId, requestId, delegationId);
|
||||
};
|
||||
|
||||
// リクエストヘッダーからrequestIdを取得する
|
||||
export const retrieveRequestId = (req: Request): string | undefined => {
|
||||
return req.header('x-request-id');
|
||||
};
|
||||
|
||||
/**
|
||||
* リクエストのIPアドレスを取得します
|
||||
* @param {Request}
|
||||
* @return {string | undefined}
|
||||
*/
|
||||
export const retrieveIp = (req: Request): string | undefined => {
|
||||
// ローカル環境では直近の送信元IPを取得する
|
||||
if (process.env.STAGE === 'local') {
|
||||
return req.ip;
|
||||
}
|
||||
const ip = req.header('x-forwarded-for');
|
||||
if (typeof ip === 'string') {
|
||||
return ip;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
4
data_migration_tools/server/src/common/log/index.ts
Normal file
4
data_migration_tools/server/src/common/log/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Context } from "./types";
|
||||
import { makeContext, retrieveRequestId, retrieveIp } from "./context";
|
||||
|
||||
export { Context, makeContext, retrieveRequestId, retrieveIp };
|
||||
34
data_migration_tools/server/src/common/log/types.ts
Normal file
34
data_migration_tools/server/src/common/log/types.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export class Context {
|
||||
/**
|
||||
* APIの操作ユーザーを追跡するためのID
|
||||
*/
|
||||
trackingId: string;
|
||||
/**
|
||||
* APIの操作ユーザーのIPアドレス
|
||||
*/
|
||||
ip: string;
|
||||
/**
|
||||
* ユーザーの操作を一意に識別するためのID
|
||||
*/
|
||||
requestId: string;
|
||||
/**
|
||||
* APIの代行操作ユーザーを追跡するためのID
|
||||
*/
|
||||
delegationId?: string | undefined;
|
||||
|
||||
constructor(externalId: string, requestId: string, delegationId?: string) {
|
||||
this.trackingId = externalId;
|
||||
this.delegationId = delegationId;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
/**
|
||||
* ログにユーザーを特定する情報を出力する
|
||||
*/
|
||||
getTrackingId(): string {
|
||||
if (this.delegationId) {
|
||||
return `${this.requestId}_${this.trackingId} by ${this.delegationId}`;
|
||||
} else {
|
||||
return `${this.requestId}_${this.trackingId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
data_migration_tools/server/src/common/password/index.ts
Normal file
3
data_migration_tools/server/src/common/password/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { makePassword } from "./password";
|
||||
|
||||
export { makePassword };
|
||||
35
data_migration_tools/server/src/common/password/password.ts
Normal file
35
data_migration_tools/server/src/common/password/password.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export const makePassword = (): string => {
|
||||
// パスワードの文字数を決定
|
||||
const passLength = 8;
|
||||
|
||||
// パスワードに使用可能な文字を決定(今回はアルファベットの大文字と小文字 + 数字 + symbolsの記号)
|
||||
const lowerCase = "abcdefghijklmnopqrstuvwxyz";
|
||||
const upperCase = lowerCase.toLocaleUpperCase();
|
||||
const numbers = "0123456789";
|
||||
const symbols = "@#$%^&*\\-_+=[]{}|:',.?/`~\"();!";
|
||||
const chars = lowerCase + upperCase + numbers + symbols;
|
||||
|
||||
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!)から2種類以上組み合わせ
|
||||
const charaTypePattern =
|
||||
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
|
||||
|
||||
// autoGeneratedPasswordが以上の条件を満たせばvalidがtrueになる
|
||||
let valid = false;
|
||||
let autoGeneratedPassword: string = "";
|
||||
|
||||
while (!valid) {
|
||||
// パスワードをランダムに決定
|
||||
while (autoGeneratedPassword.length < passLength) {
|
||||
// 上で決定したcharsの中からランダムに1文字ずつ追加
|
||||
const index = Math.floor(Math.random() * chars.length);
|
||||
autoGeneratedPassword += chars[index];
|
||||
}
|
||||
|
||||
// パスワードが上で決定した条件をすべて満たしているかチェック
|
||||
// 条件を満たすまでループ
|
||||
valid =
|
||||
autoGeneratedPassword.length == passLength &&
|
||||
charaTypePattern.test(autoGeneratedPassword);
|
||||
}
|
||||
return autoGeneratedPassword;
|
||||
};
|
||||
143
data_migration_tools/server/src/common/repository/index.ts
Normal file
143
data_migration_tools/server/src/common/repository/index.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import {
|
||||
ObjectLiteral,
|
||||
Repository,
|
||||
EntityTarget,
|
||||
UpdateResult,
|
||||
DeleteResult,
|
||||
UpdateQueryBuilder,
|
||||
Brackets,
|
||||
FindOptionsWhere,
|
||||
} from 'typeorm';
|
||||
import { Context } from '../log';
|
||||
|
||||
/**
|
||||
* VS Code上で型解析エラーが発生するため、typeorm内の型定義と同一の型定義をここに記述する
|
||||
*/
|
||||
type QueryDeepPartialEntity<T> = _QueryDeepPartialEntity<
|
||||
ObjectLiteral extends T ? unknown : T
|
||||
>;
|
||||
type _QueryDeepPartialEntity<T> = {
|
||||
[P in keyof T]?:
|
||||
| (T[P] extends Array<infer U>
|
||||
? Array<_QueryDeepPartialEntity<U>>
|
||||
: T[P] extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<_QueryDeepPartialEntity<U>>
|
||||
: _QueryDeepPartialEntity<T[P]>)
|
||||
| (() => string);
|
||||
};
|
||||
|
||||
interface InsertEntityOptions {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const insertEntity = async <T extends InsertEntityOptions & ObjectLiteral>(
|
||||
entity: EntityTarget<T>,
|
||||
repository: Repository<T>,
|
||||
value: QueryDeepPartialEntity<T>,
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<T> => {
|
||||
let query = repository.createQueryBuilder().insert().into(entity);
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
const result = await query.values(value).execute();
|
||||
// result.identifiers[0].idがnumber型でない場合はエラー
|
||||
if (typeof result.identifiers[0].id !== 'number') {
|
||||
throw new Error('Failed to insert entity');
|
||||
}
|
||||
const where: FindOptionsWhere<T> = { id: result.identifiers[0].id } as T;
|
||||
|
||||
// 結果をもとにセレクトする
|
||||
const inserted = await repository.findOne({
|
||||
where,
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (!inserted) {
|
||||
throw new Error('Failed to insert entity');
|
||||
}
|
||||
return inserted;
|
||||
};
|
||||
|
||||
const insertEntities = async <T extends InsertEntityOptions & ObjectLiteral>(
|
||||
entity: EntityTarget<T>,
|
||||
repository: Repository<T>,
|
||||
values: QueryDeepPartialEntity<T>[],
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<T[]> => {
|
||||
let query = repository.createQueryBuilder().insert().into(entity);
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
const result = await query.values(values).execute();
|
||||
|
||||
// 挿入するレコードが0で、結果も0であれば、からの配列を返す
|
||||
if (values.length === 0 && result.identifiers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 挿入するレコード数と挿入されたレコード数が一致しない場合はエラー
|
||||
if (result.identifiers.length !== values.length) {
|
||||
throw new Error('Failed to insert entities');
|
||||
}
|
||||
const where: FindOptionsWhere<T>[] = result.identifiers.map((i) => {
|
||||
// idがnumber型でない場合はエラー
|
||||
if (typeof i.id !== 'number') {
|
||||
throw new Error('Failed to insert entities');
|
||||
}
|
||||
return { id: i.id } as T;
|
||||
});
|
||||
|
||||
// 結果をもとにセレクトする
|
||||
const inserted = await repository.find({
|
||||
where,
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (!inserted) {
|
||||
throw new Error('Failed to insert entity');
|
||||
}
|
||||
return inserted;
|
||||
};
|
||||
|
||||
const updateEntity = async <T extends ObjectLiteral>(
|
||||
repository: Repository<T>,
|
||||
criteria:
|
||||
| string
|
||||
| ((qb: UpdateQueryBuilder<T>) => string)
|
||||
| Brackets
|
||||
| ObjectLiteral
|
||||
| ObjectLiteral[],
|
||||
values: QueryDeepPartialEntity<T>,
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<UpdateResult> => {
|
||||
let query = repository.createQueryBuilder().update();
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
return await query.set(values).where(criteria).execute();
|
||||
};
|
||||
|
||||
const deleteEntity = async <T extends ObjectLiteral>(
|
||||
repository: Repository<T>,
|
||||
criteria: string | Brackets | ObjectLiteral | ObjectLiteral[],
|
||||
isCommentOut: boolean,
|
||||
context: Context,
|
||||
): Promise<DeleteResult> => {
|
||||
let query = repository.createQueryBuilder().delete();
|
||||
if (isCommentOut) {
|
||||
query = query.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
}
|
||||
return await query.where(criteria).execute();
|
||||
};
|
||||
|
||||
export { insertEntity, insertEntities, updateEntity, deleteEntity };
|
||||
10
data_migration_tools/server/src/common/types/role/index.ts
Normal file
10
data_migration_tools/server/src/common/types/role/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ADMIN_ROLES, USER_ROLES } from '../../../constants';
|
||||
|
||||
/**
|
||||
* Token.roleに配置されうる文字列リテラル型
|
||||
*/
|
||||
export type Roles =
|
||||
| (typeof ADMIN_ROLES)[keyof typeof ADMIN_ROLES]
|
||||
| (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||
|
||||
export type UserRoles = (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||
27
data_migration_tools/server/src/common/types/sort/index.ts
Normal file
27
data_migration_tools/server/src/common/types/sort/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {
|
||||
TASK_LIST_SORTABLE_ATTRIBUTES,
|
||||
SORT_DIRECTIONS,
|
||||
} from '../../../constants';
|
||||
|
||||
export type TaskListSortableAttribute =
|
||||
(typeof TASK_LIST_SORTABLE_ATTRIBUTES)[number];
|
||||
|
||||
export type SortDirection = (typeof SORT_DIRECTIONS)[number];
|
||||
|
||||
export const isTaskListSortableAttribute = (
|
||||
arg: string,
|
||||
): arg is TaskListSortableAttribute => {
|
||||
const param = arg as TaskListSortableAttribute;
|
||||
if (TASK_LIST_SORTABLE_ATTRIBUTES.includes(param)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isSortDirection = (arg: string): arg is SortDirection => {
|
||||
const param = arg as SortDirection;
|
||||
if (SORT_DIRECTIONS.includes(param)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
11
data_migration_tools/server/src/common/types/sort/util.ts
Normal file
11
data_migration_tools/server/src/common/types/sort/util.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { SortDirection, TaskListSortableAttribute } from '.';
|
||||
|
||||
export const getDirection = (direction: SortDirection): SortDirection => {
|
||||
return direction;
|
||||
};
|
||||
|
||||
export const getTaskListSortableAttribute = (
|
||||
TaskListSortableAttribute: TaskListSortableAttribute,
|
||||
): TaskListSortableAttribute => {
|
||||
return TaskListSortableAttribute;
|
||||
};
|
||||
231
data_migration_tools/server/src/common/types/types.ts
Normal file
231
data_migration_tools/server/src/common/types/types.ts
Normal file
@ -0,0 +1,231 @@
|
||||
export class csvInputFile {
|
||||
type: string;
|
||||
account_id: string;
|
||||
parent_id: string;
|
||||
email: string;
|
||||
company_name: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
country: string;
|
||||
state: string;
|
||||
start_date: Date;
|
||||
expired_date: Date;
|
||||
user_email: string;
|
||||
author_id: string;
|
||||
recording_mode: string;
|
||||
wt1: string;
|
||||
wt2: string;
|
||||
wt3: string;
|
||||
wt4: string;
|
||||
wt5: string;
|
||||
wt6: string;
|
||||
wt7: string;
|
||||
wt8: string;
|
||||
wt9: string;
|
||||
wt10: string;
|
||||
wt11: string;
|
||||
wt12: string;
|
||||
wt13: string;
|
||||
wt14: string;
|
||||
wt15: string;
|
||||
wt16: string;
|
||||
wt17: string;
|
||||
wt18: string;
|
||||
wt19: string;
|
||||
wt20: string;
|
||||
}
|
||||
export class AccountsOutputFileStep1 {
|
||||
accountId: number;
|
||||
type: string;
|
||||
companyName: string;
|
||||
country: string;
|
||||
dealerAccountId?: number;
|
||||
adminName: string;
|
||||
adminMail: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export class AccountsOutputFile {
|
||||
accountId: number;
|
||||
type: number;
|
||||
companyName: string;
|
||||
country: string;
|
||||
dealerAccountId?: number;
|
||||
adminName: string;
|
||||
adminMail: string;
|
||||
userId: number;
|
||||
}
|
||||
export class AccountsInputFile {
|
||||
accountId: number;
|
||||
type: number;
|
||||
companyName: string;
|
||||
country: string;
|
||||
dealerAccountId?: number;
|
||||
adminName: string;
|
||||
adminMail: string;
|
||||
userId: number;
|
||||
}
|
||||
export class UsersOutputFile {
|
||||
accountId: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
role: string;
|
||||
authorId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export class UsersInputFile {
|
||||
accountId: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
role: string;
|
||||
authorId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export class LicensesOutputFile {
|
||||
expiry_date: string;
|
||||
account_id: number;
|
||||
type: string;
|
||||
status: string;
|
||||
allocated_user_id?: number;
|
||||
}
|
||||
export class LicensesInputFile {
|
||||
expiry_date: string;
|
||||
account_id: number;
|
||||
type: string;
|
||||
status: string;
|
||||
allocated_user_id?: number;
|
||||
}
|
||||
|
||||
export class WorktypesOutputFile {
|
||||
account_id: number;
|
||||
custom_worktype_id: string;
|
||||
}
|
||||
export class WorktypesInputFile {
|
||||
account_id: number;
|
||||
custom_worktype_id: string;
|
||||
}
|
||||
|
||||
export class CardLicensesInputFile {
|
||||
license_id: number;
|
||||
issue_id: number;
|
||||
card_license_key: string;
|
||||
activated_at?: string;
|
||||
created_at?: string;
|
||||
created_by?: string;
|
||||
updated_at?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export function isAccountsInputFileArray(obj: any): obj is AccountsInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isAccountsInputFile(item));
|
||||
}
|
||||
export function isAccountsInputFile(obj: any): obj is AccountsInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"accountId" in obj &&
|
||||
typeof obj.accountId === "number" &&
|
||||
"type" in obj &&
|
||||
typeof obj.type === "number" &&
|
||||
"companyName" in obj &&
|
||||
typeof obj.companyName === "string" &&
|
||||
"country" in obj &&
|
||||
typeof obj.country === "string" &&
|
||||
("dealerAccountId" in obj
|
||||
? typeof obj.dealerAccountId === "number"
|
||||
: true) &&
|
||||
"adminName" in obj &&
|
||||
typeof obj.adminName === "string" &&
|
||||
"adminMail" in obj &&
|
||||
typeof obj.adminMail === "string" &&
|
||||
"userId" in obj &&
|
||||
typeof obj.userId === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export function isUsersInputFileArray(obj: any): obj is UsersInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isUsersInputFile(item));
|
||||
}
|
||||
export function isUsersInputFile(obj: any): obj is UsersInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"accountId" in obj &&
|
||||
"userId" in obj &&
|
||||
"name" in obj &&
|
||||
"role" in obj &&
|
||||
"authorId" in obj &&
|
||||
"email" in obj &&
|
||||
typeof obj.accountId === "number" &&
|
||||
typeof obj.userId === "number" &&
|
||||
typeof obj.name === "string" &&
|
||||
typeof obj.role === "string" &&
|
||||
typeof obj.authorId === "string" &&
|
||||
typeof obj.email === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isLicensesInputFileArray(obj: any): obj is LicensesInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isLicensesInputFile(item));
|
||||
}
|
||||
export function isLicensesInputFile(obj: any): obj is LicensesInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"expiry_date" in obj &&
|
||||
"account_id" in obj &&
|
||||
"type" in obj &&
|
||||
"status" in obj &&
|
||||
typeof obj.expiry_date === "string" &&
|
||||
typeof obj.account_id === "number" &&
|
||||
typeof obj.type === "string" &&
|
||||
typeof obj.status === "string" &&
|
||||
(obj.allocated_user_id === null ||
|
||||
typeof obj.allocated_user_id === "number")
|
||||
);
|
||||
}
|
||||
|
||||
export function isWorktypesInputFileArray(
|
||||
obj: any
|
||||
): obj is WorktypesInputFile[] {
|
||||
return Array.isArray(obj) && obj.every((item) => isWorktypesInputFile(item));
|
||||
}
|
||||
export function isWorktypesInputFile(obj: any): obj is WorktypesInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"account_id" in obj &&
|
||||
"custom_worktype_id" in obj &&
|
||||
typeof obj.account_id === "number" &&
|
||||
typeof obj.custom_worktype_id === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isCardLicensesInputFileArray(
|
||||
obj: any
|
||||
): obj is CardLicensesInputFile[] {
|
||||
return (
|
||||
Array.isArray(obj) && obj.every((item) => isCardLicensesInputFile(item))
|
||||
);
|
||||
}
|
||||
export function isCardLicensesInputFile(
|
||||
obj: any
|
||||
): obj is CardLicensesInputFile {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"license_id" in obj &&
|
||||
"issue_id" in obj &&
|
||||
"card_license_key" in obj &&
|
||||
typeof obj.license_id === "number" &&
|
||||
typeof obj.issue_id === "number" &&
|
||||
typeof obj.card_license_key === "string" &&
|
||||
(obj.activated_at === null || typeof obj.activated_at === "string") &&
|
||||
(obj.created_at === null || typeof obj.created_at === "string") &&
|
||||
(obj.created_by === null || typeof obj.created_by === "string") &&
|
||||
(obj.updated_at === null || typeof obj.updated_at === "string") &&
|
||||
(obj.updated_by === null || typeof obj.updated_by === "string")
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from "class-validator";
|
||||
|
||||
@ValidatorConstraint()
|
||||
export class IsAdminPassword implements ValidatorConstraintInterface {
|
||||
validate(value: string): boolean {
|
||||
// 8文字~64文字でなければ早期に不合格
|
||||
const minLength = 8;
|
||||
const maxLength = 64;
|
||||
if (value.length < minLength || value.length > maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!)から2種類以上組み合わせ
|
||||
const charaTypePattern =
|
||||
/^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/;
|
||||
return new RegExp(charaTypePattern).test(value);
|
||||
}
|
||||
defaultMessage(): string {
|
||||
return "Admin password rule not satisfied";
|
||||
}
|
||||
}
|
||||
|
||||
export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => {
|
||||
return (object: any, propertyName: string) => {
|
||||
registerDecorator({
|
||||
name: "IsAdminPasswordvalid",
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [],
|
||||
options: validationOptions,
|
||||
validator: IsAdminPassword,
|
||||
});
|
||||
};
|
||||
};
|
||||
406
data_migration_tools/server/src/constants/index.ts
Normal file
406
data_migration_tools/server/src/constants/index.ts
Normal 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;
|
||||
@ -0,0 +1,12 @@
|
||||
import { Controller, Logger } from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { AccountsService } from "./accounts.service";
|
||||
|
||||
@ApiTags("accounts")
|
||||
@Controller("accounts")
|
||||
export class AccountsController {
|
||||
private readonly logger = new Logger(AccountsController.name);
|
||||
constructor(
|
||||
private readonly accountService: AccountsService //private readonly cryptoService: CryptoService,
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UsersRepositoryModule } from "../../repositories/users/users.repository.module";
|
||||
import { AccountsRepositoryModule } from "../../repositories/accounts/accounts.repository.module";
|
||||
import { AccountsController } from "./accounts.controller";
|
||||
import { AccountsService } from "./accounts.service";
|
||||
import { AdB2cModule } from "../../gateways/adb2c/adb2c.module";
|
||||
import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
AdB2cModule,
|
||||
BlobstorageModule,
|
||||
],
|
||||
controllers: [AccountsController],
|
||||
providers: [AccountsService],
|
||||
})
|
||||
export class AccountsModule {}
|
||||
@ -0,0 +1,227 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
import { AccountsRepositoryService } from "../../repositories/accounts/accounts.repository.service";
|
||||
import {
|
||||
AdB2cService,
|
||||
ConflictError,
|
||||
isConflictError,
|
||||
} from "../../gateways/adb2c/adb2c.service";
|
||||
import { Account } from "../../repositories/accounts/entity/account.entity";
|
||||
import { User } from "../../repositories/users/entity/user.entity";
|
||||
import { MANUAL_RECOVERY_REQUIRED } from "../../constants";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
import { Context } from "../../common/log";
|
||||
import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service";
|
||||
|
||||
@Injectable()
|
||||
export class AccountsService {
|
||||
constructor(
|
||||
private readonly accountRepository: AccountsRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly blobStorageService: BlobstorageService
|
||||
) {}
|
||||
private readonly logger = new Logger(AccountsService.name);
|
||||
|
||||
/**
|
||||
* アカウント情報をDBに作成する
|
||||
* @param companyName
|
||||
* @param country
|
||||
* @param [dealerAccountId]
|
||||
* @returns account
|
||||
*/
|
||||
async createAccount(
|
||||
context: Context,
|
||||
companyName: string,
|
||||
country: string,
|
||||
dealerAccountId: number | undefined,
|
||||
email: string,
|
||||
password: string,
|
||||
username: string,
|
||||
role: string,
|
||||
acceptedEulaVersion: string,
|
||||
acceptedPrivacyNoticeVersion: string,
|
||||
acceptedDpaVersion: string,
|
||||
type: number,
|
||||
accountId: number,
|
||||
userId: number
|
||||
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.createAccount.name
|
||||
} | params: { ` +
|
||||
`dealerAccountId: ${dealerAccountId}, ` +
|
||||
`role: ${role}, ` +
|
||||
`acceptedEulaVersion: ${acceptedEulaVersion}, ` +
|
||||
`acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` +
|
||||
`acceptedDpaVersion: ${acceptedDpaVersion}, ` +
|
||||
`type: ${type}, ` +
|
||||
`accountId: ${accountId}, ` +
|
||||
`userId: ${userId} };`
|
||||
);
|
||||
try {
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
try {
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
context,
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] create externalUser failed`
|
||||
);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// メールアドレス重複エラー
|
||||
if (isConflictError(externalUser)) {
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] email conflict. externalUser: ${externalUser}`
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010301"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
let account: Account;
|
||||
let user: User;
|
||||
try {
|
||||
// アカウントと管理者をセットで作成
|
||||
const { newAccount, adminUser } =
|
||||
await this.accountRepository.createAccount(
|
||||
context,
|
||||
companyName,
|
||||
country,
|
||||
dealerAccountId,
|
||||
type,
|
||||
externalUser.sub,
|
||||
role,
|
||||
accountId,
|
||||
userId,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] adminUser.external_id: ${
|
||||
user.external_id
|
||||
}`
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(`[${context.getTrackingId()}] create account failed`);
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// 新規作成アカウント用のBlobコンテナを作成
|
||||
try {
|
||||
await this.blobStorageService.createContainer(
|
||||
context,
|
||||
account.id,
|
||||
country
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] create container failed`
|
||||
);
|
||||
//リカバリ処理
|
||||
// idpのユーザーを削除
|
||||
await this.deleteAdB2cUser(externalUser.sub, context);
|
||||
|
||||
// DBのアカウントを削除
|
||||
await this.deleteAccount(account.id, user.id, context);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
accountId: account.id,
|
||||
userId: user.id,
|
||||
externalUserId: user.external_id,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createAccount.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// AdB2cのユーザーを削除
|
||||
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
||||
private async deleteAdB2cUser(
|
||||
externalUserId: string,
|
||||
context: Context
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.createAccount.name
|
||||
} | params: { ` + `externalUserId: ${externalUserId}};`
|
||||
);
|
||||
try {
|
||||
await this.adB2cService.deleteUser(externalUserId, context);
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] delete externalUser: ${externalUserId} | params: { ` +
|
||||
`externalUserId: ${externalUserId}, };`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteAdB2cUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DBのアカウントを削除
|
||||
private async deleteAccount(
|
||||
accountId: number,
|
||||
userId: number,
|
||||
context: Context
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteAccount.name
|
||||
} | params: { accountId: ${accountId}, userId: ${userId} };`
|
||||
);
|
||||
try {
|
||||
await this.accountRepository.deleteAccount(context, accountId, userId);
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] delete account: ${accountId}, user: ${userId}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete account: ${accountId}, user: ${userId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteAccount.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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 {};
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
import { DataSource } from "typeorm";
|
||||
@ -0,0 +1 @@
|
||||
export class DeleteResponse {}
|
||||
@ -0,0 +1,209 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Req,
|
||||
Logger,
|
||||
HttpException,
|
||||
} from "@nestjs/common";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
import fs from "fs";
|
||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { Request } from "express";
|
||||
import { RegisterRequest, RegisterResponse } from "./types/types";
|
||||
import { RegisterService } from "./register.service";
|
||||
import { AccountsService } from "../accounts/accounts.service";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { makeContext } from "../../common/log";
|
||||
import {
|
||||
isAccountsInputFileArray,
|
||||
isUsersInputFileArray,
|
||||
isLicensesInputFileArray,
|
||||
isWorktypesInputFileArray,
|
||||
isCardLicensesInputFileArray,
|
||||
} from "../../common/types/types";
|
||||
import { makePassword } from "../../common/password/password";
|
||||
import {
|
||||
USER_ROLES,
|
||||
MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC,
|
||||
} from "../../constants";
|
||||
|
||||
@ApiTags("register")
|
||||
@Controller("register")
|
||||
export class RegisterController {
|
||||
private readonly logger = new Logger(RegisterController.name);
|
||||
constructor(
|
||||
private readonly registerService: RegisterService,
|
||||
private readonly accountsService: AccountsService,
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: RegisterResponse,
|
||||
description: "成功時のレスポンス",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: "想定外のサーバーエラー",
|
||||
})
|
||||
@ApiOperation({ operationId: "dataRegist" })
|
||||
async dataRegist(
|
||||
@Body() body: RegisterRequest,
|
||||
@Req() req: Request
|
||||
): Promise<RegisterResponse> {
|
||||
const context = makeContext("iko", "register");
|
||||
|
||||
const inputFilePath = body.inputFilePath;
|
||||
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.dataRegist.name
|
||||
} | params: { inputFilePath: ${inputFilePath}};`
|
||||
);
|
||||
|
||||
try {
|
||||
// 読み込みファイルのフルパス
|
||||
const accouncsFileFullPath = inputFilePath + "accounts.json";
|
||||
const usersFileFullPath = inputFilePath + "users.json";
|
||||
const licensesFileFullPath = inputFilePath + "licenses.json";
|
||||
const worktypesFileFullPath = inputFilePath + "worktypes.json";
|
||||
const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json";
|
||||
|
||||
// ファイル存在チェックと読み込み
|
||||
if (
|
||||
!fs.existsSync(accouncsFileFullPath) ||
|
||||
!fs.existsSync(usersFileFullPath) ||
|
||||
!fs.existsSync(licensesFileFullPath) ||
|
||||
!fs.existsSync(worktypesFileFullPath) ||
|
||||
!fs.existsSync(cardLicensesFileFullPath)
|
||||
) {
|
||||
this.logger.error(`file not exists from ${inputFilePath}`);
|
||||
throw new Error(`file not exists from ${inputFilePath}`);
|
||||
}
|
||||
|
||||
// アカウントの登録用ファイル読み込み
|
||||
const accountsObject = JSON.parse(
|
||||
fs.readFileSync(accouncsFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(account)
|
||||
if (!isAccountsInputFileArray(accountsObject)) {
|
||||
throw new Error("input file is not accountsInputFiles");
|
||||
}
|
||||
|
||||
for (const accountsInputFile of accountsObject) {
|
||||
// ランダムなパスワードを生成する
|
||||
const ramdomPassword = makePassword();
|
||||
await this.accountsService.createAccount(
|
||||
context,
|
||||
accountsInputFile.companyName,
|
||||
accountsInputFile.country,
|
||||
accountsInputFile.dealerAccountId,
|
||||
accountsInputFile.adminMail,
|
||||
ramdomPassword,
|
||||
accountsInputFile.adminName,
|
||||
"none",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
accountsInputFile.type,
|
||||
accountsInputFile.accountId,
|
||||
accountsInputFile.userId
|
||||
);
|
||||
|
||||
// ratelimit対応のためsleepを行う
|
||||
await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC);
|
||||
}
|
||||
// const accountsInputFiles = accountsObject as AccountsInputFile[];
|
||||
|
||||
// ユーザの登録用ファイル読み込み
|
||||
const usersObject = JSON.parse(
|
||||
fs.readFileSync(usersFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(user)
|
||||
if (!isUsersInputFileArray(usersObject)) {
|
||||
throw new Error("input file is not usersInputFiles");
|
||||
}
|
||||
|
||||
for (const usersInputFile of usersObject) {
|
||||
this.logger.log(usersInputFile.name);
|
||||
await this.usersService.createUser(
|
||||
context,
|
||||
usersInputFile.name,
|
||||
usersInputFile.role === USER_ROLES.AUTHOR
|
||||
? USER_ROLES.AUTHOR
|
||||
: USER_ROLES.NONE,
|
||||
usersInputFile.email,
|
||||
true,
|
||||
true,
|
||||
usersInputFile.accountId,
|
||||
usersInputFile.userId,
|
||||
usersInputFile.authorId,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
);
|
||||
// ratelimit対応のためsleepを行う
|
||||
await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC);
|
||||
}
|
||||
|
||||
// ライセンスの登録用ファイル読み込み
|
||||
const licensesObject = JSON.parse(
|
||||
fs.readFileSync(licensesFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(license)
|
||||
if (!isLicensesInputFileArray(licensesObject)) {
|
||||
throw new Error("input file is not licensesInputFiles");
|
||||
}
|
||||
|
||||
// ワークタイプの登録用ファイル読み込み
|
||||
const worktypesObject = JSON.parse(
|
||||
fs.readFileSync(worktypesFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(Worktypes)
|
||||
if (!isWorktypesInputFileArray(worktypesObject)) {
|
||||
throw new Error("input file is not WorktypesInputFiles");
|
||||
}
|
||||
|
||||
// カードライセンスの登録用ファイル読み込み
|
||||
const cardLicensesObject = JSON.parse(
|
||||
fs.readFileSync(cardLicensesFileFullPath, "utf8")
|
||||
);
|
||||
|
||||
// 型ガード(cardLicenses)
|
||||
if (!isCardLicensesInputFileArray(cardLicensesObject)) {
|
||||
throw new Error("input file is not cardLicensesInputFiles");
|
||||
}
|
||||
|
||||
// ライセンス・ワークタイプ・カードライセンスの登録
|
||||
await this.registerService.registLicenseAndWorktypeData(
|
||||
context,
|
||||
licensesObject,
|
||||
worktypesObject,
|
||||
cardLicensesObject
|
||||
);
|
||||
|
||||
return {};
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.dataRegist.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { RegisterController } from "./register.controller";
|
||||
import { RegisterService } from "./register.service";
|
||||
import { AccountsService } from "../accounts/accounts.service";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { LicensesRepositoryModule } from "../../repositories/licenses/licenses.repository.module";
|
||||
import { WorktypesRepositoryModule } from "../../repositories/worktypes/worktypes.repository.module";
|
||||
import { UsersRepositoryModule } from "../../repositories/users/users.repository.module";
|
||||
import { AccountsRepositoryModule } from "../../repositories/accounts/accounts.repository.module";
|
||||
import { AdB2cModule } from "../../gateways/adb2c/adb2c.module";
|
||||
import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
LicensesRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
AccountsRepositoryModule,
|
||||
UsersRepositoryModule,
|
||||
AdB2cModule,
|
||||
BlobstorageModule,
|
||||
],
|
||||
controllers: [RegisterController],
|
||||
providers: [RegisterService, AccountsService, UsersService],
|
||||
})
|
||||
export class RegisterModule {}
|
||||
@ -0,0 +1,68 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
import { Context } from "../../common/log";
|
||||
import {
|
||||
LicensesInputFile,
|
||||
WorktypesInputFile,
|
||||
CardLicensesInputFile,
|
||||
} from "../../common/types/types";
|
||||
import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service";
|
||||
import { WorktypesRepositoryService } from "../../repositories/worktypes/worktypes.repository.service";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
@Injectable()
|
||||
export class RegisterService {
|
||||
constructor(
|
||||
private readonly licensesRepository: LicensesRepositoryService,
|
||||
private readonly worktypesRepository: WorktypesRepositoryService
|
||||
) {}
|
||||
private readonly logger = new Logger(RegisterService.name);
|
||||
|
||||
/**
|
||||
* Regist Data
|
||||
* @param inputFilePath: string
|
||||
*/
|
||||
async registLicenseAndWorktypeData(
|
||||
context: Context,
|
||||
licensesInputFiles: LicensesInputFile[],
|
||||
worktypesInputFiles: WorktypesInputFile[],
|
||||
cardlicensesInputFiles: CardLicensesInputFile[]
|
||||
): Promise<void> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.registLicenseAndWorktypeData.name
|
||||
}`
|
||||
);
|
||||
|
||||
try {
|
||||
this.logger.log("Licenses register start");
|
||||
await this.licensesRepository.insertLicenses(context, licensesInputFiles);
|
||||
this.logger.log("Licenses register end");
|
||||
|
||||
this.logger.log("Worktypes register start");
|
||||
await this.worktypesRepository.createWorktype(
|
||||
context,
|
||||
worktypesInputFiles
|
||||
);
|
||||
this.logger.log("Worktypes register end");
|
||||
|
||||
this.logger.log("CardLicenses register start");
|
||||
await this.licensesRepository.insertCardLicenses(
|
||||
context,
|
||||
cardlicensesInputFiles
|
||||
);
|
||||
this.logger.log("CardLicenses register end");
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${
|
||||
this.registLicenseAndWorktypeData.name
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RegisterRequest {
|
||||
@ApiProperty()
|
||||
inputFilePath: string;
|
||||
|
||||
}
|
||||
|
||||
export class RegisterResponse {}
|
||||
|
||||
@ -0,0 +1,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を4つのJSONファイルの出力する(出力先はinputと同じにする)
|
||||
const outputFilePath = body.inputFilePath;
|
||||
const usersOutputFile = transferResponse.usersOutputFileLines;
|
||||
const licensesOutputFile = transferResponse.licensesOutputFileLines;
|
||||
const worktypesOutputFile = transferResponse.worktypesOutputFileLines;
|
||||
this.transferService.outputJsonFile(
|
||||
context,
|
||||
outputFilePath,
|
||||
accountsOutputFile,
|
||||
usersOutputFile,
|
||||
licensesOutputFile,
|
||||
worktypesOutputFile
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.dataRegist.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { transferController } from "./transfer.controller";
|
||||
import { transferService } from "./transfer.service";
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [transferController],
|
||||
providers: [transferService],
|
||||
})
|
||||
export class transferModule {}
|
||||
@ -0,0 +1,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をBC(Countryの親)に付け替える
|
||||
distributor.dealerAccountId = account.dealerAccountId;
|
||||
}
|
||||
});
|
||||
// typeがCountryのアカウントを取り除く
|
||||
accountsOutputFileStep1 = accountsOutputFileStep1.filter(
|
||||
(account) => account.type !== MIGRATION_TYPE.COUNTRY
|
||||
);
|
||||
|
||||
// typeをtierに変換し、AccountsOutputFileに変換する
|
||||
let accountsOutputFile: AccountsOutputFile[] = [];
|
||||
accountsOutputFileStep1.forEach((account) => {
|
||||
let tier = 0;
|
||||
switch (account.type) {
|
||||
case MIGRATION_TYPE.ADMINISTRATOR:
|
||||
tier = TIERS.TIER1;
|
||||
break;
|
||||
case MIGRATION_TYPE.BC:
|
||||
tier = TIERS.TIER2;
|
||||
break;
|
||||
case MIGRATION_TYPE.DISTRIBUTOR:
|
||||
tier = TIERS.TIER3;
|
||||
break;
|
||||
case MIGRATION_TYPE.DEALER:
|
||||
tier = TIERS.TIER4;
|
||||
break;
|
||||
case MIGRATION_TYPE.CUSTOMER:
|
||||
tier = TIERS.TIER5;
|
||||
break;
|
||||
}
|
||||
accountsOutputFile.push({
|
||||
accountId: account.accountId,
|
||||
type: tier,
|
||||
companyName: account.companyName,
|
||||
country: account.country,
|
||||
dealerAccountId: account.dealerAccountId,
|
||||
adminName: account.adminName,
|
||||
adminMail: account.adminMail,
|
||||
userId: account.userId,
|
||||
});
|
||||
});
|
||||
return accountsOutputFile;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.relocateHierarchy.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSONファイルの出力
|
||||
* @param outputFilePath: string
|
||||
* @param accountsOutputFile: AccountsOutputFile[]
|
||||
* @param usersOutputFile: UsersOutputFile[]
|
||||
* @param licensesOutputFile: LicensesOutputFile[]
|
||||
* @param worktypesOutputFile: WorktypesOutputFile[]
|
||||
*/
|
||||
async outputJsonFile(
|
||||
context: Context,
|
||||
outputFilePath: string,
|
||||
accountsOutputFile: AccountsOutputFile[],
|
||||
usersOutputFile: UsersOutputFile[],
|
||||
licensesOutputFile: LicensesOutputFile[],
|
||||
worktypesOutputFile: WorktypesOutputFile[]
|
||||
): Promise<void> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.outputJsonFile.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
// JSONファイルの出力を行う
|
||||
// accountsOutputFile配列の出力
|
||||
const accountsOutputFileJson = JSON.stringify(accountsOutputFile);
|
||||
fs.writeFileSync(
|
||||
`${outputFilePath}_accounts.json`,
|
||||
accountsOutputFileJson
|
||||
);
|
||||
// usersOutputFile
|
||||
const usersOutputFileJson = JSON.stringify(usersOutputFile);
|
||||
fs.writeFileSync(`${outputFilePath}_users.json`, usersOutputFileJson);
|
||||
// licensesOutputFile
|
||||
const licensesOutputFileJson = JSON.stringify(licensesOutputFile);
|
||||
fs.writeFileSync(
|
||||
`${outputFilePath}_licenses.json`,
|
||||
licensesOutputFileJson
|
||||
);
|
||||
// worktypesOutputFile
|
||||
const worktypesOutputFileJson = JSON.stringify(worktypesOutputFile);
|
||||
fs.writeFileSync(
|
||||
`${outputFilePath}_worktypes.json`,
|
||||
worktypesOutputFileJson
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.outputJsonFile.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* データのバリデーションチェック
|
||||
* @param csvInputFile: csvInputFile[]
|
||||
*/
|
||||
async validateInputData(
|
||||
context: Context,
|
||||
csvInputFile: csvInputFile[]
|
||||
): Promise<void> {
|
||||
// パラメータ内容が長大なのでログには出さない
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.validateInputData.name}`
|
||||
);
|
||||
|
||||
try {
|
||||
// csvInputFileのバリデーションチェックを行う
|
||||
csvInputFile.forEach((line, index) => {
|
||||
// typeのバリデーションチェック
|
||||
if (
|
||||
line.type !== MIGRATION_TYPE.ADMINISTRATOR &&
|
||||
line.type !== MIGRATION_TYPE.BC &&
|
||||
line.type !== MIGRATION_TYPE.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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import {
|
||||
AccountsOutputFileStep1,
|
||||
LicensesOutputFile,
|
||||
UsersOutputFile,
|
||||
WorktypesOutputFile,
|
||||
} from "src/common/types/types";
|
||||
|
||||
export class transferRequest {
|
||||
@ApiProperty()
|
||||
inputFilePath: string;
|
||||
}
|
||||
|
||||
export class transferResponse {}
|
||||
|
||||
export class registInputDataResponse {
|
||||
@ApiProperty()
|
||||
accountsOutputFileStep1Lines: AccountsOutputFileStep1[];
|
||||
@ApiProperty()
|
||||
usersOutputFileLines: UsersOutputFile[];
|
||||
@ApiProperty()
|
||||
licensesOutputFileLines: LicensesOutputFile[];
|
||||
@ApiProperty()
|
||||
worktypesOutputFileLines: WorktypesOutputFile[];
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { Controller, Logger } from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { UsersService } from "./users.service";
|
||||
|
||||
@ApiTags("users")
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
private readonly logger = new Logger(UsersController.name);
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AdB2cModule } from "../../gateways/adb2c/adb2c.module";
|
||||
import { UsersRepositoryModule } from "../../repositories/users/users.repository.module";
|
||||
import { UsersController } from "./users.controller";
|
||||
import { UsersService } from "./users.service";
|
||||
|
||||
@Module({
|
||||
imports: [UsersRepositoryModule, AdB2cModule],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
306
data_migration_tools/server/src/features/users/users.service.ts
Normal file
306
data_migration_tools/server/src/features/users/users.service.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
||||
import { makeErrorResponse } from "../../common/error/makeErrorResponse";
|
||||
import { makePassword } from "../../common/password/password";
|
||||
import {
|
||||
AdB2cService,
|
||||
ConflictError,
|
||||
isConflictError,
|
||||
} from "../../gateways/adb2c/adb2c.service";
|
||||
import {
|
||||
User as EntityUser,
|
||||
newUser,
|
||||
} from "../../repositories/users/entity/user.entity";
|
||||
import { UsersRepositoryService } from "../../repositories/users/users.repository.service";
|
||||
import { MANUAL_RECOVERY_REQUIRED, USER_ROLES } from "../../constants";
|
||||
import { Context } from "../../common/log";
|
||||
import { UserRoles } from "../../common/types/role";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private readonly logger = new Logger(UsersService.name);
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly adB2cService: AdB2cService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates user
|
||||
* @param context
|
||||
* @param name
|
||||
* @param role
|
||||
* @param email
|
||||
* @param autoRenew
|
||||
* @param notification
|
||||
* @param accountId
|
||||
* @param userid
|
||||
* @param [authorId]
|
||||
* @param [encryption]
|
||||
* @param [encryptionPassword]
|
||||
* @param [prompt]
|
||||
* @returns user
|
||||
*/
|
||||
async createUser(
|
||||
context: Context,
|
||||
name: string,
|
||||
role: UserRoles,
|
||||
email: string,
|
||||
autoRenew: boolean,
|
||||
notification: boolean,
|
||||
accountId: number,
|
||||
userid: number,
|
||||
authorId?: string | undefined,
|
||||
encryption?: boolean | undefined,
|
||||
encryptionPassword?: string | undefined,
|
||||
prompt?: boolean | undefined
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.createUser.name} | params: { ` +
|
||||
`role: ${role}, ` +
|
||||
`autoRenew: ${autoRenew}, ` +
|
||||
`notification: ${notification}, ` +
|
||||
`accountId: ${accountId}, ` +
|
||||
`userid: ${userid}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`encryption: ${encryption}, ` +
|
||||
`prompt: ${prompt} };`
|
||||
);
|
||||
|
||||
//authorIdが重複していないかチェックする
|
||||
if (authorId) {
|
||||
let isAuthorIdDuplicated = false;
|
||||
try {
|
||||
isAuthorIdDuplicated = await this.usersRepository.existsAuthorId(
|
||||
context,
|
||||
accountId,
|
||||
authorId
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
if (isAuthorIdDuplicated) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010302"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ランダムなパスワードを生成する
|
||||
const ramdomPassword = makePassword();
|
||||
|
||||
//Azure AD B2Cにユーザーを新規登録する
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
try {
|
||||
this.logger.log(`name=${name}`);
|
||||
// idpにユーザーを作成
|
||||
externalUser = await this.adB2cService.createUser(
|
||||
context,
|
||||
email,
|
||||
ramdomPassword,
|
||||
name
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] create externalUser failed`
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// メールアドレス重複エラー
|
||||
if (isConflictError(externalUser)) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010301"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
//Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する
|
||||
let newUser: EntityUser;
|
||||
|
||||
try {
|
||||
//roleに応じてユーザー情報を作成する
|
||||
const newUserInfo = this.createNewUserInfo(
|
||||
context,
|
||||
userid,
|
||||
role,
|
||||
accountId,
|
||||
externalUser.sub,
|
||||
autoRenew,
|
||||
notification,
|
||||
authorId,
|
||||
encryption,
|
||||
encryptionPassword,
|
||||
prompt
|
||||
);
|
||||
// ユーザ作成
|
||||
newUser = await this.usersRepository.createNormalUser(
|
||||
context,
|
||||
newUserInfo
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(`[${context.getTrackingId()}]create user failed`);
|
||||
//リカバリー処理
|
||||
//Azure AD B2Cに登録したユーザー情報を削除する
|
||||
await this.deleteB2cUser(externalUser.sub, context);
|
||||
|
||||
switch (e.code) {
|
||||
case "ER_DUP_ENTRY":
|
||||
//AuthorID重複エラー
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E010302"),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createUser.name}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Azure AD B2Cに登録したユーザー情報を削除する
|
||||
// TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
|
||||
private async deleteB2cUser(externalUserId: string, context: Context) {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteB2cUser.name
|
||||
} | params: { externalUserId: ${externalUserId} }`
|
||||
);
|
||||
try {
|
||||
await this.adB2cService.deleteUser(externalUserId, context);
|
||||
this.logger.log(
|
||||
`[${context.getTrackingId()}] delete externalUser: ${externalUserId}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteB2cUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DBに登録したユーザー情報を削除する
|
||||
private async deleteUser(userId: number, context: Context) {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.deleteUser.name
|
||||
} | params: { userId: ${userId} }`
|
||||
);
|
||||
try {
|
||||
await this.usersRepository.deleteNormalUser(context, userId);
|
||||
this.logger.log(`[${context.getTrackingId()}] delete user: ${userId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete user: ${userId}`
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// roleを受け取って、roleに応じたnewUserを作成して返却する
|
||||
private createNewUserInfo(
|
||||
context: Context,
|
||||
id: number,
|
||||
role: UserRoles,
|
||||
accountId: number,
|
||||
externalId: string,
|
||||
autoRenew: boolean,
|
||||
notification: boolean,
|
||||
authorId?: string | undefined,
|
||||
encryption?: boolean | undefined,
|
||||
encryptionPassword?: string | undefined,
|
||||
prompt?: boolean | undefined
|
||||
): newUser {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.createNewUserInfo.name
|
||||
} | params: { ` +
|
||||
`id: ${id}, ` +
|
||||
`role: ${role}, ` +
|
||||
`accountId: ${accountId}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`autoRenew: ${autoRenew}, ` +
|
||||
`notification: ${notification}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`encryption: ${encryption}, ` +
|
||||
`prompt: ${prompt} };`
|
||||
);
|
||||
try {
|
||||
switch (role) {
|
||||
case USER_ROLES.NONE:
|
||||
case USER_ROLES.TYPIST:
|
||||
return {
|
||||
id,
|
||||
account_id: accountId,
|
||||
external_id: externalId,
|
||||
auto_renew: autoRenew,
|
||||
notification,
|
||||
role,
|
||||
accepted_dpa_version: null,
|
||||
accepted_eula_version: null,
|
||||
accepted_privacy_notice_version: null,
|
||||
encryption: false,
|
||||
encryption_password: null,
|
||||
prompt: false,
|
||||
author_id: null,
|
||||
};
|
||||
case USER_ROLES.AUTHOR:
|
||||
return {
|
||||
id,
|
||||
account_id: accountId,
|
||||
external_id: externalId,
|
||||
auto_renew: autoRenew,
|
||||
notification,
|
||||
role,
|
||||
author_id: authorId ?? null,
|
||||
encryption: encryption ?? false,
|
||||
encryption_password: encryptionPassword ?? null,
|
||||
prompt: prompt ?? false,
|
||||
accepted_dpa_version: null,
|
||||
accepted_eula_version: null,
|
||||
accepted_privacy_notice_version: null,
|
||||
};
|
||||
default:
|
||||
//不正なroleが指定された場合はログを出力してエラーを返す
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] [NOT IMPLEMENT] [RECOVER] role: ${role}`
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse("E009999"),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
return e;
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.createNewUserInfo.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
218
data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts
Normal file
218
data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user