Merge branch 'develop' into main

This commit is contained in:
maruyama.t 2023-11-13 16:32:56 +09:00
commit dd00b2fe9b
42 changed files with 1890 additions and 284 deletions

View File

@ -2,7 +2,6 @@ import { Route, Routes } from "react-router-dom";
import TopPage from "pages/TopPage";
import AuthPage from "pages/AuthPage";
import LoginPage from "pages/LoginPage";
import SamplePage from "pages/SamplePage";
import { AuthErrorPage } from "pages/ErrorPage";
import { NotFoundPage } from "pages/ErrorPage/notFound";
import { RouteAuthGuard } from "components/auth/routeAuthGuard";
@ -53,11 +52,6 @@ const AppRouter: React.FC = () => (
path="/license"
element={<RouteAuthGuard component={<LicensePage />} />}
/>
<Route
path="/xxx"
element={<RouteAuthGuard component={<SamplePage />} />}
/>
{/* XXX ヘッダーの挙動確認のため仮のページを作成 */}
<Route
path="/account"
element={<RouteAuthGuard component={<AccountPage />} />}

View File

@ -39,6 +39,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [
"E010501",
];
/**
*
* @const {string[]}
*/
export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"];
/**
*
* @const {number}

View File

@ -46,6 +46,7 @@ export const DelegationBar: React.FC = (): JSX.Element => {
alt="Exit"
title="Exit"
onClick={onClickExit}
data-tag="exit-delegation"
/>
</div>
);

View File

@ -7,7 +7,6 @@ export const HEADER_MENUS_LICENSE = "License";
export const HEADER_MENUS_DICTATIONS = "Dictations";
export const HEADER_MENUS_WORKFLOW = "Workflow";
export const HEADER_MENUS_PARTNER = "Partners";
export const HEADER_MENUS_XXX = "XXX"; // XXX 仮のタブ
export const HEADER_MENUS: {
key: HeaderMenus;
@ -44,7 +43,6 @@ export const HEADER_MENUS: {
label: getTranslationID("common.label.headerPartners"),
path: "/partners",
},
{ key: HEADER_MENUS_XXX, label: "xxx", path: "/xxx" }, // XXX 仮のタブ
];
export const HEADER_NAME = getTranslationID("common.label.headerName");

View File

@ -87,6 +87,7 @@ const LoginedHeader: React.FC<HeaderProps> = (props: HeaderProps) => {
? styles.isActive
: ""
}
data-tag={`menu-${x.key}`}
>
{t(x.label)}
</a>
@ -101,6 +102,7 @@ const LoginedHeader: React.FC<HeaderProps> = (props: HeaderProps) => {
<span
className={styles.accountSignout}
onClick={onSignoutButton}
data-tag="logout"
style={{ pointerEvents: isDelegation ? "none" : "auto" }}
>
<img src={logout} alt="" className={styles.accountIcon} />

View File

@ -8,8 +8,7 @@ export type HeaderMenus =
| "License"
| "Dictations"
| "Workflow"
| "Partners"
| "XXX";
| "Partners";
// ログイン後に遷移しうるパス
export type LoginedPaths =
@ -18,5 +17,4 @@ export type LoginedPaths =
| "/license"
| "/dictations"
| "/workflow"
| "/partners"
| "/xxx";
| "/partners";

View File

@ -20,7 +20,6 @@ export const isLoginPaths = (d: string): d is LoginedPaths => {
case "/dictations":
case "/workflow":
case "/partners":
case "/xxx":
return true;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -39,22 +39,34 @@ const Snackbar: React.FC<SnackbarProps> = (props) => {
const isShow = isOpen ? styles.isShow : "";
return (
<div className={`${styles.snackbar} ${isAlert} ${isShow}`}>
<div
className={`${styles.snackbar} ${isAlert} ${isShow}`}
data-tag="snackbar"
>
{level === "error" ? (
<img src={reportWhite} className={styles.snackbarIcon} alt="check" />
<img
src={reportWhite}
className={styles.snackbarIcon}
alt="check"
data-tag="snackbar-error"
/>
) : (
<img
src={checkCircleWhite}
className={styles.snackbarIcon}
alt="report"
data-tag="snackbar-report"
/>
)}
<p className={styles.txNormal}>{message}</p>
<p className={styles.txNormal} data-tag="snackbar-text">
{message}
</p>
{level === "error" && (
<button
style={{ marginLeft: "auto" }}
type="button"
onClick={onCloseSnackbar}
data-tag="close-snackbar"
>
<img
src={closeWhite}

View File

@ -1,4 +1,4 @@
import { Dealer } from "api/api";
import { Dealer, User } from "api/api";
import { RootState } from "app/store";
export const selectAccountInfo = (state: RootState) =>
@ -10,7 +10,8 @@ export const selectDealers = (state: RootState) => {
const { country } = state.account.domain.getAccountInfo.account;
return dealers.filter((x: Dealer) => x.country === country);
};
export const selectUsers = (state: RootState) => state.account.domain.users;
export const selectUsers = (state: RootState) =>
state.account.domain.users.filter((x: User) => x.emailVerified);
export const selectIsLoading = (state: RootState) =>
state.account.apps.isLoading;
export const selectUpdateAccountInfo = (state: RootState) =>

View File

@ -81,7 +81,9 @@ export const isAdminUser = (): boolean => {
if (!token) {
return false;
}
return token.role.includes(ADMIN_ROLES.ADMIN);
// token.roleを" "で分割して配列にする
const role = token.role.split(" ");
return role.includes(ADMIN_ROLES.ADMIN);
};
/**
@ -95,7 +97,9 @@ export const isStandardUser = (): boolean => {
if (!token) {
return false;
}
return token.role.includes(ADMIN_ROLES.STANDARD);
// token.roleを" "で分割して配列にする
const role = token.role.split(" ");
return role.includes(ADMIN_ROLES.STANDARD);
};
/**
@ -108,7 +112,9 @@ export const isAuthorUser = (): boolean => {
if (!token) {
return false;
}
return token.role.includes(USER_ROLES.AUTHOR);
// token.roleを" "で分割して配列にする
const role = token.role.split(" ");
return role.includes(USER_ROLES.AUTHOR);
};
/**
@ -132,5 +138,8 @@ export const isTypistUser = (): boolean => {
if (!token) {
return false;
}
return token.role.includes(USER_ROLES.TYPIST);
// token.roleを" "で分割して配列にする
const role = token.role.split(" ");
// roleの中に"typist"が含まれているかどうかを返す
return role.includes(USER_ROLES.TYPIST);
};

View File

@ -1,6 +1,7 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getAccessToken, setToken } from "features/auth";
import { setToken } from "features/auth";
import { KEYS_TO_PRESERVE } from "components/auth/constants";
import {
AuthApi,
UsersApi,
@ -42,7 +43,18 @@ export const loginAsync = createAsyncThunk<
refreshToken: data.refreshToken,
})
);
// ローカルストレージに残すキー
const keysToPreserve = KEYS_TO_PRESERVE;
// すべてのローカルストレージキーを取得
const allKeys = Object.keys(localStorage);
// 特定のキーを除外して削除
allKeys.forEach((key) => {
if (!keysToPreserve.includes(key)) {
localStorage.removeItem(key);
}
});
return data;
} catch (e) {
// e ⇒ errorObjectに変換"

View File

@ -56,7 +56,11 @@ export const DeleteAccountPopup: React.FC<DeleteAccountPopupProps> = (
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("deleteAccountPopup.label.title"))}
<button type="button" onClick={closePopup}>
<button
type="button"
onClick={closePopup}
data-tag="close-delegate-popup"
>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
@ -92,6 +96,7 @@ export const DeleteAccountPopup: React.FC<DeleteAccountPopupProps> = (
)}
className={`${styles.formDelete} ${styles.marginBtm1} ${styles.isActive}`}
onClick={onDeleteAccount}
data-tag="delete-account"
/>
<p>
<input
@ -102,6 +107,7 @@ export const DeleteAccountPopup: React.FC<DeleteAccountPopupProps> = (
)}
className={`${styles.formButtonTx} ${styles.marginBtm1}`}
onClick={closePopup}
data-tag="cancel-delete-account"
/>
</p>
</dd>

View File

@ -364,6 +364,7 @@ const AccountPage: React.FC = (): JSX.Element => {
}
`}
onClick={onSaveChangesButton}
data-tag="savechanges-account"
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
@ -377,7 +378,11 @@ const AccountPage: React.FC = (): JSX.Element => {
<ul className={styles.linkBottom}>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a className={styles.linkTx} onClick={onDeleteAccountOpen}>
<a
className={styles.linkTx}
onClick={onDeleteAccountOpen}
data-tag="open-delete-account-popup"
>
{t(getTranslationID("accountPage.label.deleteAccount"))}
</a>
</li>

View File

@ -1,7 +1,14 @@
import { useMsal } from "@azure/msal-react";
import { AppDispatch } from "app/store";
import { isIdToken } from "common/token";
import { loadAccessToken, loadRefreshToken } from "features/auth";
import {
clearToken,
isAdminUser,
isApproveTier,
isStandardUser,
loadAccessToken,
loadRefreshToken,
} from "features/auth";
import { loginAsync, selectLocalStorageKeyforIdToken } from "features/login";
import React, { useCallback, useEffect } from "react";
import Footer from "components/footer";
@ -10,6 +17,7 @@ import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { isErrorObject } from "common/errors";
import { TIERS } from "components/auth/constants";
const LoginPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
@ -51,7 +59,29 @@ const LoginPage: React.FC = (): JSX.Element => {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
navigate("/xxx");
// 第一~第四階層の管理者はライセンス画面へ遷移
if (
isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]) &&
isAdminUser()
) {
navigate("/license");
return;
}
// 第五階層の管理者はユーザー画面へ遷移
if (isApproveTier([TIERS.TIER5]) && isAdminUser()) {
navigate("/user");
return;
}
// 一般ユーザーはdictationPageへ遷移
if (isStandardUser()) {
navigate("/dictations");
return;
}
// それ以外は認証エラー画面へ遷移
instance.logoutRedirect({
postLogoutRedirectUri: "/AuthError",
});
clearToken();
}
},
[dispatch, i18n.language, instance, navigate]

View File

@ -1,39 +0,0 @@
import { useMsal } from "@azure/msal-react";
import { AppDispatch } from "app/store";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import Footer from "components/footer";
import Header from "components/header";
import { clearToken } from "features/auth";
import { clearUserInfo } from "features/login";
import React from "react";
import { useDispatch } from "react-redux";
import styles from "styles/app.module.scss";
const SamplePage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
const dispatch: AppDispatch = useDispatch();
return (
<div className={styles.wrap}>
<Header />
<UpdateTokenTimer />
<div>
<button
type="button"
className={styles.buttonText}
onClick={() => {
instance.logoutRedirect({ postLogoutRedirectUri: "/" });
dispatch(clearToken());
dispatch(clearUserInfo());
}}
>
sign out
</button>
</div>
<Footer />
</div>
);
};
export default SamplePage;

View File

@ -109,6 +109,7 @@ const TermsPage: React.FC = (): JSX.Element => {
target="_blank"
className={styles.linkTx}
onClick={() => setIsClickedEulaLink(true)}
data-tag="open-eula"
>
{t(getTranslationID("termsPage.label.linkOfEula"))}
</a>
@ -123,6 +124,7 @@ const TermsPage: React.FC = (): JSX.Element => {
value=""
onChange={(e) => setIsCheckedEula(e.target.checked)}
disabled={!isClickedEulaLink}
data-tag="accept-eula"
/>
{t(
getTranslationID("termsPage.label.checkBoxForConsent")
@ -140,6 +142,7 @@ const TermsPage: React.FC = (): JSX.Element => {
target="_blank"
className={styles.linkTx}
onClick={() => setIsClickedDpaLink(true)}
data-tag="open-dpa"
>
{t(getTranslationID("termsPage.label.linkOfDpa"))}
</a>
@ -154,6 +157,7 @@ const TermsPage: React.FC = (): JSX.Element => {
value=""
onChange={(e) => setIsCheckedDpa(e.target.checked)}
disabled={!isClickedDpaLink}
data-tag="accept-dpa"
/>
{t(
getTranslationID("termsPage.label.checkBoxForConsent")
@ -172,6 +176,7 @@ const TermsPage: React.FC = (): JSX.Element => {
canClickButton() ? styles.isActive : ""
}`}
onClick={onAcceptTermsOfUse}
data-tag="accept-terms"
/>
</p>
</dd>

View File

@ -86,6 +86,7 @@ const TopPage: React.FC = (): JSX.Element => {
};
instance.loginRedirect(loginRequest);
}}
data-tag="signin"
>
{t(getTranslationID("topPage.label.signInButton"))}
<img

View File

@ -8,6 +8,8 @@
"version": "1.0.0",
"dependencies": {
"@azure/functions": "^4.0.0",
"@azure/identity": "^3.1.3",
"@microsoft/microsoft-graph-client": "^3.0.5",
"@sendgrid/mail": "^7.7.0",
"dotenv": "^16.0.3",
"mysql2": "^2.3.3",
@ -38,6 +40,123 @@
"node": ">=6.0.0"
}
},
"node_modules/@azure/abort-controller": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
"integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
"dependencies": {
"tslib": "^2.2.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@azure/core-auth": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz",
"integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-util": "^1.1.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@azure/core-client": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.7.3.tgz",
"integrity": "sha512-kleJ1iUTxcO32Y06dH9Pfi9K4U+Tlb111WXEnbt7R/ne+NLRwppZiTGJuTD5VVoxTMK5NTbEtm5t2vcdNCFe2g==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.4.0",
"@azure/core-rest-pipeline": "^1.9.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.12.2.tgz",
"integrity": "sha512-wLLJQdL4v1yoqYtEtjKNjf8pJ/G/BqVomAWxcKOR1KbZJyCEnCv04yks7Y1NhJ3JzxbDs307W67uX0JzklFdCg==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.4.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.3.0",
"@azure/logger": "^1.0.0",
"form-data": "^4.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
"dependencies": {
"@tootallnate/once": "2",
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@azure/core-tracing": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz",
"integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==",
"dependencies": {
"tslib": "^2.2.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@azure/core-util": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.6.1.tgz",
"integrity": "sha512-h5taHeySlsV9qxuK64KZxy4iln1BtMYlNt5jbuEFN3UFSAd1EwKg/Gjl5a6tZ/W8t6li3xPnutOx7zbDyXnPmQ==",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@azure/functions": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.0.1.tgz",
@ -50,6 +169,94 @@
"node": ">=18.0"
}
},
"node_modules/@azure/identity": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-3.1.3.tgz",
"integrity": "sha512-y0jFjSfHsVPwXSwi3KaSPtOZtJZqhiqAhWUXfFYBUd/+twUBovZRXspBwLrF5rJe0r5NyvmScpQjL+TYDTQVvw==",
"deprecated": "Please upgrade to the latest version of this package to get necessary fixes",
"dependencies": {
"@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.4.0",
"@azure/core-rest-pipeline": "^1.1.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
"@azure/logger": "^1.0.0",
"@azure/msal-browser": "^2.32.2",
"@azure/msal-common": "^9.0.2",
"@azure/msal-node": "^1.14.6",
"events": "^3.0.0",
"jws": "^4.0.0",
"open": "^8.0.0",
"stoppable": "^1.1.0",
"tslib": "^2.2.0",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@azure/logger": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz",
"integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==",
"dependencies": {
"tslib": "^2.2.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@azure/msal-browser": {
"version": "2.38.3",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.38.3.tgz",
"integrity": "sha512-2WuLFnWWPR1IdvhhysT18cBbkXx1z0YIchVss5AwVA95g7CU5CpT3d+5BcgVGNXDXbUU7/5p0xYHV99V5z8C/A==",
"deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.",
"dependencies": {
"@azure/msal-common": "13.3.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-browser/node_modules/@azure/msal-common": {
"version": "13.3.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz",
"integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-9.1.1.tgz",
"integrity": "sha512-we9xR8lvu47fF0h+J8KyXoRy9+G/fPzm3QEa2TrdR3jaVS3LKAyE2qyMuUkNdbVkvzl8Zr9f7l+IUSP22HeqXw==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node": {
"version": "1.18.4",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.18.4.tgz",
"integrity": "sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==",
"deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.",
"dependencies": {
"@azure/msal-common": "13.3.1",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
"engines": {
"node": "10 || 12 || 14 || 16 || 18"
}
},
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
"version": "13.3.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz",
"integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
@ -704,6 +911,13 @@
"dev": true,
"optional": true
},
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==",
"optional": true,
"peer": true
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1364,6 +1578,32 @@
"node": ">=10"
}
},
"node_modules/@microsoft/microsoft-graph-client": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-3.0.5.tgz",
"integrity": "sha512-xQADFNLUhE78RzYadFZtOmy/5wBZenSZhVK193m40MTDC5hl1aYMQO1QOJApnKga8WcvMCDCU10taRhuXTOz5w==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependenciesMeta": {
"@azure/identity": {
"optional": true
},
"@azure/msal-browser": {
"optional": true
},
"buffer": {
"optional": true
},
"stream-browserify": {
"optional": true
}
}
},
"node_modules/@npmcli/fs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
@ -1780,7 +2020,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"devOptional": true,
"dependencies": {
"debug": "4"
},
@ -1907,8 +2146,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "0.26.1",
@ -2499,6 +2737,11 @@
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -2954,6 +3197,16 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -2999,7 +3252,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@ -3109,11 +3361,18 @@
"node": ">= 0.4"
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"engines": {
"node": ">=8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@ -3173,6 +3432,14 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.576",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz",
@ -3263,6 +3530,14 @@
"node": ">=4"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -3753,7 +4028,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"devOptional": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
@ -3870,6 +4144,31 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ioredis": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
"optional": true,
"peer": true,
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
"lodash.defaults": "^4.2.0",
"lodash.isarguments": "^3.1.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0",
"standard-as-callback": "^2.1.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@ -3895,6 +4194,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -3945,6 +4258,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -4747,6 +5071,90 @@
"node": ">=6"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken/node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -4783,12 +5191,61 @@
"node": ">=8"
}
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"optional": true,
"peer": true
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
"optional": true,
"peer": true
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@ -4958,7 +5415,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@ -4967,7 +5423,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@ -5601,6 +6056,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -5896,6 +6367,66 @@
"node": ">= 6"
}
},
"node_modules/redis": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"optional": true,
"peer": true,
"dependencies": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-redis"
}
},
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==",
"optional": true,
"peer": true
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"optional": true,
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"optional": true,
"peer": true,
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/redis/node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/reflect-metadata": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
@ -6123,8 +6654,13 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=8"
"node": ">=6.9.0"
}
},
"node_modules/smart-buffer": {
@ -6262,6 +6798,22 @@
"node": ">=10"
}
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"optional": true,
"peer": true
},
"node_modules/stoppable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
"engines": {
"node": ">=4",
"npm": ">=6"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -6388,8 +6940,8 @@
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {

View File

@ -13,6 +13,8 @@
},
"dependencies": {
"@azure/functions": "^4.0.0",
"@azure/identity": "^3.1.3",
"@microsoft/microsoft-graph-client": "^3.0.5",
"@sendgrid/mail": "^7.7.0",
"dotenv": "^16.0.3",
"mysql2": "^2.3.3",

View File

@ -0,0 +1,74 @@
import { ClientSecretCredential } from "@azure/identity";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { AdB2cResponse, AdB2cUser } from "./types/types";
import { error } from "console";
export class Adb2cTooManyRequestsError extends Error {}
export class AdB2cService {
private graphClient: Client;
constructor() {
// ADB2Cへの認証情報
if (
!process.env.ADB2C_TENANT_ID ||
!process.env.ADB2C_CLIENT_ID ||
!process.env.ADB2C_CLIENT_SECRET
) {
throw error;
}
const credential = new ClientSecretCredential(
process.env.ADB2C_TENANT_ID,
process.env.ADB2C_CLIENT_ID,
process.env.ADB2C_CLIENT_SECRET
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["https://graph.microsoft.com/.default"],
});
this.graphClient = Client.initWithMiddleware({ authProvider });
}
/**
* Azure AD B2Cからユーザ情報を取得する
* @param externalIds ID
* @returns
*/
async getUsers(externalIds: string[]): Promise<AdB2cUser[]> {
const chunkExternalIds = splitArrayInChunksOfFifteen(externalIds);
try {
const b2cUsers: AdB2cUser[] = [];
for (let index = 0; index < chunkExternalIds.length; index++) {
const element = chunkExternalIds[index];
const res: AdB2cResponse = await this.graphClient
.api(`users/`)
.select(["id", "displayName", "identities"])
.filter(`id in (${element.map((y) => `'${y}'`).join(",")})`)
.get();
b2cUsers.push(...res.value);
}
return b2cUsers;
} catch (e) {
const { statusCode } = e;
if (statusCode === 429) {
throw new Adb2cTooManyRequestsError();
}
throw e;
} finally {
}
}
}
const splitArrayInChunksOfFifteen = (arr: string[]): string[][] => {
const result: string[][] = [];
const chunkSize = 15; // SDKの制限数
for (let i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
};

View File

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

View File

@ -0,0 +1 @@
export const ADB2C_PREFIX = "adb2c-external-id:"

View File

@ -0,0 +1,19 @@
import { ADB2C_PREFIX } from './constants';
/**
* ADB2Cのユーザー格納用のキーを生成する
* @param externalId ID
* @returns
*/
export const makeADB2CKey = (externalId: string): string => {
return `${ADB2C_PREFIX}${externalId}`;
}
/**
* ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する
* @param key
* @returns ID
*/
export const restoreAdB2cID = (key: string): string => {
return key.replace(ADB2C_PREFIX, '');
}

View File

@ -0,0 +1,7 @@
export const getMailFrom = (): string => {
const from = process.env.MAIL_FROM;
if (typeof from === "string") {
return from;
}
throw new Error("MAIL_FROM not found");
};

View File

@ -1,71 +0,0 @@
import { v4 as uuidv4 } from "uuid";
import { DataSource } from "typeorm";
import { User } from "../../entity/user.entity";
import { Account } from "../../entity/account.entity";
import { ADMIN_ROLES, USER_ROLES } from "../../constants";
type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[];
tier2Accounts: { account: Account; users: User[] }[];
tier3Accounts: { account: Account; users: User[] }[];
tier4Accounts: { account: Account; users: User[] }[];
tier5Accounts: { account: Account; users: User[] }[];
};
// 上書きされたら困る項目を除外したAccount型
type OverrideAccount = Omit<
Account,
"id" | "primary_admin_user_id" | "secondary_admin_user_id" | "user"
>;
// 上書きされたら困る項目を除外したUser型
type OverrideUser = Omit<
User,
"id" | "account" | "license" | "userGroupMembers"
>;
type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] };
type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] };
/**
* ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する
* @param dataSource
* @param defaultUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
* @returns
*/
export const makeTestUser = async (
datasource: DataSource,
defaultUserValue?: UserDefault
): Promise<User> => {
const d = defaultUserValue;
const { identifiers } = await datasource.getRepository(User).insert({
account_id: d?.account_id ?? -1,
external_id: d?.external_id ?? uuidv4(),
role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`,
author_id: d?.author_id,
accepted_eula_version: d?.accepted_eula_version ?? "1.0",
accepted_dpa_version: d?.accepted_dpa_version ?? "1.0",
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
notification: d?.notification ?? true,
encryption: d?.encryption ?? true,
encryption_password: d?.encryption_password,
prompt: d?.prompt ?? true,
created_by: d?.created_by ?? "test_runner",
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? "updater",
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as User;
const user = await datasource.getRepository(User).findOne({
where: {
id: result.id,
},
});
if (!user) {
throw new Error("Unexpected null");
}
return user;
};

View File

@ -0,0 +1,70 @@
import {
LICENSE_EXPIRATION_DAYS,
LICENSE_EXPIRATION_THRESHOLD_DAYS,
TRIAL_LICENSE_EXPIRATION_DAYS,
} from "../../constants";
// ライセンス算出用に、その日の始まりの時刻0:00:00.000)の日付を取得する
export class DateWithZeroTime extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(); // 引数がない場合、現在の日付で初期化
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
this.setHours(0, 0, 0, 0); // 時分秒を"0:00:00.000"に固定
}
}
// ライセンス算出用に、その日の終わりの時刻23:59:59.999)の日付を取得する
export class DateWithDayEndTime extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(); // 引数がない場合、現在の日付で初期化
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
this.setHours(23, 59, 59, 999); // 時分秒を"23:59:59.999"に固定
}
}
// ライセンスの算出用に、閾値となる時刻23:59:59.999)の日付を取得する
export class ExpirationThresholdDate extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(); // 引数がない場合、現在の日付で初期化
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
this.setDate(this.getDate() + LICENSE_EXPIRATION_THRESHOLD_DAYS);
this.setHours(23, 59, 59, 999); // 時分秒を"23:59:59.999"に固定
}
}
// 新規トライアルライセンス発行時の有効期限算出用に、30日後の日付を取得する
export class NewTrialLicenseExpirationDate extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(); // 引数がない場合、現在の日付で初期化
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
this.setDate(this.getDate() + TRIAL_LICENSE_EXPIRATION_DAYS);
this.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
this.setMilliseconds(0);
}
}
// 新規ライセンス割り当て時の有効期限算出用に、365日後の日付を取得する
export class NewAllocatedLicenseExpirationDate extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(); // 引数がない場合、現在の日付で初期化
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
this.setDate(this.getDate() + LICENSE_EXPIRATION_DAYS);
this.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
this.setMilliseconds(0);
}
}

View File

@ -6,7 +6,8 @@ import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
OneToOne,
JoinColumn,
} from "typeorm";
@Entity({ name: "accounts" })
@ -65,6 +66,11 @@ export class Account {
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@OneToMany(() => User, (user) => user.id)
user: User[] | null;
@OneToOne(() => User, (user) => user.id)
@JoinColumn({ name: "primary_admin_user_id" })
primaryAdminUser: User | null;
@OneToOne(() => User, (user) => user.id)
@JoinColumn({ name: "secondary_admin_user_id" })
secondaryAdminUser: User | null;
}

View File

@ -0,0 +1,63 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
JoinColumn,
OneToOne,
} from "typeorm";
import { bigintTransformer } from "../common/entity";
import { User } from "./user.entity";
@Entity({ name: "licenses" })
export class License {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true, type: "datetime" })
expiry_date: Date | null;
@Column()
account_id: number;
@Column()
type: string;
@Column()
status: string;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
allocated_user_id: number | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
order_id: number | null;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column({ nullable: true, type: "bigint", transformer: bigintTransformer })
delete_order_id: number | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
@OneToOne(() => User, (user) => user.license)
@JoinColumn({ name: "allocated_user_id" })
user: User | null;
}

View File

@ -4,7 +4,9 @@ import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
OneToOne,
} from "typeorm";
import { License } from "./license.entity";
@Entity({ name: "users" })
export class User {
@ -70,4 +72,7 @@ export class User {
type: "datetime",
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@OneToOne(() => License, (license) => license.user)
license: License | null;
}

View File

@ -0,0 +1,347 @@
import { app, InvocationContext, Timer } from "@azure/functions";
import { Between, DataSource, In, IsNull, MoreThan, Not } from "typeorm";
import { User } from "../entity/user.entity";
import { Account } from "../entity/account.entity";
import {
ADB2C_SIGN_IN_TYPE,
LICENSE_ALLOCATED_STATUS,
TIERS,
} from "../constants";
import * as dotenv from "dotenv";
import { License } from "../entity/license.entity";
import {
DateWithDayEndTime,
DateWithZeroTime,
ExpirationThresholdDate,
} from "../common/types/types";
import { getMailFrom } from "../common/getEnv/getEnv";
import { AdB2cService } from "../adb2c/adb2c.service";
import { SendGridService } from "../sendgrid/sendgrid.service";
export async function licenseAlertProcessing(
context: InvocationContext,
datasource: DataSource,
sendgrid: SendGridService,
adb2c: AdB2cService
) {
context.log("[IN]licenseAlertProcessing");
const mailFrom = getMailFrom();
const accountRepository = datasource.getRepository(Account);
// 第五のアカウントを取得
const accounts = await accountRepository.find({
where: {
tier: TIERS.TIER5,
},
relations: {
primaryAdminUser: true,
secondaryAdminUser: true,
},
});
const licenseRepository = datasource.getRepository(License);
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
const currentDateWithZeroTime = new DateWithZeroTime();
const currentDateWithDayEndTime = new DateWithDayEndTime();
const sendTargetAccounts = [] as accountInfo[];
const counts = async () => {
for (const account of accounts) {
// 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
const allocatableLicenseWithMargin = await licenseRepository.count({
where: [
{
account_id: account.id,
status: In([
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
LICENSE_ALLOCATED_STATUS.REUSABLE,
]),
expiry_date: MoreThan(expiringSoonDate),
},
{
account_id: account.id,
status: In([
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
LICENSE_ALLOCATED_STATUS.REUSABLE,
]),
expiry_date: IsNull(),
},
],
});
// 有効期限が現在日付からしきい値以内のライセンス数を取得する
const expiringSoonLicense = await licenseRepository.count({
where: {
account_id: account.id,
expiry_date: Between(currentDate, expiringSoonDate),
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
},
});
// shortage算出
let shortage = allocatableLicenseWithMargin - expiringSoonLicense;
shortage = shortage >= 0 ? 0 : Math.abs(shortage);
// AutoRenewが未チェックかつ、有効期限当日のライセンスが割り当てられているユーザー数を取得
const userCount = await licenseRepository.count({
where: [
{
account_id: account.id,
expiry_date: Between(
currentDateWithZeroTime,
currentDateWithDayEndTime
),
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
user: {
auto_renew: false,
},
},
],
relations: {
user: true,
},
});
// 上で取得したshortageとユーザー数のどちらかが1以上ならプライマリ、セカンダリ管理者、親企業名を保持する
// shortageとユーザー数のどちらかが1以上 = アラートメールを送る必要がある)
let primaryAdminExternalId: string | undefined;
let secondaryAdminExternalId: string | undefined;
let parentCompanyName: string | undefined;
if (shortage !== 0 || userCount !== 0) {
primaryAdminExternalId = account.primaryAdminUser
? account.primaryAdminUser.external_id
: undefined;
secondaryAdminExternalId = account.secondaryAdminUser
? account.secondaryAdminUser.external_id
: undefined;
// 第五のアカウントを取得
// strictNullChecks対応
if (account.parent_account_id) {
const parent = await accountRepository.findOne({
where: {
id: account.parent_account_id,
},
});
parentCompanyName = parent?.company_name;
}
} else {
primaryAdminExternalId = undefined;
secondaryAdminExternalId = undefined;
parentCompanyName = undefined;
}
sendTargetAccounts.push({
accountId: account.id,
companyName: account.company_name,
parentCompanyName: parentCompanyName,
shortage: shortage,
userCountOfLicenseExpiringSoon: userCount,
primaryAdminExternalId: primaryAdminExternalId,
secondaryAdminExternalId: secondaryAdminExternalId,
primaryAdminEmail: undefined,
secondaryAdminEmail: undefined,
});
}
};
await counts();
// ADB2Cからユーザーを取得する用の外部ID配列を作成
const externalIds = [] as string[];
sendTargetAccounts.map((x) => {
if (x.primaryAdminExternalId) {
externalIds.push(x.primaryAdminExternalId);
}
if (x.secondaryAdminExternalId) {
externalIds.push(x.secondaryAdminExternalId);
}
});
const adb2cUsers = await adb2c.getUsers(externalIds);
// ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ
sendTargetAccounts.map((info) => {
const primaryAdminUser = adb2cUsers.find(
(adb2c) => info.primaryAdminExternalId === adb2c.id
);
if (primaryAdminUser) {
const primaryAdminMail = primaryAdminUser.identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS
)?.issuerAssignedId;
if (primaryAdminMail) {
info.primaryAdminEmail = primaryAdminMail;
}
const secondaryAdminUser = adb2cUsers.find(
(adb2c) => info.secondaryAdminExternalId === adb2c.id
);
if (secondaryAdminUser) {
const secondaryAdminMail = secondaryAdminUser.identities?.find(
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS
)?.issuerAssignedId;
if (secondaryAdminMail) {
info.secondaryAdminEmail = secondaryAdminMail;
}
}
}
});
const sendMail = async () => {
for (const targetAccount of sendTargetAccounts) {
// プライマリ管理者が入っているかチェック
// 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ
if (targetAccount.primaryAdminExternalId) {
// メール送信
// strictNullChecks対応
if (targetAccount.primaryAdminEmail) {
// ライセンス不足メール
if (targetAccount.shortage !== 0) {
const { subject, text, html } =
await sendgrid.createMailContentOfLicenseShortage();
// メールを送信
try {
await sendgrid.sendMail(
targetAccount.primaryAdminEmail,
mailFrom,
subject,
text,
html
);
context.log(
`Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}`
);
} catch {
context.log(
`Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}`
);
}
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
if (targetAccount.secondaryAdminEmail) {
// ライセンス不足メール
if (targetAccount.shortage !== 0) {
const { subject, text, html } =
await sendgrid.createMailContentOfLicenseShortage();
// メールを送信
try {
await sendgrid.sendMail(
targetAccount.secondaryAdminEmail,
mailFrom,
subject,
text,
html
);
context.log(
`Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}`
);
} catch {
context.log(
`Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
);
}
}
}
}
// ライセンス失効警告メール
if (targetAccount.userCountOfLicenseExpiringSoon !== 0) {
const { subject, text, html } =
await sendgrid.createMailContentOfLicenseExpiringSoon();
// メールを送信
try {
await sendgrid.sendMail(
targetAccount.primaryAdminEmail,
mailFrom,
subject,
text,
html
);
context.log(
`Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}`
);
} catch {
context.log(
`Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}`
);
}
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
if (targetAccount.secondaryAdminEmail) {
// ライセンス不足メール
if (targetAccount.shortage !== 0) {
const { subject, text, html } =
await sendgrid.createMailContentOfLicenseExpiringSoon();
// メールを送信
try {
await sendgrid.sendMail(
targetAccount.secondaryAdminEmail,
mailFrom,
subject,
text,
html
);
context.log(
`Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}`
);
} catch {
context.log(
`Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
);
}
}
}
}
}
}
}
};
await sendMail();
context.log("[OUT]licenseAlertProcessing");
}
export async function licenseAlert(
myTimer: Timer,
context: InvocationContext
): Promise<void> {
context.log("[IN]licenseAlert");
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
const datasource = new DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [User, Account, License],
});
await datasource.initialize();
const adb2c = new AdB2cService();
const sendgrid = new SendGridService();
try {
await licenseAlertProcessing(context, datasource, sendgrid, adb2c);
} catch (e) {
context.log("licenseAlertProcessing failed");
context.error(e);
} finally {
await datasource.destroy();
context.log("[OUT]licenseAlert");
}
}
app.timer("licenseAlert", {
schedule: "0 */1 * * * *",
handler: licenseAlert,
});
class accountInfo {
accountId: number;
companyName: string;
parentCompanyName: string | undefined;
shortage: number;
userCountOfLicenseExpiringSoon: number;
primaryAdminExternalId: string | undefined;
secondaryAdminExternalId: string | undefined;
primaryAdminEmail: string | undefined;
secondaryAdminEmail: string | undefined;
}

View File

@ -1,70 +0,0 @@
import { app, InvocationContext, Timer } from "@azure/functions";
import { DataSource } from "typeorm";
import { User } from "../entity/user.entity";
import { SendGridService } from "../sendgrid/sendgrid.service";
import * as dotenv from "dotenv";
// タイマートリガー処理のサンプルです
// TODO:開発が進んだら削除すること
export async function timerTriggerExample(
myTimer: Timer,
context: InvocationContext
): Promise<void> {
context.log("Timer function processed request.");
dotenv.config({ path: ".env" });
const datasource = new DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [User],
});
try {
await datasource.initialize();
const userRepository = datasource.getRepository(User); // Userエンティティに対応するリポジトリを取得
// ユーザーを検索
const users = await userRepository.find();
console.log(users);
} catch (e) {
console.error(e);
} finally {
await datasource.destroy();
}
}
// test実行確認用サンプル
// TODO:開発が進んだら削除すること
export async function testExample(datasource: DataSource): Promise<User[]> {
let users: User[];
const userRepository = datasource.getRepository(User); // Userエンティティに対応するリポジトリを取得
// ユーザーを検索
users = await userRepository.find();
return users;
}
// test実行確認用サンプル
// TODO:開発が進んだら削除すること
export async function testSendgridExample(): Promise<string> {
const sendgrid = new SendGridService();
// メールを送信
await sendgrid.sendMail(
"oura.a89@gmail.com",
process.env.MAIL_FROM,
"testMail",
"test!",
"html"
);
return "sucsess";
}
app.timer("timerTriggerExample", {
schedule: "0 */1 * * * *",
handler: timerTriggerExample,
});

View File

@ -1,25 +1,48 @@
import sendgrid from "@sendgrid/mail";
import { error } from "console";
export class SendGridService {
constructor() {
if (!process.env.SENDGRID_API_KEY) {
throw error;
}
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
}
/**
*
* ()
* @param accountId ID
* @param userId ID
* @param email
* @returns
*/
async createMailContent(
accountId: number,
userId: number,
email: string
): Promise<{ subject: string; text: string; html: string }> {
async createMailContentOfLicenseShortage(): Promise<{
subject: string;
text: string;
html: string;
}> {
return {
subject: "Verify your new account",
text: `The verification URL.`,
html: `<p>The verification URL.<p>`,
subject: "ライセンス在庫不足通知",
text: `ライセンス在庫不足通知:本文`,
html: `<p>ライセンス在庫不足通知:本文</p>`,
};
}
/**
* ()
* @param accountId ID
* @param userId ID
* @param email
* @returns
*/
async createMailContentOfLicenseExpiringSoon(): Promise<{
subject: string;
text: string;
html: string;
}> {
return {
subject: "ライセンス失効警告 ",
text: `ライセンス失効警告:本文`,
html: `<p>ライセンス失効警告:本文</p>`,
};
}

View File

@ -0,0 +1,198 @@
import { v4 as uuidv4 } from "uuid";
import { DataSource } from "typeorm";
import { User } from "../../entity/user.entity";
import { Account } from "../../entity/account.entity";
import { ADMIN_ROLES, USER_ROLES } from "../../constants";
import { License } from "../../entity/license.entity";
type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[];
tier2Accounts: { account: Account; users: User[] }[];
tier3Accounts: { account: Account; users: User[] }[];
tier4Accounts: { account: Account; users: User[] }[];
tier5Accounts: { account: Account; users: User[] }[];
};
// 上書きされたら困る項目を除外したAccount型
type OverrideAccount = Omit<
Account,
"id" | "primary_admin_user_id" | "secondary_admin_user_id" | "user"
>;
// 上書きされたら困る項目を除外したUser型
type OverrideUser = Omit<
User,
"id" | "account" | "license" | "userGroupMembers"
>;
type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] };
type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] };
/**
* ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する
* @param dataSource
* @param defaultUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
* @returns
*/
export const makeTestUser = async (
datasource: DataSource,
defaultUserValue?: UserDefault
): Promise<User> => {
const d = defaultUserValue;
const { identifiers } = await datasource.getRepository(User).insert({
account_id: d?.account_id ?? -1,
external_id: d?.external_id ?? uuidv4(),
role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`,
author_id: d?.author_id,
accepted_eula_version: d?.accepted_eula_version ?? "1.0",
accepted_dpa_version: d?.accepted_dpa_version ?? "1.0",
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
notification: d?.notification ?? true,
encryption: d?.encryption ?? true,
encryption_password: d?.encryption_password,
prompt: d?.prompt ?? true,
created_by: d?.created_by ?? "test_runner",
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? "updater",
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as User;
const user = await datasource.getRepository(User).findOne({
where: {
id: result.id,
},
});
if (!user) {
throw new Error("Unexpected null");
}
return user;
};
/**
* ユーティリティ: 指定したプロパティを上書きしたアカウントとその管理者ユーザーを作成する
* @param dataSource
* @param defaultUserValue Account型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
* @param defaultAdminUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト(account_id等の所属関係が破壊される上書きは無視する)
* @returns
*/
export const makeTestAccount = async (
datasource: DataSource,
defaultAccountValue?: AccountDefault,
defaultAdminUserValue?: UserDefault,
isPrimaryAdminNotExist?: boolean,
isSecondaryAdminNotExist?: boolean
): Promise<{ account: Account; admin: User }> => {
let accountId: number;
let userId: number;
{
const d = defaultAccountValue;
const { identifiers } = await datasource.getRepository(Account).insert({
tier: d?.tier ?? 1,
parent_account_id: d?.parent_account_id ?? undefined,
country: d?.country ?? "US",
delegation_permission: d?.delegation_permission ?? false,
locked: d?.locked ?? false,
company_name: d?.company_name ?? "test inc.",
verified: d?.verified ?? true,
deleted_at: d?.deleted_at ?? "",
created_by: d?.created_by ?? "test_runner",
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? "updater",
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as Account;
accountId = result.id;
}
{
const d = defaultAdminUserValue;
const { identifiers } = await datasource.getRepository(User).insert({
external_id: d?.external_id ?? uuidv4(),
account_id: accountId,
role: d?.role ?? "admin none",
author_id: d?.author_id ?? undefined,
accepted_eula_version: d?.accepted_eula_version ?? "1.0",
accepted_dpa_version: d?.accepted_dpa_version ?? "1.0",
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
notification: d?.notification ?? true,
encryption: d?.encryption ?? true,
encryption_password: d?.encryption_password ?? "password",
prompt: d?.prompt ?? true,
deleted_at: d?.deleted_at ?? "",
created_by: d?.created_by ?? "test_runner",
created_at: d?.created_at ?? new Date(),
updated_by: d?.updated_by ?? "updater",
updated_at: d?.updated_at ?? new Date(),
});
const result = identifiers.pop() as User;
userId = result.id;
}
// Accountの管理者を設定する
let secondaryAdminUserId: number | null = null;
if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) {
secondaryAdminUserId = userId;
}
await datasource.getRepository(Account).update(
{ id: accountId },
{
primary_admin_user_id: isPrimaryAdminNotExist ? null : userId,
secondary_admin_user_id: secondaryAdminUserId,
}
);
const account = await datasource.getRepository(Account).findOne({
where: {
id: accountId,
},
});
const admin = await datasource.getRepository(User).findOne({
where: {
id: userId,
},
});
if (!account || !admin) {
throw new Error("Unexpected null");
}
return {
account: account,
admin: admin,
};
};
export const createLicense = async (
datasource: DataSource,
licenseId: number,
expiry_date: Date | null,
accountId: number,
type: string,
status: string,
allocated_user_id: number | null,
order_id: number | null,
deleted_at: Date | null,
delete_order_id: number | null
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
expiry_date: expiry_date,
account_id: accountId,
type: type,
status: status,
allocated_user_id: allocated_user_id,
order_id: order_id,
deleted_at: deleted_at,
delete_order_id: delete_order_id,
created_by: "test_runner",
created_at: new Date(),
updated_by: "updater",
updated_at: new Date(),
});
identifiers.pop() as License;
};

View File

@ -0,0 +1,339 @@
import { DataSource } from "typeorm";
import { licenseAlertProcessing } from "../functions/licenseAlert";
import { makeTestAccount, createLicense } from "./common/utility";
import * as dotenv from "dotenv";
import {
DateWithDayEndTime,
DateWithZeroTime,
ExpirationThresholdDate,
NewTrialLicenseExpirationDate,
} from "../common/types/types";
import { AdB2cUser } from "../adb2c/types/types";
import { ADB2C_SIGN_IN_TYPE } from "../constants";
import { SendGridService } from "../sendgrid/sendgrid.service";
import { AdB2cService } from "../adb2c/adb2c.service";
import { InvocationContext } from "@azure/functions";
describe("licenseAlert", () => {
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: "sqlite",
database: ":memory:",
logging: false,
entities: [__dirname + "/../../**/*.entity{.ts,.js}"],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it("ライセンス在庫不足メールが送信され、ライセンス失効警告メールが送信されないこと", async () => {
if (!source) fail();
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
// 呼び出し回数でテスト成否を判定
const spyShortage = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseShortage"
);
const spyExpirySoon = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseExpiringSoon"
);
const spySend = jest.spyOn(sendgridMock, "sendMail");
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
const { account, admin } = await makeTestAccount(
source,
{ tier: 5 },
{ external_id: "external_id1" }
);
await createLicense(
source,
1,
expiringSoonDate,
account.id,
"STANDARD",
"Allocated",
admin.id,
null,
null,
null
);
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
expect(spyShortage.mock.calls).toHaveLength(1);
expect(spyExpirySoon.mock.calls).toHaveLength(0);
expect(spySend.mock.calls).toHaveLength(1);
});
it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => {
if (!source) fail();
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
// 呼び出し回数でテスト成否を判定
const spyShortage = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseShortage"
);
const spyExpirySoon = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseExpiringSoon"
);
const spySend = jest.spyOn(sendgridMock, "sendMail");
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new DateWithDayEndTime(currentDate.getTime());
const { account, admin } = await makeTestAccount(
source,
{ tier: 5 },
{ external_id: "external_id2", auto_renew: false }
);
await createLicense(
source,
1,
expiringSoonDate,
account.id,
"STANDARD",
"Allocated",
admin.id,
null,
null,
null
);
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
expect(spyShortage.mock.calls).toHaveLength(1);
expect(spyExpirySoon.mock.calls).toHaveLength(1);
expect(spySend.mock.calls).toHaveLength(2);
});
it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => {
if (!source) fail();
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
// 呼び出し回数でテスト成否を判定
const spyShortage = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseShortage"
);
const spyExpirySoon = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseExpiringSoon"
);
const spySend = jest.spyOn(sendgridMock, "sendMail");
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
const expiryDate = new NewTrialLicenseExpirationDate(currentDate.getTime());
const { account, admin } = await makeTestAccount(
source,
{ tier: 5 },
{ external_id: "external_id3" }
);
await createLicense(
source,
1,
expiringSoonDate,
account.id,
"STANDARD",
"Allocated",
admin.id,
null,
null,
null
);
await createLicense(
source,
2,
expiryDate,
account.id,
"STANDARD",
"Unallocated",
null,
null,
null,
null
);
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
expect(spyShortage.mock.calls).toHaveLength(0);
expect(spyExpirySoon.mock.calls).toHaveLength(0);
expect(spySend.mock.calls).toHaveLength(0);
});
it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => {
if (!source) fail();
const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
// 呼び出し回数でテスト成否を判定
const spyShortage = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseShortage"
);
const spyExpirySoon = jest.spyOn(
sendgridMock,
"createMailContentOfLicenseExpiringSoon"
);
const spySend = jest.spyOn(sendgridMock, "sendMail");
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new DateWithDayEndTime(currentDate.getTime());
const { account, admin } = await makeTestAccount(
source,
{ tier: 5 },
{ external_id: "external_id4", auto_renew: true }
);
await createLicense(
source,
1,
expiringSoonDate,
account.id,
"STANDARD",
"Allocated",
admin.id,
null,
null,
null
);
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
expect(spyShortage.mock.calls).toHaveLength(1);
expect(spyExpirySoon.mock.calls).toHaveLength(0);
expect(spySend.mock.calls).toHaveLength(1);
});
});
// テスト用sendgrid
export class SendGridServiceMock {
/**
* ()
* @param accountId ID
* @param userId ID
* @param email
* @returns
*/
async createMailContentOfLicenseShortage(): Promise<{
subject: string;
text: string;
html: string;
}> {
return {
subject: "ライセンス在庫不足通知",
text: `ライセンス在庫不足通知:本文`,
html: `<p>ライセンス在庫不足通知:本文</p>`,
};
}
/**
* ()
* @param accountId ID
* @param userId ID
* @param email
* @returns
*/
async createMailContentOfLicenseExpiringSoon(): Promise<{
subject: string;
text: string;
html: string;
}> {
return {
subject: "ライセンス失効警告",
text: `ライセンス失効警告:本文`,
html: `<p>ライセンス失効警告:本文</p>`,
};
}
/**
*
* @param to
* @param from
* @param subject
* @param text
* @param html
* @returns mail
*/
async sendMail(
to: string,
from: string,
subject: string,
text: string,
html: string
): Promise<void> {
return;
}
}
// テスト用adb2c
export class AdB2cServiceMock {
/**
* Azure AD B2Cからユーザ情報を取得する
* @param externalIds ID
* @returns
*/
async getUsers(externalIds: string[]): Promise<AdB2cUser[]> {
const AdB2cMockUsers: AdB2cUser[] = [
{
id: "external_id1",
displayName: "test1",
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: "issuer",
issuerAssignedId: "test1@mail.com",
},
],
},
{
id: "external_id2",
displayName: "test2",
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: "issuer",
issuerAssignedId: "test2@mail.com",
},
],
},
{
id: "external_id3",
displayName: "test3",
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: "issuer",
issuerAssignedId: "test3@mail.com",
},
],
},
{
id: "external_id4",
displayName: "test4",
identities: [
{
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
issuer: "issuer",
issuerAssignedId: "test4@mail.com",
},
],
},
];
return AdB2cMockUsers;
}
}

View File

@ -1,42 +0,0 @@
import { DataSource } from "typeorm";
import {
testExample,
testSendgridExample,
} from "../functions/timerTriggerExample";
import { makeTestUser } from "../common/test/utility";
import * as dotenv from "dotenv";
describe("timerTriggerExample", () => {
dotenv.config({ path: ".env.local", override: true });
let source: DataSource | null = null;
beforeEach(async () => {
source = new DataSource({
type: "sqlite",
database: ":memory:",
logging: false,
entities: [__dirname + "/../../**/*.entity{.ts,.js}"],
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
});
return source.initialize();
});
afterEach(async () => {
if (!source) return;
await source.destroy();
source = null;
});
it("sample test(DB)", async () => {
const count = 5;
for (let i = 0; i < count; i++) {
await makeTestUser(source);
}
const result = await testExample(source);
expect(result.length).toEqual(count);
});
it("sample test(sendgrid)", async () => {
await testSendgridExample();
});
});

View File

@ -8,6 +8,7 @@
"strict": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true
"esModuleInterop": true,
"strictNullChecks": true,
}
}

View File

@ -1 +1,3 @@
export const ADB2C_PREFIX = 'adb2c-external-id:';
export const IDTOKEN_PREFIX = 'id-token:';

View File

@ -1,4 +1,4 @@
import { ADB2C_PREFIX } from './constants';
import { ADB2C_PREFIX, IDTOKEN_PREFIX } from './constants';
/**
* ADB2Cのユーザー格納用のキーを生成する
@ -17,3 +17,12 @@ export const makeADB2CKey = (externalId: string): string => {
export const restoreAdB2cID = (key: string): string => {
return key.replace(ADB2C_PREFIX, '');
};
/**
* ADB2CのIDトークン格納用のキーを生成する
* @param idToken IDトークン
* @returns
*/
export const makeIDTokenKey = (idToken: string): string => {
return `${IDTOKEN_PREFIX}${idToken}`;
};

View File

@ -38,6 +38,8 @@ import { TermsService } from '../../features/terms/terms.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
import { TermsModule } from '../../features/terms/terms.module';
import { CacheModule } from '@nestjs/common';
import { RedisModule } from '../../gateways/redis/redis.module';
import { RedisService } from '../../gateways/redis/redis.service';
export const makeTestingModule = async (
datasource: DataSource,
@ -77,6 +79,7 @@ export const makeTestingModule = async (
SortCriteriaRepositoryModule,
WorktypesRepositoryModule,
TermsRepositoryModule,
RedisModule,
CacheModule.register({ isGlobal: true }),
],
providers: [
@ -90,6 +93,7 @@ export const makeTestingModule = async (
TemplatesService,
WorkflowsService,
TermsService,
RedisService,
],
})
.useMocker(async (token) => {

View File

@ -33,6 +33,8 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, TIERS } from '../../constants';
import jwt from 'jsonwebtoken';
import { AccessToken, RefreshToken } from '../../common/token';
import { makeIDTokenKey } from '../../common/cache';
import { RedisService } from '../../gateways/redis/redis.service';
@ApiTags('auth')
@Controller('auth')
@ -41,6 +43,7 @@ export class AuthController {
// TODO「タスク 1828: IDトークンを一度しか使えないようにする」で使用する予定
// private readonly redisService: RedisService,
private readonly authService: AuthService,
private readonly redisService: RedisService,
) {}
@Post('token')
@ -77,6 +80,18 @@ export class AuthController {
const context = makeContext(uuidv4());
const key = makeIDTokenKey(body.idToken);
const isTokenExists = await this.redisService.get<boolean>(key);
if (!isTokenExists) {
// IDトークンがキャッシュに存在しない場合(idTokenの有効期限をADB2Cの有効期限と合わせる(300秒))
await this.redisService.set(key, true, 300);
} else {
// IDトークンがキャッシュに存在する場合エラー
throw new HttpException(
makeErrorResponse('E000106'),
HttpStatus.UNAUTHORIZED,
);
}
// 同意済み利用規約バージョンが最新かチェック
const isAcceptedLatestVersion =
await this.authService.isAcceptedLatestVersion(context, idToken);

View File

@ -4,15 +4,10 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module';
import { RedisService } from '../../gateways/redis/redis.service';
@Module({
imports: [
ConfigModule,
AdB2cModule,
UsersRepositoryModule,
TermsRepositoryModule,
],
imports: [ConfigModule, AdB2cModule, UsersRepositoryModule],
controllers: [AuthController],
providers: [AuthService],
providers: [AuthService, RedisService],
})
export class AuthModule {}

View File

@ -886,11 +886,12 @@ export class AccountsRepositoryService {
where: {
id: primaryAdminUserId,
account_id: myAccountId,
email_verified: true,
},
});
if (!primaryAdminUser) {
throw new AdminUserNotFoundError(
`Primary admin user is not found. id: ${primaryAdminUserId}, account_id: ${myAccountId}`,
`Primary admin user is not found or email not verified. id: ${primaryAdminUserId}, account_id: ${myAccountId}`,
);
}
}
@ -901,11 +902,12 @@ export class AccountsRepositoryService {
where: {
id: secondryAdminUserId,
account_id: myAccountId,
email_verified: true,
},
});
if (!secondryAdminUser) {
throw new AdminUserNotFoundError(
`Secondry admin user is not found. id: ${secondryAdminUserId}, account_id: ${myAccountId}`,
`Secondary admin user is not found or email not verified. id: ${secondryAdminUserId}, account_id: ${myAccountId}`,
);
}
}