## 概要 [Task3879: function修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3879) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 - 修正箇所がほかの機能に影響していないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## クエリの変更 - Repositoryを変更し、クエリが変更された場合は変更内容を確認する - Before/Afterのクエリ - クエリ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など - 行った修正がデグレを発生させていないことを確認できるか - 具体的にどのような確認をしたか - どのケースに対してどのような手段でデグレがないことを担保しているか ## 補足 - 相談、参考資料などがあれば
516 lines
16 KiB
TypeScript
516 lines
16 KiB
TypeScript
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";
|
||
import https from "https";
|
||
|
||
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(context, 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}` },
|
||
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
||
}
|
||
);
|
||
|
||
{
|
||
// 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.role === 1 ? user.author_id : undefined,
|
||
encryption: user.role === 1 ? user.encryption === 1 : undefined,
|
||
encryptionPassword:
|
||
user.encryption === 1 ? user.encryption_password : undefined,
|
||
prompt: user.role === 1 ? user.prompt === 1 : undefined,
|
||
},
|
||
{
|
||
headers: { authorization: `Bearer ${token}` },
|
||
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
||
}
|
||
);
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.error(JSON.stringify(e.response?.data));
|
||
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,
|
||
});
|