diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index 7abc3ca..68d22ed 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -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={} />} /> - } />} - /> - {/* XXX ヘッダーの挙動確認のため仮のページを作成 */} } />} diff --git a/dictation_client/src/components/auth/constants.ts b/dictation_client/src/components/auth/constants.ts index cf2b972..ef98041 100644 --- a/dictation_client/src/components/auth/constants.ts +++ b/dictation_client/src/components/auth/constants.ts @@ -39,6 +39,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [ "E010501", ]; +/** + * ローカルストレージに残すキー類 + * @const {string[]} + */ +export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"]; + /** * アクセストークンを更新する基準の秒数 * @const {number} diff --git a/dictation_client/src/components/delegate/index.tsx b/dictation_client/src/components/delegate/index.tsx index 1bfce9e..a4750de 100644 --- a/dictation_client/src/components/delegate/index.tsx +++ b/dictation_client/src/components/delegate/index.tsx @@ -46,6 +46,7 @@ export const DelegationBar: React.FC = (): JSX.Element => { alt="Exit" title="Exit" onClick={onClickExit} + data-tag="exit-delegation" /> ); diff --git a/dictation_client/src/components/header/constants.ts b/dictation_client/src/components/header/constants.ts index 66a2665..7dc14fe 100644 --- a/dictation_client/src/components/header/constants.ts +++ b/dictation_client/src/components/header/constants.ts @@ -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"); diff --git a/dictation_client/src/components/header/loginedHeader.tsx b/dictation_client/src/components/header/loginedHeader.tsx index 86f5e68..57ee5dd 100644 --- a/dictation_client/src/components/header/loginedHeader.tsx +++ b/dictation_client/src/components/header/loginedHeader.tsx @@ -87,6 +87,7 @@ const LoginedHeader: React.FC = (props: HeaderProps) => { ? styles.isActive : "" } + data-tag={`menu-${x.key}`} > {t(x.label)} @@ -101,6 +102,7 @@ const LoginedHeader: React.FC = (props: HeaderProps) => { diff --git a/dictation_client/src/components/header/types.ts b/dictation_client/src/components/header/types.ts index f80bbe9..622b15b 100644 --- a/dictation_client/src/components/header/types.ts +++ b/dictation_client/src/components/header/types.ts @@ -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"; diff --git a/dictation_client/src/components/header/utils.ts b/dictation_client/src/components/header/utils.ts index 937786e..10aa168 100644 --- a/dictation_client/src/components/header/utils.ts +++ b/dictation_client/src/components/header/utils.ts @@ -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 diff --git a/dictation_client/src/components/snackbar/index.tsx b/dictation_client/src/components/snackbar/index.tsx index 2f41161..0967d86 100644 --- a/dictation_client/src/components/snackbar/index.tsx +++ b/dictation_client/src/components/snackbar/index.tsx @@ -39,22 +39,34 @@ const Snackbar: React.FC = (props) => { const isShow = isOpen ? styles.isShow : ""; return ( - + {level === "error" ? ( - + ) : ( )} - {message} + + {message} + {level === "error" && ( @@ -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) => diff --git a/dictation_client/src/features/auth/utils.ts b/dictation_client/src/features/auth/utils.ts index edfd3c2..a296ff9 100644 --- a/dictation_client/src/features/auth/utils.ts +++ b/dictation_client/src/features/auth/utils.ts @@ -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); }; diff --git a/dictation_client/src/features/login/operations.ts b/dictation_client/src/features/login/operations.ts index ffbf4e9..08a3a06 100644 --- a/dictation_client/src/features/login/operations.ts +++ b/dictation_client/src/features/login/operations.ts @@ -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に変換" diff --git a/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx b/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx index 2fcb13c..54aa4d2 100644 --- a/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx +++ b/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx @@ -56,7 +56,11 @@ export const DeleteAccountPopup: React.FC = ( {t(getTranslationID("deleteAccountPopup.label.title"))} - + @@ -92,6 +96,7 @@ export const DeleteAccountPopup: React.FC = ( )} className={`${styles.formDelete} ${styles.marginBtm1} ${styles.isActive}`} onClick={onDeleteAccount} + data-tag="delete-account" /> = ( )} className={`${styles.formButtonTx} ${styles.marginBtm1}`} onClick={closePopup} + data-tag="cancel-delete-account" /> diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx index 924048c..79963ae 100644 --- a/dictation_client/src/pages/AccountPage/index.tsx +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -364,6 +364,7 @@ const AccountPage: React.FC = (): JSX.Element => { } `} onClick={onSaveChangesButton} + data-tag="savechanges-account" /> { {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} - + {t(getTranslationID("accountPage.label.deleteAccount"))} diff --git a/dictation_client/src/pages/LoginPage/index.tsx b/dictation_client/src/pages/LoginPage/index.tsx index e395607..e298c40 100644 --- a/dictation_client/src/pages/LoginPage/index.tsx +++ b/dictation_client/src/pages/LoginPage/index.tsx @@ -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] diff --git a/dictation_client/src/pages/SamplePage/index.tsx b/dictation_client/src/pages/SamplePage/index.tsx deleted file mode 100644 index b048ff6..0000000 --- a/dictation_client/src/pages/SamplePage/index.tsx +++ /dev/null @@ -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 ( - - - - - { - instance.logoutRedirect({ postLogoutRedirectUri: "/" }); - dispatch(clearToken()); - dispatch(clearUserInfo()); - }} - > - sign out - - - - - ); -}; - -export default SamplePage; diff --git a/dictation_client/src/pages/TermsPage/index.tsx b/dictation_client/src/pages/TermsPage/index.tsx index 586b029..cb45b5d 100644 --- a/dictation_client/src/pages/TermsPage/index.tsx +++ b/dictation_client/src/pages/TermsPage/index.tsx @@ -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"))} @@ -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"))} @@ -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" /> diff --git a/dictation_client/src/pages/TopPage/index.tsx b/dictation_client/src/pages/TopPage/index.tsx index 20ab130..1a4e435 100644 --- a/dictation_client/src/pages/TopPage/index.tsx +++ b/dictation_client/src/pages/TopPage/index.tsx @@ -86,6 +86,7 @@ const TopPage: React.FC = (): JSX.Element => { }; instance.loginRedirect(loginRequest); }} + data-tag="signin" > {t(getTranslationID("topPage.label.signInButton"))} =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": { diff --git a/dictation_function/package.json b/dictation_function/package.json index 7f208f9..02589f6 100644 --- a/dictation_function/package.json +++ b/dictation_function/package.json @@ -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", diff --git a/dictation_function/src/adb2c/adb2c.service.ts b/dictation_function/src/adb2c/adb2c.service.ts new file mode 100644 index 0000000..8caa96c --- /dev/null +++ b/dictation_function/src/adb2c/adb2c.service.ts @@ -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 { + 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; +}; diff --git a/dictation_function/src/adb2c/types/types.ts b/dictation_function/src/adb2c/types/types.ts new file mode 100644 index 0000000..a7261ef --- /dev/null +++ b/dictation_function/src/adb2c/types/types.ts @@ -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; +}; diff --git a/dictation_function/src/common/cache/constants.ts b/dictation_function/src/common/cache/constants.ts new file mode 100644 index 0000000..da6c13e --- /dev/null +++ b/dictation_function/src/common/cache/constants.ts @@ -0,0 +1 @@ +export const ADB2C_PREFIX = "adb2c-external-id:" \ No newline at end of file diff --git a/dictation_function/src/common/cache/index.ts b/dictation_function/src/common/cache/index.ts new file mode 100644 index 0000000..067be25 --- /dev/null +++ b/dictation_function/src/common/cache/index.ts @@ -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, ''); +} \ No newline at end of file diff --git a/dictation_function/src/common/getEnv/getEnv.ts b/dictation_function/src/common/getEnv/getEnv.ts new file mode 100644 index 0000000..7a8a738 --- /dev/null +++ b/dictation_function/src/common/getEnv/getEnv.ts @@ -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"); +}; diff --git a/dictation_function/src/common/test/utility.ts b/dictation_function/src/common/test/utility.ts deleted file mode 100644 index 768eec5..0000000 --- a/dictation_function/src/common/test/utility.ts +++ /dev/null @@ -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 => { - 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; -}; diff --git a/dictation_function/src/common/types/types.ts b/dictation_function/src/common/types/types.ts new file mode 100644 index 0000000..88ff9a4 --- /dev/null +++ b/dictation_function/src/common/types/types.ts @@ -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); + } +} diff --git a/dictation_function/src/entity/account.entity.ts b/dictation_function/src/entity/account.entity.ts index 2463d5e..a8fa438 100644 --- a/dictation_function/src/entity/account.entity.ts +++ b/dictation_function/src/entity/account.entity.ts @@ -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; } diff --git a/dictation_function/src/entity/license.entity.ts b/dictation_function/src/entity/license.entity.ts new file mode 100644 index 0000000..2f32ae3 --- /dev/null +++ b/dictation_function/src/entity/license.entity.ts @@ -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; +} diff --git a/dictation_function/src/entity/user.entity.ts b/dictation_function/src/entity/user.entity.ts index 9a9b26d..573a017 100644 --- a/dictation_function/src/entity/user.entity.ts +++ b/dictation_function/src/entity/user.entity.ts @@ -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; } diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts new file mode 100644 index 0000000..d5d715a --- /dev/null +++ b/dictation_function/src/functions/licenseAlert.ts @@ -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 { + 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; +} diff --git a/dictation_function/src/functions/timerTriggerExample.ts b/dictation_function/src/functions/timerTriggerExample.ts deleted file mode 100644 index 8374607..0000000 --- a/dictation_function/src/functions/timerTriggerExample.ts +++ /dev/null @@ -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 { - 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 { - let users: User[]; - const userRepository = datasource.getRepository(User); // Userエンティティに対応するリポジトリを取得 - - // ユーザーを検索 - users = await userRepository.find(); - return users; -} - -// test実行確認用サンプル -// TODO:開発が進んだら削除すること -export async function testSendgridExample(): Promise { - 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, -}); diff --git a/dictation_function/src/sendgrid/sendgrid.service.ts b/dictation_function/src/sendgrid/sendgrid.service.ts index a5bed4c..2fd9a7a 100644 --- a/dictation_function/src/sendgrid/sendgrid.service.ts +++ b/dictation_function/src/sendgrid/sendgrid.service.ts @@ -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: `The verification URL.`, + subject: "ライセンス在庫不足通知", + text: `ライセンス在庫不足通知:本文`, + html: `ライセンス在庫不足通知:本文`, + }; + } + + /** + * メールコンテンツを作成する(ライセンス不足) + * @param accountId 認証対象のユーザーが所属するアカウントのID + * @param userId 認証対象のユーザーのID + * @param email 認証対象のユーザーのメールアドレス + * @returns メールのサブジェクトとコンテンツ + */ + async createMailContentOfLicenseExpiringSoon(): Promise<{ + subject: string; + text: string; + html: string; + }> { + return { + subject: "ライセンス失効警告 ", + text: `ライセンス失効警告:本文`, + html: `ライセンス失効警告:本文`, }; } diff --git a/dictation_function/src/test/common/utility.ts b/dictation_function/src/test/common/utility.ts new file mode 100644 index 0000000..cce735a --- /dev/null +++ b/dictation_function/src/test/common/utility.ts @@ -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 => { + 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 => { + 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; +}; diff --git a/dictation_function/src/test/licenseAlert.spec.ts b/dictation_function/src/test/licenseAlert.spec.ts new file mode 100644 index 0000000..c94ec14 --- /dev/null +++ b/dictation_function/src/test/licenseAlert.spec.ts @@ -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: `ライセンス在庫不足通知:本文`, + }; + } + + /** + * メールコンテンツを作成する(ライセンス不足) + * @param accountId 認証対象のユーザーが所属するアカウントのID + * @param userId 認証対象のユーザーのID + * @param email 認証対象のユーザーのメールアドレス + * @returns メールのサブジェクトとコンテンツ + */ + async createMailContentOfLicenseExpiringSoon(): Promise<{ + subject: string; + text: string; + html: string; + }> { + return { + subject: "ライセンス失効警告", + text: `ライセンス失効警告:本文`, + html: `ライセンス失効警告:本文`, + }; + } + + /** + * メールを送信する + * @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 { + return; + } +} + +// テスト用adb2c +export class AdB2cServiceMock { + /** + * Azure AD B2Cからユーザ情報を取得する + * @param externalIds 外部ユーザーID + * @returns ユーザ情報 + */ + async getUsers(externalIds: string[]): Promise { + 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; + } +} diff --git a/dictation_function/src/test/timerTriggerExample.spec.ts b/dictation_function/src/test/timerTriggerExample.spec.ts deleted file mode 100644 index 5d30ff9..0000000 --- a/dictation_function/src/test/timerTriggerExample.spec.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/dictation_function/tsconfig.json b/dictation_function/tsconfig.json index 4e3609a..dd9c7a4 100644 --- a/dictation_function/tsconfig.json +++ b/dictation_function/tsconfig.json @@ -8,6 +8,7 @@ "strict": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "esModuleInterop": true + "esModuleInterop": true, + "strictNullChecks": true, } } \ No newline at end of file diff --git a/dictation_server/src/common/cache/constants.ts b/dictation_server/src/common/cache/constants.ts index b79dd78..9f588f6 100644 --- a/dictation_server/src/common/cache/constants.ts +++ b/dictation_server/src/common/cache/constants.ts @@ -1 +1,3 @@ export const ADB2C_PREFIX = 'adb2c-external-id:'; + +export const IDTOKEN_PREFIX = 'id-token:'; diff --git a/dictation_server/src/common/cache/index.ts b/dictation_server/src/common/cache/index.ts index 3355a54..4985492 100644 --- a/dictation_server/src/common/cache/index.ts +++ b/dictation_server/src/common/cache/index.ts @@ -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}`; +}; diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 9c3d578..61c0396 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -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) => { diff --git a/dictation_server/src/features/auth/auth.controller.ts b/dictation_server/src/features/auth/auth.controller.ts index a07b666..32d7697 100644 --- a/dictation_server/src/features/auth/auth.controller.ts +++ b/dictation_server/src/features/auth/auth.controller.ts @@ -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(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); diff --git a/dictation_server/src/features/auth/auth.module.ts b/dictation_server/src/features/auth/auth.module.ts index 2e3dc4c..d86bf30 100644 --- a/dictation_server/src/features/auth/auth.module.ts +++ b/dictation_server/src/features/auth/auth.module.ts @@ -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 {} diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 65fe99d..8380461 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -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}`, ); } }
{message}
+ {message} +
{t(getTranslationID("deleteAccountPopup.label.title"))} - +
= ( )} className={`${styles.formButtonTx} ${styles.marginBtm1}`} onClick={closePopup} + data-tag="cancel-delete-account" />
The verification URL.
`, + subject: "ライセンス在庫不足通知", + text: `ライセンス在庫不足通知:本文`, + html: `
ライセンス在庫不足通知:本文
ライセンス失効警告:本文