Merged PR 786: Azure Functions実装(一括登録)
## 概要 [Task3756: Azure Functions実装(一括登録)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3756) - ユーザー一括登録用のAzure Functionを実装しました。 ## レビューポイント - 処理の流れがラフスケッチと認識通りでしょうか? - JSONファイルの内容はイメージ通りでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 - テスト実行 実際の詳細な動作についてはdevelop環境で確認します。
This commit is contained in:
parent
31de71f743
commit
d6a47932e7
2
dictation_function/codegen.sh
Normal file
2
dictation_function/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/dictation_function/src/api/odms/openapi.json -o /app/dictation_function/src/api/
|
||||
7
dictation_function/openapitools.json
Normal file
7
dictation_function/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"
|
||||
}
|
||||
}
|
||||
1118
dictation_function/package-lock.json
generated
1118
dictation_function/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,11 +9,13 @@
|
||||
"clean": "rimraf dist",
|
||||
"prestart": "npm run clean && npm run build",
|
||||
"start": "func start",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"codegen": "sh codegen.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/functions": "^4.0.0",
|
||||
"@azure/identity": "^3.1.3",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.5",
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"dotenv": "^16.0.3",
|
||||
@ -22,11 +24,13 @@
|
||||
"typeorm": "^0.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openapitools/openapi-generator-cli": "^2.9.0",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/node": "18.x",
|
||||
"@types/redis": "^2.8.13",
|
||||
"@types/redis-mock": "^0.17.3",
|
||||
"azure-functions-core-tools": "^4.x",
|
||||
"base64url": "^3.0.1",
|
||||
"jest": "^28.0.3",
|
||||
"redis-mock": "^0.56.3",
|
||||
"rimraf": "^5.0.0",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export type AdB2cResponse = {
|
||||
'@odata.context': string;
|
||||
"@odata.context": string;
|
||||
value: AdB2cUser[];
|
||||
};
|
||||
export type AdB2cUser = {
|
||||
|
||||
4
dictation_function/src/api/.gitignore
vendored
Normal file
4
dictation_function/src/api/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
wwwroot/*.js
|
||||
node_modules
|
||||
typings
|
||||
dist
|
||||
1
dictation_function/src/api/.npmignore
Normal file
1
dictation_function/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
|
||||
23
dictation_function/src/api/.openapi-generator-ignore
Normal file
23
dictation_function/src/api/.openapi-generator-ignore
Normal file
@ -0,0 +1,23 @@
|
||||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
||||
8
dictation_function/src/api/.openapi-generator/FILES
Normal file
8
dictation_function/src/api/.openapi-generator/FILES
Normal file
@ -0,0 +1,8 @@
|
||||
.gitignore
|
||||
.npmignore
|
||||
api.ts
|
||||
base.ts
|
||||
common.ts
|
||||
configuration.ts
|
||||
git_push.sh
|
||||
index.ts
|
||||
1
dictation_function/src/api/.openapi-generator/VERSION
Normal file
1
dictation_function/src/api/.openapi-generator/VERSION
Normal file
@ -0,0 +1 @@
|
||||
7.1.0
|
||||
8686
dictation_function/src/api/api.ts
Normal file
8686
dictation_function/src/api/api.ts
Normal file
File diff suppressed because it is too large
Load Diff
86
dictation_function/src/api/base.ts
Normal file
86
dictation_function/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
dictation_function/src/api/common.ts
Normal file
150
dictation_function/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
dictation_function/src/api/configuration.ts
Normal file
110
dictation_function/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
dictation_function/src/api/git_push.sh
Normal file
57
dictation_function/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
dictation_function/src/api/index.ts
Normal file
18
dictation_function/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";
|
||||
|
||||
5357
dictation_function/src/api/odms/openapi.json
Normal file
5357
dictation_function/src/api/odms/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
184
dictation_function/src/blobstorage/blobstorage.service.ts
Normal file
184
dictation_function/src/blobstorage/blobstorage.service.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import {
|
||||
BlobServiceClient,
|
||||
StorageSharedKeyCredential,
|
||||
} from "@azure/storage-blob";
|
||||
import { IMPORT_USERS_CONTAINER_NAME } from "../constants";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
|
||||
export class BlobstorageService {
|
||||
private readonly blobServiceClient: BlobServiceClient;
|
||||
private readonly sharedKeyCredential: StorageSharedKeyCredential;
|
||||
constructor() {
|
||||
if (
|
||||
!process.env.STORAGE_ACCOUNT_NAME_IMPORT ||
|
||||
!process.env.STORAGE_ACCOUNT_KEY_IMPORT ||
|
||||
!process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT
|
||||
) {
|
||||
throw new Error("Storage account information is missing");
|
||||
}
|
||||
|
||||
this.sharedKeyCredential = new StorageSharedKeyCredential(
|
||||
process.env.STORAGE_ACCOUNT_NAME_IMPORT,
|
||||
process.env.STORAGE_ACCOUNT_KEY_IMPORT
|
||||
);
|
||||
this.blobServiceClient = new BlobServiceClient(
|
||||
process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT,
|
||||
this.sharedKeyCredential
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Lists blobs
|
||||
* @returns blobs
|
||||
*/
|
||||
async listBlobs(context: InvocationContext): Promise<string[]> {
|
||||
context.log(`[IN] ${this.listBlobs.name}`);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
|
||||
const blobNames: string[] = [];
|
||||
// stage.json以外のファイルを取得
|
||||
for await (const blob of containerClient.listBlobsFlat({
|
||||
prefix: "U_",
|
||||
})) {
|
||||
blobNames.push(blob.name);
|
||||
}
|
||||
|
||||
return blobNames;
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.listBlobs.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a blob
|
||||
* @param context
|
||||
* @param filename
|
||||
* @returns file buffer
|
||||
*/
|
||||
public async downloadFileData(
|
||||
context: InvocationContext,
|
||||
filename: string
|
||||
): Promise<string | undefined> {
|
||||
context.log(
|
||||
`[IN] ${this.downloadFileData.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
const blobClient = containerClient.getBlobClient(filename);
|
||||
try {
|
||||
const downloadBlockBlobResponse = await blobClient.downloadToBuffer();
|
||||
return downloadBlockBlobResponse.toString();
|
||||
} catch (error) {
|
||||
// ファイルが存在しない場合はundefinedを返す
|
||||
if (error?.statusCode === 404) {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.downloadFileData.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates file
|
||||
* @param context
|
||||
* @param filename
|
||||
* @param data
|
||||
* @returns file
|
||||
*/
|
||||
public async updateFile(
|
||||
context: InvocationContext,
|
||||
filename: string,
|
||||
data: string
|
||||
): Promise<boolean> {
|
||||
context.log(
|
||||
`[IN] ${this.updateFile.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
|
||||
const { response } = await containerClient.uploadBlockBlob(
|
||||
filename,
|
||||
data,
|
||||
data.length
|
||||
);
|
||||
if (response.errorCode) {
|
||||
context.log(`update failed. response errorCode: ${response.errorCode}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.updateFile.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes file
|
||||
* @param context
|
||||
* @param filename
|
||||
* @returns file
|
||||
*/
|
||||
public async deleteFile(
|
||||
context: InvocationContext,
|
||||
filename: string
|
||||
): Promise<void> {
|
||||
context.log(
|
||||
`[IN] ${this.deleteFile.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
const blobClient = containerClient.getBlobClient(filename);
|
||||
await blobClient.deleteIfExists();
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.deleteFile.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether file exists is
|
||||
* @param context
|
||||
* @param filename
|
||||
* @returns file exists
|
||||
*/
|
||||
public async isFileExists(
|
||||
context: InvocationContext,
|
||||
filename: string
|
||||
): Promise<boolean> {
|
||||
context.log(
|
||||
`[IN] ${this.isFileExists.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
const blobClient = containerClient.getBlobClient(filename);
|
||||
return await blobClient.exists();
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.isFileExists.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
155
dictation_function/src/blobstorage/types/guards.ts
Normal file
155
dictation_function/src/blobstorage/types/guards.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { IMPORT_USERS_STAGES } from "../../constants";
|
||||
import { ErrorRow, ImportData, ImportJson, StageJson } from "./types";
|
||||
|
||||
const isErrorRow = (obj: any): obj is ErrorRow => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const errorRow = obj as ErrorRow;
|
||||
if (errorRow.name === undefined || typeof errorRow.name !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (errorRow.row === undefined || typeof errorRow.row !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (errorRow.error === undefined || typeof errorRow.error !== "string") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isStageJson = (obj: any): obj is StageJson => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const stageJson = obj as StageJson;
|
||||
if (
|
||||
stageJson.filename !== undefined &&
|
||||
typeof stageJson.filename !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (stageJson.update === undefined || typeof stageJson.update !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (stageJson.row !== undefined && typeof stageJson.row !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
stageJson.errors !== undefined &&
|
||||
(!Array.isArray(stageJson.errors) ||
|
||||
!stageJson.errors.every((x) => isErrorRow(x)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
stageJson.state === undefined ||
|
||||
!Object.values(IMPORT_USERS_STAGES).includes(stageJson.state)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isImportData = (obj: any): obj is ImportData => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const importData = obj as ImportData;
|
||||
if (importData.name === undefined || typeof importData.name !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (importData.email === undefined || typeof importData.email !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (importData.role === undefined || typeof importData.role !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.author_id !== undefined &&
|
||||
typeof importData.author_id !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.auto_renew === undefined ||
|
||||
typeof importData.auto_renew !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.notification === undefined ||
|
||||
typeof importData.notification !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.encryption !== undefined &&
|
||||
typeof importData.encryption !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.encryption_password !== undefined &&
|
||||
typeof importData.encryption_password !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.prompt !== undefined &&
|
||||
typeof importData.prompt !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isImportJson = (obj: any): obj is ImportJson => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const importJson = obj as ImportJson;
|
||||
if (
|
||||
importJson.account_id === undefined ||
|
||||
typeof importJson.account_id !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.user_id === undefined ||
|
||||
typeof importJson.user_id !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.user_role === undefined ||
|
||||
typeof importJson.user_role !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.external_id === undefined ||
|
||||
typeof importJson.external_id !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.delegation_account_id !== undefined &&
|
||||
typeof importJson.delegation_account_id !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.delegation_user_id !== undefined &&
|
||||
typeof importJson.delegation_user_id !== "number"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.file_name === undefined ||
|
||||
typeof importJson.file_name !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.data === undefined ||
|
||||
!Array.isArray(importJson.data) ||
|
||||
!importJson.data.every((x) => isImportData(x))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
56
dictation_function/src/blobstorage/types/types.ts
Normal file
56
dictation_function/src/blobstorage/types/types.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { IMPORT_USERS_STAGES, USER_ROLES } from "../../constants";
|
||||
|
||||
export type StageType =
|
||||
(typeof IMPORT_USERS_STAGES)[keyof typeof IMPORT_USERS_STAGES];
|
||||
|
||||
export type StageJson = {
|
||||
filename?: string | undefined;
|
||||
update: number;
|
||||
row?: number | undefined;
|
||||
errors?: ErrorRow[] | undefined;
|
||||
state: StageType;
|
||||
};
|
||||
|
||||
export type ErrorRow = {
|
||||
name: string;
|
||||
row: number;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type ImportJson = {
|
||||
account_id: number;
|
||||
user_id: number;
|
||||
user_role: RoleType;
|
||||
external_id: string;
|
||||
delegation_account_id?: number | undefined;
|
||||
delegation_user_id?: number | undefined;
|
||||
file_name: string;
|
||||
date: number;
|
||||
data: ImportData[];
|
||||
};
|
||||
|
||||
export type ImportData = {
|
||||
name: string;
|
||||
email: string;
|
||||
role: number;
|
||||
author_id?: string | undefined;
|
||||
auto_renew: number;
|
||||
notification: number;
|
||||
encryption?: number | undefined;
|
||||
encryption_password?: string | undefined;
|
||||
prompt?: number | undefined;
|
||||
};
|
||||
|
||||
export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||
|
||||
export type User = {
|
||||
name: string;
|
||||
role: RoleType;
|
||||
email: string;
|
||||
autoRenew: boolean;
|
||||
notification: boolean;
|
||||
authorId?: string | undefined;
|
||||
encryption?: boolean | undefined;
|
||||
encryptionPassword?: string | undefined;
|
||||
prompt?: boolean | undefined;
|
||||
};
|
||||
79
dictation_function/src/common/errors/code.ts
Normal file
79
dictation_function/src/common/errors/code.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
エラーコード作成方針
|
||||
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へのリクエスト上限超過エラー
|
||||
"E010001", // パラメータ形式不正エラー
|
||||
"E010201", // 未認証ユーザエラー
|
||||
"E010202", // 認証済ユーザエラー
|
||||
"E010203", // 管理ユーザ権限エラー
|
||||
"E010204", // ユーザ不在エラー
|
||||
"E010205", // DBのRoleが想定外の値エラー
|
||||
"E010206", // DBのTierが想定外の値エラー
|
||||
"E010207", // ユーザーのRole変更不可エラー
|
||||
"E010208", // ユーザーの暗号化パスワード不足エラー
|
||||
"E010209", // ユーザーの同意済み利用規約バージョンが最新でないエラー
|
||||
"E010301", // メールアドレス登録済みエラー
|
||||
"E010302", // authorId重複エラー
|
||||
"E010401", // PONumber重複エラー
|
||||
"E010501", // アカウント不在エラー
|
||||
"E010502", // アカウント情報変更不可エラー
|
||||
"E010503", // 代行操作不許可エラー
|
||||
"E010601", // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||
"E010602", // タスク変更権限不足エラー
|
||||
"E010603", // タスク不在エラー
|
||||
"E010701", // Blobファイル不在エラー
|
||||
"E010801", // ライセンス不在エラー
|
||||
"E010802", // ライセンス取り込み済みエラー
|
||||
"E010803", // ライセンス発行済みエラー
|
||||
"E010804", // ライセンス数不足エラー
|
||||
"E010805", // ライセンス有効期限切れエラー
|
||||
"E010806", // ライセンス割り当て不可エラー
|
||||
"E010807", // ライセンス割り当て解除不可エラー
|
||||
"E010808", // ライセンス注文キャンセル不可エラー
|
||||
"E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
|
||||
"E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
|
||||
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
|
||||
"E010908", // タイピストグループ不在エラー
|
||||
"E010909", // タイピストグループ名重複エラー
|
||||
"E011001", // ワークタイプ重複エラー
|
||||
"E011002", // ワークタイプ登録上限超過エラー
|
||||
"E011003", // ワークタイプ不在エラー
|
||||
"E011004", // ワークタイプ使用中エラー
|
||||
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
"E013002", // ワークフロー不在エラー
|
||||
"E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった)
|
||||
"E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)
|
||||
"E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた)
|
||||
"E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた)
|
||||
"E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた)
|
||||
"E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている)
|
||||
"E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた)
|
||||
"E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている)
|
||||
"E015001", // タイピストグループ削除済みエラー
|
||||
"E015002", // タイピストグループがワークフローに紐づいているエラー
|
||||
"E015003", // タイピストグループがルーティングされているエラー
|
||||
"E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった)
|
||||
"E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた)
|
||||
"E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた)
|
||||
] as const;
|
||||
3
dictation_function/src/common/errors/index.ts
Normal file
3
dictation_function/src/common/errors/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./code";
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
9
dictation_function/src/common/errors/types.ts
Normal file
9
dictation_function/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
dictation_function/src/common/errors/utils.ts
Normal file
101
dictation_function/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;
|
||||
};
|
||||
3
dictation_function/src/common/jwt/index.ts
Normal file
3
dictation_function/src/common/jwt/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { isVerifyError, sign, verify, decode, getJwtKey } from "./jwt";
|
||||
|
||||
export { isVerifyError, sign, verify, decode, getJwtKey };
|
||||
250
dictation_function/src/common/jwt/jwt.spec.ts
Normal file
250
dictation_function/src/common/jwt/jwt.spec.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import { sign, verify, isVerifyError } from "./jwt";
|
||||
import base64url from "base64url";
|
||||
|
||||
test("success sign and verify", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const payload = verify<{ value: "testvalue" }>(token, publicKey);
|
||||
if (isVerifyError(payload)) {
|
||||
throw new Error(`${payload.reason} | ${payload.message}`);
|
||||
}
|
||||
|
||||
expect(payload.value).toBe("testvalue");
|
||||
});
|
||||
|
||||
test("failed sign and verify (jwt expired)", () => {
|
||||
// 有効期限を0秒にすることで、検証を行った時点で有効期限切れにする
|
||||
const token = sign({ value: "testvalue" }, 0, privateKey);
|
||||
const payload = verify<{ value: "testvalue" }>(token, publicKey);
|
||||
if (!isVerifyError(payload)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(payload.reason).toBe("ExpiredError");
|
||||
});
|
||||
|
||||
test("failed sign and verify (invalid key pair)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
// 秘密鍵と対ではない公開鍵を使用して検証する
|
||||
const payload = verify<{ value: "testvalue" }>(token, anotherPublicKey);
|
||||
if (!isVerifyError(payload)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(payload.reason).toBe("InvalidToken");
|
||||
expect(payload.message).toBe("invalid signature");
|
||||
});
|
||||
|
||||
test("failed sign and verify (invalid public key)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
// 公開鍵の形式になっていない文字列を使用して検証する
|
||||
const payload = verify<{ value: "testvalue" }>(token, fakePublicKey);
|
||||
if (!isVerifyError(payload)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(payload.reason).toBe("InvalidToken");
|
||||
expect(payload.message).toBe(
|
||||
"secretOrPublicKey must be an asymmetric key when using RS256"
|
||||
);
|
||||
});
|
||||
|
||||
test("failed sign (invalid private key)", () => {
|
||||
expect(() => {
|
||||
// 不正な秘密鍵で署名しようとする場合はエラーがthrowされる
|
||||
sign({ value: "testvalue" }, 5 * 60, fakePrivateKey);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test("success rewrite-token verify (as is)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const { header, payload, verifySignature } = splitToken(token);
|
||||
|
||||
{
|
||||
// 何も操作せずに構築しなおした場合、成功する
|
||||
const validToken = rebuildToken(header, payload, verifySignature);
|
||||
|
||||
const value = verify<{ value: string }>(validToken, publicKey);
|
||||
if (isVerifyError(value)) {
|
||||
throw new Error(`${value.reason} | ${value.message}`);
|
||||
}
|
||||
|
||||
expect(value.value).toBe("testvalue");
|
||||
}
|
||||
});
|
||||
|
||||
test("failed rewrite-token verify (override algorithm)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const { payload, verifySignature } = splitToken(token);
|
||||
|
||||
{
|
||||
// 検証アルゴリズムを「検証なし」に書き換える
|
||||
const headerObject = { alg: "none" };
|
||||
const payloadObject = JSON.parse(payload) as {
|
||||
value: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
// 内容を操作して構築しなおした場合、失敗する
|
||||
const customToken = rebuildToken(
|
||||
JSON.stringify(headerObject),
|
||||
JSON.stringify(payloadObject),
|
||||
verifySignature
|
||||
);
|
||||
|
||||
const value = verify<{ value: string }>(customToken, publicKey);
|
||||
if (!isVerifyError(value)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(value.reason).toBe("InvalidToken");
|
||||
expect(value.message).toBe("invalid algorithm");
|
||||
}
|
||||
});
|
||||
|
||||
test("failed rewrite-token verify (override expire)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const { header, payload, verifySignature } = splitToken(token);
|
||||
|
||||
{
|
||||
// expの値を操作する
|
||||
const payloadObject = JSON.parse(payload) as {
|
||||
value: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
payloadObject.exp = payloadObject.exp + 100000;
|
||||
|
||||
// 内容を操作して構築しなおした場合、失敗する
|
||||
const customToken = rebuildToken(
|
||||
header,
|
||||
JSON.stringify(payloadObject),
|
||||
verifySignature
|
||||
);
|
||||
|
||||
const value = verify<{ value: string }>(customToken, publicKey);
|
||||
if (!isVerifyError(value)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(value.reason).toBe("InvalidToken");
|
||||
expect(value.message).toBe("invalid signature");
|
||||
}
|
||||
});
|
||||
|
||||
// JWT改竄テスト用ユーティリティ
|
||||
const splitToken = (
|
||||
token: string
|
||||
): { header: string; payload: string; verifySignature: string } => {
|
||||
const splited = token.split(".");
|
||||
|
||||
const header = base64url.decode(splited[0]);
|
||||
const payload = base64url.decode(splited[1]);
|
||||
const verifySignature = splited[2];
|
||||
return { header, payload, verifySignature };
|
||||
};
|
||||
|
||||
// JWT改竄テスト用ユーティリティ
|
||||
const rebuildToken = (
|
||||
header: string,
|
||||
payload: string,
|
||||
verifySignature: string
|
||||
): string => {
|
||||
const rebuild_header = base64url.encode(header);
|
||||
const rebuild_payload = base64url.encode(payload);
|
||||
return `${rebuild_header}.${rebuild_payload}.${verifySignature}`;
|
||||
};
|
||||
|
||||
// テスト用に生成した秘密鍵
|
||||
const privateKey = [
|
||||
"-----BEGIN RSA PRIVATE KEY-----",
|
||||
"MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F",
|
||||
"GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a",
|
||||
"fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa",
|
||||
"FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq",
|
||||
"zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF",
|
||||
"+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN",
|
||||
"LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq",
|
||||
"L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0",
|
||||
"WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE",
|
||||
"x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm",
|
||||
"+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX",
|
||||
"cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc",
|
||||
"BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H",
|
||||
"IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+",
|
||||
"y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb",
|
||||
"iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9",
|
||||
"BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95",
|
||||
"mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/",
|
||||
"O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s",
|
||||
"xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust",
|
||||
"nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT",
|
||||
"tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK",
|
||||
"YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM",
|
||||
"iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk",
|
||||
"JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==",
|
||||
"-----END RSA PRIVATE KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// テスト用に生成した公開鍵
|
||||
const publicKey = [
|
||||
"-----BEGIN PUBLIC KEY-----",
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTVLNpW0/FzVCU7qo1DD",
|
||||
"jOkYWx6s/jE56YOOc3UzaaG/zb1FGyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23",
|
||||
"tL7p3PS0ZCsLeggcyLctbJAzLy/afF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc5",
|
||||
"7Tli2x8NiOZr5dafs3vMuIIKNsBaFAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxD",
|
||||
"alw5DCd0mmdY0vrbRsgkbej0ZzzqzukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn2",
|
||||
"1AIcn8P3rKZmDyPH+9KNfWC8+ubF+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3a",
|
||||
"IQIDAQAB",
|
||||
"-----END PUBLIC KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// テスト用に作成した、違う秘密鍵から生成した公開鍵
|
||||
const anotherPublicKey = [
|
||||
"-----BEGIN PUBLIC KEY-----",
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3",
|
||||
"2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig",
|
||||
"n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9",
|
||||
"RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk",
|
||||
"EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x",
|
||||
"2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81",
|
||||
"KQIDAQAB",
|
||||
"-----END PUBLIC KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// 秘密鍵のように見えるが想定する形式と違う
|
||||
const fakePrivateKey = [
|
||||
"-----BAGIN RSA PRIVATE KEY-----",
|
||||
"MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F",
|
||||
"GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a",
|
||||
"fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa",
|
||||
"FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq",
|
||||
"zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF",
|
||||
"+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN",
|
||||
"LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq",
|
||||
"L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0",
|
||||
"WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE",
|
||||
"x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm",
|
||||
"+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX",
|
||||
"cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc",
|
||||
"BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H",
|
||||
"IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+",
|
||||
"y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb",
|
||||
"iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9",
|
||||
"BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95",
|
||||
"mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/",
|
||||
"O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s",
|
||||
"xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust",
|
||||
"nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT",
|
||||
"tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK",
|
||||
"YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM",
|
||||
"iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk",
|
||||
"JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==",
|
||||
"-----END RSA PRIVATE KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// 公開鍵のように見えるが想定する形式と違う
|
||||
const fakePublicKey = [
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3",
|
||||
"2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig",
|
||||
"n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9",
|
||||
"RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk",
|
||||
"EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x",
|
||||
"2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81",
|
||||
"KQIDAQAB",
|
||||
].join("\n");
|
||||
130
dictation_function/src/common/jwt/jwt.ts
Normal file
130
dictation_function/src/common/jwt/jwt.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import * as jwt from "jsonwebtoken";
|
||||
// XXX: decodeがうまく使えないことがあるので応急対応 バージョン9以降だとなる?
|
||||
import { decode as jwtDecode } from "jsonwebtoken";
|
||||
|
||||
export type VerifyError = {
|
||||
reason: "ExpiredError" | "InvalidToken" | "InvalidTimeStamp" | "Unknown";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const isVerifyError = (arg: unknown): arg is VerifyError => {
|
||||
const value = arg as VerifyError;
|
||||
if (value.message === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.reason === undefined) {
|
||||
return false;
|
||||
}
|
||||
switch (value.reason) {
|
||||
case "ExpiredError":
|
||||
case "InvalidTimeStamp":
|
||||
case "InvalidToken":
|
||||
case "Unknown":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Payloadと秘密鍵を使用して署名されたJWTを生成します
|
||||
* @param {T} payload payloadの型
|
||||
* @param {number} expirationSeconds トークンの有効期限(秒)
|
||||
* @param {string} privateKey 署名に使用する秘密鍵
|
||||
* @return {string} 署名済みトークン
|
||||
* @throws {Error} 秘密鍵の形式が間違っている等の理由が格納されたErrorオブジェクト
|
||||
*/
|
||||
export const sign = <T extends object>(
|
||||
payload: T,
|
||||
expirationSeconds: number,
|
||||
privateKey: string
|
||||
): string => {
|
||||
try {
|
||||
const token = jwt.sign(payload, privateKey, {
|
||||
expiresIn: expirationSeconds,
|
||||
algorithm: "RS256",
|
||||
});
|
||||
return token;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* tokenと公開鍵を使用して検証済みJWTのpayloadを取得します
|
||||
* @param {string} token JWT
|
||||
* @param {string} publicKey 検証に使用する公開鍵
|
||||
* @return {T | VerifyError} Payload または 検証エラーの内容を表すオブジェクト
|
||||
*/
|
||||
export const verify = <T extends object>(
|
||||
token: string,
|
||||
publicKey: string
|
||||
): T | VerifyError => {
|
||||
try {
|
||||
const payload = jwt.verify(token, publicKey, {
|
||||
algorithms: ["RS256"],
|
||||
}) as T;
|
||||
return payload;
|
||||
} catch (e) {
|
||||
if (e instanceof jwt.TokenExpiredError) {
|
||||
return {
|
||||
reason: "ExpiredError",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.NotBeforeError) {
|
||||
return {
|
||||
reason: "InvalidTimeStamp",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.JsonWebTokenError) {
|
||||
return {
|
||||
reason: "InvalidToken",
|
||||
message: e.message,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
reason: "Unknown",
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* tokenから未検証のJWTのpayloadを取得します
|
||||
* @param {string} token JWT
|
||||
* @return {T | VerifyError} Payload または デコードエラーの内容を表すオブジェクト
|
||||
*/
|
||||
export const decode = <T extends object>(token: string): T | VerifyError => {
|
||||
try {
|
||||
const payload = jwtDecode(token, {
|
||||
json: true,
|
||||
}) as T;
|
||||
return payload;
|
||||
} catch (e) {
|
||||
if (e instanceof jwt.TokenExpiredError) {
|
||||
return {
|
||||
reason: "ExpiredError",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.NotBeforeError) {
|
||||
return {
|
||||
reason: "InvalidTimeStamp",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.JsonWebTokenError) {
|
||||
return {
|
||||
reason: "InvalidToken",
|
||||
message: e.message,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
reason: "Unknown",
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getJwtKey = (key: string): string => key.replace(/\\n/g, "\n");
|
||||
32
dictation_function/src/common/jwt/types.ts
Normal file
32
dictation_function/src/common/jwt/types.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export type AccessToken = {
|
||||
/**
|
||||
* 外部認証サービスの識別子(代行者)
|
||||
*/
|
||||
delegateUserId?: string | undefined;
|
||||
/**
|
||||
* 外部認証サービスの識別子
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* 半角スペース区切りのRoleを表現する文字列(ex. "author admin")
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* アカウントの階層情報(1~5までの半角数字)
|
||||
*/
|
||||
tier: number;
|
||||
};
|
||||
|
||||
// システムの内部で発行し、外部に公開しないトークン
|
||||
// システム間通信用(例: Azure Functions→AppService)に使用する
|
||||
export type SystemAccessToken = {
|
||||
/**
|
||||
* トークンの発行者名(ログ記録用)
|
||||
*/
|
||||
systemName: string;
|
||||
|
||||
/**
|
||||
* 付加情報を 文字情報として格納できる
|
||||
*/
|
||||
context?: string;
|
||||
};
|
||||
@ -293,3 +293,45 @@ export const HTTP_STATUS_CODES = {
|
||||
BAD_REQUEST: 400,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
};
|
||||
|
||||
/**
|
||||
* ユーザー一括登録用のBlobコンテナ名
|
||||
* @const {string}
|
||||
*/
|
||||
export const IMPORT_USERS_CONTAINER_NAME = "import-users";
|
||||
|
||||
/**
|
||||
* ユーザー一括登録の最大処理時間(分)
|
||||
* @const {number}
|
||||
*/
|
||||
export const IMPORT_USERS_MAX_DURATION_MINUTES = 30;
|
||||
|
||||
/**
|
||||
* ユーザー一括登録のステージ管理ファイル名
|
||||
* @const {string}
|
||||
*/
|
||||
export const IMPORT_USERS_STAGE_FILE_NAME = "stage.json";
|
||||
|
||||
/**
|
||||
* ユーザー一括登録のステージ管理のステージ
|
||||
* @const {string}
|
||||
*/
|
||||
export const IMPORT_USERS_STAGES = {
|
||||
CREATED: "created",
|
||||
PRAPARE: "prepare",
|
||||
START: "start",
|
||||
COMPLETE: "complete",
|
||||
DONE: "done",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ユーザーのロールと数値のマッピング
|
||||
* @const {string}
|
||||
*/
|
||||
export const RoleNumberMap: Record<number, string> = {
|
||||
1: USER_ROLES.NONE,
|
||||
2: USER_ROLES.AUTHOR,
|
||||
3: USER_ROLES.TYPIST,
|
||||
} as const;
|
||||
|
||||
export const SYSTEM_IMPORT_USERS = "import-users";
|
||||
|
||||
510
dictation_function/src/functions/importUsers.ts
Normal file
510
dictation_function/src/functions/importUsers.ts
Normal file
@ -0,0 +1,510 @@
|
||||
import { app, InvocationContext, Timer } from "@azure/functions";
|
||||
import * as dotenv from "dotenv";
|
||||
import { BlobstorageService } from "../blobstorage/blobstorage.service";
|
||||
import {
|
||||
ADMIN_ROLES,
|
||||
IMPORT_USERS_MAX_DURATION_MINUTES,
|
||||
IMPORT_USERS_STAGE_FILE_NAME,
|
||||
IMPORT_USERS_STAGES,
|
||||
RoleNumberMap,
|
||||
SYSTEM_IMPORT_USERS,
|
||||
TIERS,
|
||||
} from "../constants";
|
||||
import { ErrorRow, ImportData } from "../blobstorage/types/types";
|
||||
import { Configuration, UsersApi } from "../api";
|
||||
import { createErrorObject } from "../common/errors/utils";
|
||||
import { sign, getJwtKey } from "../common/jwt";
|
||||
import { AccessToken, SystemAccessToken } from "../common/jwt/types";
|
||||
import { isImportJson, isStageJson } from "../blobstorage/types/guards";
|
||||
|
||||
export async function importUsersProcessing(
|
||||
context: InvocationContext,
|
||||
blobstorageService: BlobstorageService,
|
||||
userApi: UsersApi
|
||||
): Promise<void> {
|
||||
context.log(`[IN] importUsersProcessing`);
|
||||
try {
|
||||
dotenv.config({ path: ".env" });
|
||||
dotenv.config({ path: ".env.local", override: true });
|
||||
|
||||
const startUnixTime = getCurrentUnixTime();
|
||||
context.log(`importUsersProcessing start: ${startUnixTime}`);
|
||||
|
||||
// ファイルが存在する間ループ
|
||||
while (true) {
|
||||
// Blobストレージからファイル名の一覧を取得(stage.json以外)
|
||||
const bloblist = await blobstorageService.listBlobs(context);
|
||||
context.log(bloblist);
|
||||
|
||||
// stage.json以外のファイルが存在しない場合は処理中断
|
||||
if (bloblist.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// ファイルのうち、日付が最も古いファイルを取得
|
||||
let targetFileName = bloblist.sort().at(0);
|
||||
if (targetFileName === undefined) {
|
||||
throw new Error("targetFileName is undefined");
|
||||
}
|
||||
let row = 1;
|
||||
|
||||
// stage.jsonを取得(ダウンロード)して読み込む
|
||||
let stageData = await blobstorageService.downloadFileData(
|
||||
context,
|
||||
IMPORT_USERS_STAGE_FILE_NAME
|
||||
);
|
||||
|
||||
// stage.jsonが存在しない場合は、新規作成する
|
||||
if (stageData === undefined) {
|
||||
stageData = JSON.stringify({
|
||||
update: getCurrentUnixTime(),
|
||||
state: IMPORT_USERS_STAGES.CREATED,
|
||||
});
|
||||
const updateSuccess = await blobstorageService.updateFile(
|
||||
context,
|
||||
IMPORT_USERS_STAGE_FILE_NAME,
|
||||
stageData
|
||||
);
|
||||
if (!updateSuccess) {
|
||||
throw new Error(
|
||||
`update stage.json failed. state: ${IMPORT_USERS_STAGES.CREATED} filename: ${targetFileName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const stage = JSON.parse(stageData);
|
||||
|
||||
if (!isStageJson(stage)) {
|
||||
throw new Error("stage.json is invalid");
|
||||
}
|
||||
|
||||
// 作業中のstage.jsonが存在する場合は、処理を再開する
|
||||
if (
|
||||
stage.state !== IMPORT_USERS_STAGES.CREATED &&
|
||||
stage.state !== IMPORT_USERS_STAGES.DONE
|
||||
) {
|
||||
// stage.jsonが存在し、内部状態が処理中で、最終更新日時が10分以上前だった場合は処理中断とみなして途中から再開
|
||||
const nowUnixTime = getCurrentUnixTime();
|
||||
if (nowUnixTime - stage.update > 10 * 60) {
|
||||
// stage.jsonの内容から処理対象のfilepathを特定する
|
||||
context.log(stage.filename);
|
||||
if (stage.filename === undefined) {
|
||||
context.log("stage.filename is undefined");
|
||||
break;
|
||||
}
|
||||
targetFileName = stage.filename;
|
||||
// 処理開始行をstage.jsonを元に復元する
|
||||
row = stage.row ?? 1;
|
||||
} else {
|
||||
// 内部状態が処理中であれば処理中断(処理が終わる前にTimerから再度起動されてしまったケース)
|
||||
context.log("stage is processing");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const updateSuccess = await blobstorageService.updateFile(
|
||||
context,
|
||||
IMPORT_USERS_STAGE_FILE_NAME,
|
||||
JSON.stringify({
|
||||
update: getCurrentUnixTime(),
|
||||
state: IMPORT_USERS_STAGES.PRAPARE,
|
||||
filename: targetFileName,
|
||||
})
|
||||
);
|
||||
if (!updateSuccess) {
|
||||
throw new Error(
|
||||
`update stage.json failed. state: ${IMPORT_USERS_STAGES.PRAPARE} filename: ${targetFileName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 対象ファイルをダウンロードして読み込む
|
||||
const importsData = await blobstorageService.downloadFileData(
|
||||
context,
|
||||
targetFileName
|
||||
);
|
||||
|
||||
// 一括登録ユーザー一覧をメモリ上に展開
|
||||
const imports =
|
||||
importsData === undefined ? undefined : JSON.parse(importsData);
|
||||
if (!isImportJson(imports)) {
|
||||
throw new Error(`json: ${targetFileName} is invalid`);
|
||||
}
|
||||
|
||||
if (imports === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 代行操作トークンを発行する
|
||||
const accsessToken = await generateDelegationAccessToken(
|
||||
context,
|
||||
imports.external_id,
|
||||
imports.user_role
|
||||
);
|
||||
|
||||
// 一括登録ユーザー一覧をループして、一括登録ユーザーを一括登録する
|
||||
const errors: ErrorRow[] = [];
|
||||
for (const user of imports.data) {
|
||||
{
|
||||
// stage.jsonを更新(ユーザー追加開始)
|
||||
const updateSuccess = await blobstorageService.updateFile(
|
||||
context,
|
||||
IMPORT_USERS_STAGE_FILE_NAME,
|
||||
JSON.stringify({
|
||||
update: getCurrentUnixTime(),
|
||||
state: IMPORT_USERS_STAGES.START,
|
||||
filename: targetFileName,
|
||||
row: row,
|
||||
})
|
||||
);
|
||||
if (!updateSuccess) {
|
||||
throw new Error(
|
||||
`update stage.json failed. state: ${IMPORT_USERS_STAGES.START} filename: ${targetFileName} row: ${row}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!checkUser(context, user, targetFileName, row)) {
|
||||
throw new Error(
|
||||
`Invalid user data. filename: ${targetFileName} row: ${row}`
|
||||
);
|
||||
}
|
||||
|
||||
// ユーザーを追加する
|
||||
await addUser(context, userApi, user, accsessToken);
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
context.log(error);
|
||||
// エラーが発生したらエラーコードを控えておく
|
||||
errors.push({ row: row, error: error.code, name: user.name });
|
||||
}
|
||||
|
||||
{
|
||||
// stage.jsonを更新(ユーザー追加完了)
|
||||
const updateSuccess = await blobstorageService.updateFile(
|
||||
context,
|
||||
IMPORT_USERS_STAGE_FILE_NAME,
|
||||
JSON.stringify({
|
||||
update: getCurrentUnixTime(),
|
||||
state: IMPORT_USERS_STAGES.COMPLETE,
|
||||
filename: targetFileName,
|
||||
row: row,
|
||||
errors: errors,
|
||||
})
|
||||
);
|
||||
if (!updateSuccess) {
|
||||
throw new Error(
|
||||
`update stage.json failed. state: ${IMPORT_USERS_STAGES.COMPLETE} filename: ${targetFileName} row: ${row}`
|
||||
);
|
||||
}
|
||||
}
|
||||
row++;
|
||||
|
||||
// 500ms待機
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
// 処理対象のユーザー一覧ファイルを削除する
|
||||
await blobstorageService.deleteFile(context, targetFileName);
|
||||
|
||||
// システムトークンを発行
|
||||
const systemToken = await generateSystemToken(context);
|
||||
|
||||
// 一括登録完了メールを送信する(ODMS Cloudの一括追加完了APIを呼び出す)
|
||||
await userApi.multipleImportsComplate(
|
||||
{
|
||||
accountId: imports.account_id,
|
||||
filename: imports.file_name,
|
||||
requestTime: getCurrentUnixTime(),
|
||||
errors: errors.map((error) => {
|
||||
return {
|
||||
name: error.name,
|
||||
line: error.row,
|
||||
errorCode: error.error,
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${systemToken}` },
|
||||
}
|
||||
);
|
||||
|
||||
{
|
||||
// stage.jsonを更新(処理完了)
|
||||
const updateSuccess = await blobstorageService.updateFile(
|
||||
context,
|
||||
IMPORT_USERS_STAGE_FILE_NAME,
|
||||
JSON.stringify({
|
||||
update: getCurrentUnixTime(),
|
||||
state: IMPORT_USERS_STAGES.DONE,
|
||||
})
|
||||
);
|
||||
if (!updateSuccess) {
|
||||
throw new Error(
|
||||
`update stage.json failed. state: ${IMPORT_USERS_STAGES.DONE} filename: ${targetFileName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 経過時間を確認して、30分以上経過していたら処理を中断する
|
||||
{
|
||||
const currentUnixTime = getCurrentUnixTime();
|
||||
// 時間の差分を計算(秒)
|
||||
const elapsedSec = currentUnixTime - startUnixTime;
|
||||
|
||||
// 30分以上経過していたら処理を中断する
|
||||
if (elapsedSec > IMPORT_USERS_MAX_DURATION_MINUTES * 60) {
|
||||
context.log("timeout");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
context.log("importUsers failed.");
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] importUsersProcessing`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function importUsers(
|
||||
myTimer: Timer,
|
||||
context: InvocationContext
|
||||
): Promise<void> {
|
||||
context.log(`[IN] importUsers`);
|
||||
try {
|
||||
dotenv.config({ path: ".env" });
|
||||
dotenv.config({ path: ".env.local", override: true });
|
||||
|
||||
const blobstorageService = new BlobstorageService();
|
||||
const userApi = new UsersApi(
|
||||
new Configuration({
|
||||
basePath: process.env.BASE_PATH,
|
||||
})
|
||||
);
|
||||
|
||||
await importUsersProcessing(context, blobstorageService, userApi);
|
||||
} catch (e) {
|
||||
context.log("importUsers failed.");
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] importUsers`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ODMS CloudのAPIを呼び出してユーザーを追加する
|
||||
* @param context
|
||||
* @param user
|
||||
* @returns user
|
||||
*/
|
||||
export async function addUser(
|
||||
context: InvocationContext,
|
||||
userApi: UsersApi,
|
||||
user: ImportData,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
context.log(`[IN] addUser`);
|
||||
try {
|
||||
await userApi.signup(
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: RoleNumberMap[user.role],
|
||||
autoRenew: user.auto_renew === 1,
|
||||
notification: user.notification === 1,
|
||||
authorId: user.author_id,
|
||||
encryption: user.encryption === 1,
|
||||
encryptionPassword: user.encryption_password,
|
||||
prompt: user.prompt === 1,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] addUser`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* ユーザーのデータが正しいかどうかをチェック
|
||||
* @param context
|
||||
* @param user
|
||||
* @param fileName
|
||||
* @param row
|
||||
* @returns true if user
|
||||
*/
|
||||
function checkUser(
|
||||
context: InvocationContext,
|
||||
user: ImportData,
|
||||
fileName: string,
|
||||
row: number
|
||||
): boolean {
|
||||
context.log(
|
||||
`[IN] checkUser | params: { fileName: ${fileName}, row: ${row} }`
|
||||
);
|
||||
try {
|
||||
// 名前が255文字以内であること
|
||||
if (user.name.length > 255) {
|
||||
context.log(`name is too long. fileName: ${fileName}, row: ${row}`);
|
||||
return false;
|
||||
}
|
||||
const emailPattern =
|
||||
/^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/;
|
||||
// メールアドレスが255文字以内であること
|
||||
if (user.email.length > 255) {
|
||||
context.log(`email is too long. fileName: ${fileName}, row: ${row}`);
|
||||
return false;
|
||||
}
|
||||
if (!emailPattern.test(user.email)) {
|
||||
context.log(`Invalid email. fileName: ${fileName}, row: ${row}`);
|
||||
return false;
|
||||
}
|
||||
// ロールが(0/1/2)のいずれかであること
|
||||
if (![0, 1, 2].includes(user.role)) {
|
||||
context.log(`Invalid role number. fileName: ${fileName}, row: ${row}`);
|
||||
return false;
|
||||
}
|
||||
// ロールがAuthorの場合
|
||||
if (user.role === 1) {
|
||||
// author_idが必須
|
||||
if (user.author_id === undefined) {
|
||||
context.log(
|
||||
`author_id is required. fileName: ${fileName}, row: ${row}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// author_idが16文字以内であること
|
||||
if (user.author_id.length > 16) {
|
||||
context.log(
|
||||
`author_id is too long. fileName: ${fileName}, row: ${row}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// author_idが半角大文字英数字とハイフンであること
|
||||
if (!/^[A-Z0-9_]*$/.test(user.author_id)) {
|
||||
context.log(`author_id is invalid. fileName: ${fileName}, row: ${row}`);
|
||||
return false;
|
||||
}
|
||||
// encryptionが必須
|
||||
if (user.encryption === undefined) {
|
||||
context.log(
|
||||
`encryption is required. fileName: ${fileName}, row: ${row}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// encryptionが1の場合
|
||||
if (user.encryption === 1) {
|
||||
// encryption_passwordが必須
|
||||
if (user.encryption_password === undefined) {
|
||||
context.log(
|
||||
`encryption_password is required. fileName: ${fileName}, row: ${row}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// 4~16文字の半角英数字と記号のみであること
|
||||
if (!/^[!-~]{4,16}$/.test(user.encryption_password)) {
|
||||
context.log(
|
||||
`encryption_password is invalid. fileName: ${fileName}, row: ${row}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (user.prompt === undefined) {
|
||||
context.log(`prompt is required. fileName: ${fileName}, row: ${row}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] checkUser`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作用のアクセストークンを生成します
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @returns delegation token
|
||||
*/
|
||||
async function generateDelegationAccessToken(
|
||||
context: InvocationContext,
|
||||
externalId: string,
|
||||
role: string
|
||||
): Promise<string> {
|
||||
context.log(
|
||||
`[IN] generateDelegationAccessToken | params: { externalId: ${externalId} }`
|
||||
);
|
||||
try {
|
||||
// 要求されたトークンの寿命を決定
|
||||
const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB);
|
||||
const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? "");
|
||||
|
||||
const token = sign<AccessToken>(
|
||||
{
|
||||
role: `${role} ${ADMIN_ROLES.ADMIN}`,
|
||||
tier: TIERS.TIER5,
|
||||
userId: externalId,
|
||||
delegateUserId: SYSTEM_IMPORT_USERS,
|
||||
},
|
||||
tokenLifetime,
|
||||
privateKey
|
||||
);
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] generateDelegationAccessToken`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* System用のアクセストークンを生成します
|
||||
* @param context
|
||||
* @returns system token
|
||||
*/
|
||||
async function generateSystemToken(
|
||||
context: InvocationContext
|
||||
): Promise<string> {
|
||||
context.log(`[IN] generateSystemToken`);
|
||||
try {
|
||||
// 要求されたトークンの寿命を決定
|
||||
const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB);
|
||||
const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? "");
|
||||
|
||||
const token = sign<SystemAccessToken>(
|
||||
{
|
||||
systemName: SYSTEM_IMPORT_USERS,
|
||||
},
|
||||
tokenLifetime,
|
||||
privateKey
|
||||
);
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] generateSystemToken`);
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentUnixTime = () => Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
// 5分毎に実行
|
||||
app.timer("importUsers", {
|
||||
schedule: "0 */5 * * * *",
|
||||
handler: importUsers,
|
||||
});
|
||||
90
dictation_function/src/test/importUsers.spec.ts
Normal file
90
dictation_function/src/test/importUsers.spec.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
import { BlobstorageService } from "../blobstorage/blobstorage.service";
|
||||
import { importUsersProcessing } from "../functions/importUsers";
|
||||
import {
|
||||
PostMultipleImportsCompleteRequest,
|
||||
SignupRequest,
|
||||
UsersApi,
|
||||
} from "../api/api";
|
||||
import { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
|
||||
describe("importUsersProcessing", () => {
|
||||
dotenv.config({ path: ".env" });
|
||||
dotenv.config({ path: ".env.local", override: true });
|
||||
|
||||
it("stage.jsonがない状態でユーザー追加できること", async () => {
|
||||
const context = new InvocationContext();
|
||||
|
||||
const userApiMock = new UsersApiMock() as UsersApi;
|
||||
|
||||
// // 呼び出し回数でテスト成否を判定
|
||||
const spySignup = jest.spyOn(userApiMock, "signup");
|
||||
|
||||
const blobService = new BlobstorageService();
|
||||
|
||||
const mockListBlobs = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(["U_20210101_000000.json"])
|
||||
.mockReturnValue([]);
|
||||
const mockDownloadFileData = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValue(
|
||||
`{"account_id": 1, "user_id": 1, "external_id": "hoge", "user_role": "none", "file_name": "U_20240216_143802_8001_12211.csv", "date": 1111111, "data": [{"name": "name1","email": "email1@example.com","role": 1,"author_id": "AUTHOR_ID1","auto_renew": 1,"notification": 1,"encryption": 1,"encryption_password": "password","prompt": 1},{"name": "name2","email": "email2@example.com","role": 1,"author_id": "AUTHOR_ID2","auto_renew": 1,"notification": 1,"encryption": 1,"encryption_password": "password","prompt": 1}]}`
|
||||
);
|
||||
const mockUpdateFile = jest.fn().mockReturnValue(true);
|
||||
const mockDeleteFile = jest.fn().mockReturnValue(undefined);
|
||||
const mockIsFileExists = jest.fn().mockReturnValueOnce(false);
|
||||
|
||||
blobService.listBlobs = mockListBlobs;
|
||||
blobService.downloadFileData = mockDownloadFileData;
|
||||
blobService.updateFile = mockUpdateFile;
|
||||
blobService.deleteFile = mockDeleteFile;
|
||||
blobService.isFileExists = mockIsFileExists;
|
||||
|
||||
await importUsersProcessing(context, blobService, userApiMock);
|
||||
expect(spySignup.mock.calls).toHaveLength(2);
|
||||
}, 30000);
|
||||
|
||||
it("ファイルがない場合はそのまま終了すること", async () => {
|
||||
const context = new InvocationContext();
|
||||
|
||||
const userApiMock = new UsersApiMock() as UsersApi;
|
||||
|
||||
// // 呼び出し回数でテスト成否を判定
|
||||
const spySignup = jest.spyOn(userApiMock, "signup");
|
||||
|
||||
const blobService = new BlobstorageService();
|
||||
|
||||
const mockListBlobs = jest.fn().mockReturnValue([]);
|
||||
const mockDownloadFileData = jest.fn().mockReturnValue("");
|
||||
const mockUpdateFile = jest.fn().mockReturnValue(undefined);
|
||||
const mockDeleteFile = jest.fn().mockReturnValue(undefined);
|
||||
const mockIsFileExists = jest.fn().mockReturnValueOnce(false);
|
||||
|
||||
blobService.listBlobs = mockListBlobs;
|
||||
blobService.downloadFileData = mockDownloadFileData;
|
||||
blobService.updateFile = mockUpdateFile;
|
||||
blobService.deleteFile = mockDeleteFile;
|
||||
blobService.isFileExists = mockIsFileExists;
|
||||
|
||||
await importUsersProcessing(context, blobService, userApiMock);
|
||||
expect(spySignup.mock.calls).toHaveLength(0);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
export class UsersApiMock extends UsersApi {
|
||||
async signup(
|
||||
ignupRequest: SignupRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<object, void>> {
|
||||
return { data: {}, status: 200, statusText: "", headers: {}, config: {} };
|
||||
}
|
||||
async multipleImportsComplate(
|
||||
postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<object, void>> {
|
||||
return { data: {}, status: 200, statusText: "", headers: {}, config: {} };
|
||||
}
|
||||
}
|
||||
@ -1636,7 +1636,7 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "パラメータ不正/アカウント不在",
|
||||
"description": "パラメータ不正",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
@ -2267,6 +2267,118 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/multiple-imports": {
|
||||
"post": {
|
||||
"operationId": "multipleImports",
|
||||
"summary": "",
|
||||
"description": "ユーザーを一括登録します",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostMultipleImportsRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostMultipleImportsResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "不正なパラメータ",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/multiple-imports/complete": {
|
||||
"post": {
|
||||
"operationId": "multipleImportsComplate",
|
||||
"summary": "",
|
||||
"description": "ユーザー一括登録の完了を通知します",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostMultipleImportsCompleteRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostMultipleImportsCompleteResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "不正なパラメータ",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/files/audio/upload-finished": {
|
||||
"post": {
|
||||
"operationId": "uploadFinished",
|
||||
@ -4676,6 +4788,68 @@
|
||||
"required": ["userId"]
|
||||
},
|
||||
"PostDeleteUserResponse": { "type": "object", "properties": {} },
|
||||
"MultipleImportUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "ユーザー名" },
|
||||
"email": { "type": "string", "description": "メールアドレス" },
|
||||
"role": {
|
||||
"type": "number",
|
||||
"description": "0(none)/1(author)/2(typist)"
|
||||
},
|
||||
"authorId": { "type": "string" },
|
||||
"autoRenew": { "type": "number", "description": "0(false)/1(true)" },
|
||||
"notification": {
|
||||
"type": "number",
|
||||
"description": "0(false)/1(true)"
|
||||
},
|
||||
"encryption": { "type": "number", "description": "0(false)/1(true)" },
|
||||
"encryptionPassword": { "type": "string" },
|
||||
"prompt": { "type": "number", "description": "0(false)/1(true)" }
|
||||
},
|
||||
"required": ["name", "email", "role", "autoRenew", "notification"]
|
||||
},
|
||||
"PostMultipleImportsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filename": { "type": "string", "description": "CSVファイル名" },
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/MultipleImportUser" }
|
||||
}
|
||||
},
|
||||
"required": ["filename", "users"]
|
||||
},
|
||||
"PostMultipleImportsResponse": { "type": "object", "properties": {} },
|
||||
"MultipleImportErrors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "ユーザー名" },
|
||||
"line": { "type": "number", "description": "エラー発生行数" },
|
||||
"errorCode": { "type": "string", "description": "エラーコード" }
|
||||
},
|
||||
"required": ["name", "line", "errorCode"]
|
||||
},
|
||||
"PostMultipleImportsCompleteRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accountId": { "type": "number", "description": "アカウントID" },
|
||||
"filename": { "type": "string", "description": "CSVファイル名" },
|
||||
"requestTime": {
|
||||
"type": "number",
|
||||
"description": "一括登録受付時刻(UNIXTIME/ミリ秒)"
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/MultipleImportErrors" }
|
||||
}
|
||||
},
|
||||
"required": ["accountId", "filename", "requestTime", "errors"]
|
||||
},
|
||||
"PostMultipleImportsCompleteResponse": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"AudioOptionItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user