diff --git a/DockerfileFunctionDictation.dockerfile b/DockerfileFunctionDictation.dockerfile index 3dcb7b2..4b3356f 100644 --- a/DockerfileFunctionDictation.dockerfile +++ b/DockerfileFunctionDictation.dockerfile @@ -1,12 +1,29 @@ -# To enable ssh & remote debugging on app service change the base image to the one below -# FROM mcr.microsoft.com/azure-functions/node:4-node18-appservice +#ビルドイメージ +FROM node:18.17.1-buster AS build-container +WORKDIR /app +RUN mkdir dictation_function +COPY dictation_function/ dictation_function/ +RUN npm install --force -g n && n 18.17.1 \ + && cd dictation_function \ + && npm ci \ + && npm run build \ + && cd .. + +# 成果物イメージ FROM mcr.microsoft.com/azure-functions/node:4-node18 +WORKDIR /home/site/wwwroot +RUN mkdir build \ + && mkdir dist \ + && mkdir node_modules + +COPY --from=build-container app/dictation_function/dist/ dist/ +COPY --from=build-container app/dictation_function/node_modules/ node_modules/ +COPY --from=build-container app/dictation_function/.env ./ +COPY --from=build-container app/dictation_function/host.json ./ +COPY --from=build-container app/dictation_function/package.json ./ + +ARG BUILD_VERSION ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ - AzureFunctionsJobHost__Logging__Console__IsEnabled=true - -COPY . /home/site/wwwroot - -RUN cd /home/site/wwwroot && \ - npm install && \ - npm run build \ No newline at end of file + AzureFunctionsJobHost__Logging__Console__IsEnabled=true \ + BUILD_VERSION=${BUILD_VERSION} \ No newline at end of file diff --git a/azure-pipelines-production.yml b/azure-pipelines-production.yml index ff18972..ef0faa8 100644 --- a/azure-pipelines-production.yml +++ b/azure-pipelines-production.yml @@ -83,9 +83,24 @@ jobs: is_static_export: false verbose: false azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN) -- job: smoke_test +- job: function_deploy dependsOn: frontend_deploy condition: succeeded('frontend_deploy') + displayName: function Deploy + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureFunctionAppContainer@1 + inputs: + azureSubscription: 'omds-service-connection-prod' + appName: 'func-odms-dictation-prod' + imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)' +- job: smoke_test + dependsOn: function_deploy + condition: succeeded('function_deploy') displayName: 'smoke test' pool: name: odms-deploy-pipeline diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index 68d22ed..802e008 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -23,6 +23,7 @@ import AccountPage from "pages/AccountPage"; import AcceptToUsePage from "pages/TermsPage"; import { TemplateFilePage } from "pages/TemplateFilePage"; import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess"; +import SupportPage from "pages/SupportPage"; const AppRouter: React.FC = () => ( @@ -81,6 +82,10 @@ const AppRouter: React.FC = () => ( element={} />} /> } /> + } />} + /> } /> diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 2b5689d..1c1ac87 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -447,6 +447,12 @@ export interface CreateAccountRequest { * @memberof CreateAccountRequest */ 'acceptedEulaVersion': string; + /** + * 同意済みプライバシーポリシーのバージョン + * @type {string} + * @memberof CreateAccountRequest + */ + 'acceptedPrivacyNoticeVersion': string; /** * 同意済み利用規約のバージョン(DPA) * @type {string} @@ -746,6 +752,32 @@ export interface GetAuthorsResponse { */ 'authors': Array; } +/** + * + * @export + * @interface GetCompanyNameRequest + */ +export interface GetCompanyNameRequest { + /** + * + * @type {number} + * @memberof GetCompanyNameRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetCompanyNameResponse + */ +export interface GetCompanyNameResponse { + /** + * + * @type {string} + * @memberof GetCompanyNameResponse + */ + 'companyName': string; +} /** * * @export @@ -2000,6 +2032,12 @@ export interface UpdateAcceptedVersionRequest { * @memberof UpdateAcceptedVersionRequest */ 'acceptedEULAVersion': string; + /** + * 更新バージョン(PrivacyNotice) + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'acceptedPrivacyNoticeVersion': string; /** * 更新バージョン(DPA) * @type {string} @@ -2727,6 +2765,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCompanyName: async (getCompanyNameRequest: GetCompanyNameRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getCompanyNameRequest' is not null or undefined + assertParamExists('getCompanyName', 'getCompanyNameRequest', getCompanyNameRequest) + const localVarPath = `/accounts/company-name`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getCompanyNameRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3488,6 +3566,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.getAuthors']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCompanyName(getCompanyNameRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getCompanyName']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -3804,6 +3895,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP getAuthors(options?: any): AxiosPromise { return localVarFp.getAuthors(options).then((request) => request(axios, basePath)); }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: any): AxiosPromise { + return localVarFp.getCompanyName(getCompanyNameRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4092,6 +4193,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).getAuthors(options).then((request) => request(this.axios, this.basePath)); } + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getCompanyName(getCompanyNameRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/components/header/constants.ts b/dictation_client/src/components/header/constants.ts index 7dc14fe..f3e4f53 100644 --- a/dictation_client/src/components/header/constants.ts +++ b/dictation_client/src/components/header/constants.ts @@ -7,6 +7,7 @@ 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_SUPPORT = "Support"; export const HEADER_MENUS: { key: HeaderMenus; @@ -43,6 +44,11 @@ export const HEADER_MENUS: { label: getTranslationID("common.label.headerPartners"), path: "/partners", }, + { + key: HEADER_MENUS_SUPPORT, + label: getTranslationID("common.label.headerSupport"), + path: "/support", + }, ]; export const HEADER_NAME = getTranslationID("common.label.headerName"); diff --git a/dictation_client/src/components/header/types.ts b/dictation_client/src/components/header/types.ts index 622b15b..4170c3a 100644 --- a/dictation_client/src/components/header/types.ts +++ b/dictation_client/src/components/header/types.ts @@ -8,7 +8,8 @@ export type HeaderMenus = | "License" | "Dictations" | "Workflow" - | "Partners"; + | "Partners" + | "Support"; // ログイン後に遷移しうるパス export type LoginedPaths = @@ -17,4 +18,5 @@ export type LoginedPaths = | "/license" | "/dictations" | "/workflow" - | "/partners"; + | "/partners" + | "/support"; diff --git a/dictation_client/src/components/header/utils.ts b/dictation_client/src/components/header/utils.ts index 10aa168..a515b5b 100644 --- a/dictation_client/src/components/header/utils.ts +++ b/dictation_client/src/components/header/utils.ts @@ -20,6 +20,7 @@ export const isLoginPaths = (d: string): d is LoginedPaths => { case "/dictations": case "/workflow": case "/partners": + case "/support": return true; default: { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index c51fd3d..c434639 100644 --- a/dictation_client/src/features/dictation/dictationSlice.ts +++ b/dictation_client/src/features/dictation/dictationSlice.ts @@ -131,6 +131,10 @@ export const dictationSlice = createSlice({ }); state.domain.backup.tasks = tasks; }, + openFilePropertyInfo: (state, action: PayloadAction<{ task: Task }>) => { + const { task } = action.payload; + state.apps.selectedFileTask = task; + }, }, extraReducers: (builder) => { builder.addCase(listTasksAsync.pending, (state) => { @@ -225,6 +229,7 @@ export const { changeAssignee, changeBackupTaskChecked, changeBackupTaskAllCheched, + openFilePropertyInfo, } = dictationSlice.actions; export default dictationSlice.reducer; diff --git a/dictation_client/src/features/dictation/selectors.ts b/dictation_client/src/features/dictation/selectors.ts index 12cda85..3b6f494 100644 --- a/dictation_client/src/features/dictation/selectors.ts +++ b/dictation_client/src/features/dictation/selectors.ts @@ -34,6 +34,9 @@ export const selectParamName = (state: RootState) => export const selectSelectedTask = (state: RootState) => state.dictation.apps.selectedTask; +export const selectSelectedFileTask = (state: RootState) => + state.dictation.apps.selectedFileTask; + export const selectSelectedTranscriptionists = (state: RootState) => state.dictation.apps.assignee.selected; diff --git a/dictation_client/src/features/dictation/state.ts b/dictation_client/src/features/dictation/state.ts index f8243ef..2f3e288 100644 --- a/dictation_client/src/features/dictation/state.ts +++ b/dictation_client/src/features/dictation/state.ts @@ -26,6 +26,7 @@ export interface Apps { direction: DirectionType; paramName: SortableColumnType; selectedTask?: Task; + selectedFileTask?: Task; assignee: { selected: Assignee[]; pool: Assignee[]; diff --git a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts index 3888909..e9d9816 100644 --- a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts +++ b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts @@ -1,20 +1,25 @@ import { createSlice } from "@reduxjs/toolkit"; import { LicenseSummaryState } from "./state"; -import { getLicenseSummaryAsync } from "./operations"; +import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations"; const initialState: LicenseSummaryState = { domain: { - totalLicense: 0, - allocatedLicense: 0, - reusableLicense: 0, - freeLicense: 0, - expiringWithin14daysLicense: 0, - issueRequesting: 0, - numberOfRequesting: 0, - shortage: 0, - storageSize: 0, - usedSize: 0, - isStorageAvailable: false, + licenseSummaryInfo: { + totalLicense: 0, + allocatedLicense: 0, + reusableLicense: 0, + freeLicense: 0, + expiringWithin14daysLicense: 0, + issueRequesting: 0, + numberOfRequesting: 0, + shortage: 0, + storageSize: 0, + usedSize: 0, + isStorageAvailable: false, + }, + accountInfo: { + companyName: "", + }, }, apps: { isLoading: false, @@ -31,7 +36,10 @@ export const licenseSummarySlice = createSlice({ }, extraReducers: (builder) => { builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => { - state.domain = action.payload; + state.domain.licenseSummaryInfo = action.payload; + }); + builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => { + state.domain.accountInfo.companyName = action.payload.companyName; }); }, }); diff --git a/dictation_client/src/features/license/licenseSummary/operations.ts b/dictation_client/src/features/license/licenseSummary/operations.ts index b88df82..a5f18f8 100644 --- a/dictation_client/src/features/license/licenseSummary/operations.ts +++ b/dictation_client/src/features/license/licenseSummary/operations.ts @@ -5,6 +5,7 @@ import { openSnackbar } from "features/ui/uiSlice"; import { getAccessToken } from "features/auth"; import { AccountsApi, + GetCompanyNameResponse, GetLicenseSummaryResponse, PartnerLicenseInfo, } from "../../../api/api"; @@ -66,3 +67,59 @@ export const getLicenseSummaryAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const getCompanyNameAsync = createAsyncThunk< + // 正常時の戻り値の型 + GetCompanyNameResponse, + // 引数 + { selectedRow?: PartnerLicenseInfo }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("licenses/getCompanyNameAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + try { + const getMyAccountResponse = await accountsApi.getMyAccount({ + headers: { authorization: `Bearer ${accessToken}` }, + }); + + const { selectedRow } = args; + // 引数がない場合は自分のアカウントID取得 + const accountId = + selectedRow?.accountId ?? getMyAccountResponse?.data?.account?.accountId; + + if (accountId !== undefined) { + const getCompanyNameResponse = await accountsApi.getCompanyName( + { accountId }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + return getCompanyNameResponse.data; + } + throw new Error("accountId is undefined"); + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + const errorMessage = getTranslationID("common.message.internalServerError"); + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/license/licenseSummary/selectors.ts b/dictation_client/src/features/license/licenseSummary/selectors.ts index 87df71f..79ba5e9 100644 --- a/dictation_client/src/features/license/licenseSummary/selectors.ts +++ b/dictation_client/src/features/license/licenseSummary/selectors.ts @@ -2,6 +2,9 @@ import { RootState } from "app/store"; // 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する export const selecLicenseSummaryInfo = (state: RootState) => - state.licenseSummary.domain; + state.licenseSummary.domain.licenseSummaryInfo; + +export const selectCompanyName = (state: RootState) => + state.licenseSummary.domain.accountInfo.companyName; export const selectIsLoading = (state: RootState) => state.license; diff --git a/dictation_client/src/features/license/licenseSummary/state.ts b/dictation_client/src/features/license/licenseSummary/state.ts index 6402581..2155005 100644 --- a/dictation_client/src/features/license/licenseSummary/state.ts +++ b/dictation_client/src/features/license/licenseSummary/state.ts @@ -4,17 +4,22 @@ export interface LicenseSummaryState { } export interface Domain { - totalLicense: number; - allocatedLicense: number; - reusableLicense: number; - freeLicense: number; - expiringWithin14daysLicense: number; - issueRequesting: number; - numberOfRequesting: number; - shortage: number; - storageSize: number; - usedSize: number; - isStorageAvailable: boolean; + licenseSummaryInfo: { + totalLicense: number; + allocatedLicense: number; + reusableLicense: number; + freeLicense: number; + expiringWithin14daysLicense: number; + issueRequesting: number; + numberOfRequesting: number; + shortage: number; + storageSize: number; + usedSize: number; + isStorageAvailable: boolean; + }; + accountInfo: { + companyName: string; + }; } export interface Apps { diff --git a/dictation_client/src/features/terms/constants.ts b/dictation_client/src/features/terms/constants.ts index dd78e43..e9cbd2f 100644 --- a/dictation_client/src/features/terms/constants.ts +++ b/dictation_client/src/features/terms/constants.ts @@ -5,4 +5,5 @@ export const TERMS_DOCUMENT_TYPE = { DPA: "DPA", EULA: "EULA", + PRIVACY_NOTICE: "PrivacyNotice", } as const; diff --git a/dictation_client/src/features/terms/operations.ts b/dictation_client/src/features/terms/operations.ts index 876bb19..f9a1a0d 100644 --- a/dictation_client/src/features/terms/operations.ts +++ b/dictation_client/src/features/terms/operations.ts @@ -110,6 +110,7 @@ export const updateAcceptedVersionAsync = createAsyncThunk< updateAccceptVersions: { acceptedVerDPA: string; acceptedVerEULA: string; + acceptedVerPrivacyNotice: string; }; }, { @@ -140,6 +141,8 @@ export const updateAcceptedVersionAsync = createAsyncThunk< { idToken, acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA, + acceptedPrivacyNoticeVersion: + updateAccceptVersions.acceptedVerPrivacyNotice, acceptedDPAVersion: !(TIERS.TIER5 === tier.toString()) ? updateAccceptVersions.acceptedVerDPA : undefined, diff --git a/dictation_client/src/features/terms/selectors.ts b/dictation_client/src/features/terms/selectors.ts index 5cd00f7..75f45b9 100644 --- a/dictation_client/src/features/terms/selectors.ts +++ b/dictation_client/src/features/terms/selectors.ts @@ -14,7 +14,11 @@ export const selectTermVersions = (state: RootState) => { (termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA )?.version || ""; - return { acceptedVerDPA, acceptedVerEULA }; + const acceptedVerPrivacyNotice = + termsInfo.find( + (termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.PRIVACY_NOTICE + )?.version || ""; + return { acceptedVerDPA, acceptedVerEULA, acceptedVerPrivacyNotice }; }; export const selectTier = (state: RootState) => state.terms.domain.tier; diff --git a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx new file mode 100644 index 0000000..639d264 --- /dev/null +++ b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx @@ -0,0 +1,126 @@ +import React, { useCallback } from "react"; +import styles from "styles/app.module.scss"; +import { useSelector } from "react-redux"; +import { selectSelectedFileTask, selectIsLoading } from "features/dictation"; +import { getTranslationID } from "translation"; +import { useTranslation } from "react-i18next"; +import close from "../../assets/images/close.svg"; +import lock from "../../assets/images/lock.svg"; + +interface FilePropertyPopupProps { + onClose: (isChanged: boolean) => void; + isOpen: boolean; +} + +export const FilePropertyPopup: React.FC = (props) => { + const { onClose, isOpen } = props; + const [t] = useTranslation(); + const isLoading = useSelector(selectIsLoading); + + // ポップアップを閉じる処理 + const closePopup = useCallback(() => { + onClose(false); + }, [onClose]); + const selectedFileTask = useSelector(selectSelectedFileTask); + + return ( +
+
+

+ {t(getTranslationID("dictationPage.label.fileProperty"))} + +

+
+
+ {t(getTranslationID("dictationPage.label.general"))} +
+
{t(getTranslationID("dictationPage.label.fileName"))}
+
{selectedFileTask?.fileName.replace(".zip", "") ?? ""}
+
{t(getTranslationID("dictationPage.label.fileSize"))}
+
{selectedFileTask?.fileSize ?? ""}
+
{t(getTranslationID("dictationPage.label.fileLength"))}
+
{selectedFileTask?.audioDuration ?? ""}
+
{t(getTranslationID("dictationPage.label.authorId"))}
+
{selectedFileTask?.authorId ?? ""}
+
{t(getTranslationID("dictationPage.label.workType"))}
+
{selectedFileTask?.workType ?? ""}
+
{t(getTranslationID("dictationPage.label.priority"))}
+
{selectedFileTask?.priority ?? ""}
+
+ {t(getTranslationID("dictationPage.label.recordingStartedDate"))} +
+
{selectedFileTask?.audioCreatedDate ?? ""}
+
+ {t(getTranslationID("dictationPage.label.recordingFinishedDate"))} +
+
{selectedFileTask?.audioFinishedDate ?? ""}
+
{t(getTranslationID("dictationPage.label.uploadDate"))}
+
{selectedFileTask?.audioUploadedDate ?? ""}
+
{t(getTranslationID("dictationPage.label.encryption"))}
+
+ {selectedFileTask?.isEncrypted ? ( + encrypted + ) : ( + "" + )} +
+
{t(getTranslationID("dictationPage.label.optionItem1"))}
+
{selectedFileTask?.optionItemList[0].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem2"))}
+
{selectedFileTask?.optionItemList[1].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem3"))}
+
{selectedFileTask?.optionItemList[2].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem4"))}
+
{selectedFileTask?.optionItemList[3].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem5"))}
+
{selectedFileTask?.optionItemList[4].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem6"))}
+
{selectedFileTask?.optionItemList[5].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem7"))}
+
{selectedFileTask?.optionItemList[6].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem8"))}
+
{selectedFileTask?.optionItemList[7].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem9"))}
+
{selectedFileTask?.optionItemList[8].optionItemValue}
+
{t(getTranslationID("dictationPage.label.optionItem10"))}
+
{selectedFileTask?.optionItemList[9].optionItemValue}
+
{t(getTranslationID("dictationPage.label.comment"))}
+
{selectedFileTask?.comment ?? ""}
+
+ {t(getTranslationID("dictationPage.label.job"))} +
+
{t(getTranslationID("dictationPage.label.jobNumber"))}
+
{selectedFileTask?.jobNumber ?? ""}
+
{t(getTranslationID("dictationPage.label.status"))}
+
{selectedFileTask?.status ?? ""}
+
+ {t( + getTranslationID("dictationPage.label.transcriptionStartedDate") + )} +
+
{selectedFileTask?.transcriptionStartedDate ?? ""}
+
+ {t( + getTranslationID("dictationPage.label.transcriptionFinishedDate") + )} +
+
{selectedFileTask?.transcriptionFinishedDate ?? ""}
+
{t(getTranslationID("dictationPage.label.transcriptionist"))}
+
{selectedFileTask?.typist?.name ?? ""}
+
+ + close + {t(getTranslationID("dictationPage.label.close"))} + +
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index 320993a..8cfe9b0 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -23,6 +23,7 @@ import { changeParamName, changeDirection, changeSelectedTask, + openFilePropertyInfo, SortableColumnType, changeAssignee, listTypistsAsync, @@ -48,6 +49,7 @@ import open_in_new from "../../assets/images/open_in_new.svg"; import { DisPlayInfo } from "./displayInfo"; import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup"; import { BackupPopup } from "./backupPopup"; +import { FilePropertyPopup } from "./filePropertyPopup"; const DictationPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -63,6 +65,7 @@ const DictationPage: React.FC = (): JSX.Element => { isChangeTranscriptionistPopupOpen, setIsChangeTranscriptionistPopupOpen, ] = useState(false); + const [isFilePropertyPopupOpen, setIsFilePropertyPopupOpen] = useState(false); const [isBackupPopupOpen, setIsBackupPopupOpen] = useState(false); const onChangeTranscriptionistPopupOpen = useCallback( @@ -74,6 +77,13 @@ const DictationPage: React.FC = (): JSX.Element => { [dispatch, setIsChangeTranscriptionistPopupOpen] ); + const onClickFileProperty = useCallback( + (task: Task) => { + dispatch(openFilePropertyInfo({ task })); + setIsFilePropertyPopupOpen(true); + }, + [dispatch, setIsFilePropertyPopupOpen] + ); // 各カラムの表示/非表示 const displayColumn = useSelector(selectDisplayInfo); @@ -477,6 +487,10 @@ const DictationPage: React.FC = (): JSX.Element => { setIsBackupPopupOpen(true); }, []); + const onCloseFilePropertyPopup = useCallback(() => { + setIsFilePropertyPopupOpen(false); + }, []); + const sortIconClass = ( currentParam: SortableColumnType, currentDirection: DirectionType, @@ -532,6 +546,10 @@ const DictationPage: React.FC = (): JSX.Element => { return ( <> + {
  • - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + onClickFileProperty(x)}> {t( getTranslationID( "dictationPage.label.fileProperty" @@ -1360,11 +1379,12 @@ const DictationPage: React.FC = (): JSX.Element => {
    • - Applications + {t(getTranslationID("dictationPage.label.applications"))} = ( // apiからの値取得関係 const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo); + const companyName = useSelector(selectCompanyName); useEffect(() => { dispatch(getLicenseSummaryAsync({ selectedRow })); + dispatch(getCompanyNameAsync({ selectedRow })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch]); @@ -118,7 +122,7 @@ export const LicenseSummary: React.FC = (
      -

      {"会社名" /* TODO 会社名を表示する */}

      +

      {companyName}

      • {/* 他アカウントのライセンス情報を見ている場合は、前画面に戻る用のreturnボタンを表示 */} diff --git a/dictation_client/src/pages/SignupPage/signupConfirm.tsx b/dictation_client/src/pages/SignupPage/signupConfirm.tsx index 698e1b6..b4e6a26 100644 --- a/dictation_client/src/pages/SignupPage/signupConfirm.tsx +++ b/dictation_client/src/pages/SignupPage/signupConfirm.tsx @@ -40,6 +40,7 @@ const SignupConfirm: React.FC = (): JSX.Element => { adminMail, adminPassword, acceptedEulaVersion, + acceptedPrivacyNoticeVersion: "", acceptedDpaVersion: "", token: "", }) diff --git a/dictation_client/src/pages/SupportPage/index.tsx b/dictation_client/src/pages/SupportPage/index.tsx new file mode 100644 index 0000000..b5d2cc8 --- /dev/null +++ b/dictation_client/src/pages/SupportPage/index.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import Header from "components/header"; +import { UpdateTokenTimer } from "components/auth/updateTokenTimer"; +import { useTranslation } from "react-i18next"; +import { getTranslationID } from "translation"; +import styles from "styles/app.module.scss"; + +const SupportPage: React.FC = () => { + const { t } = useTranslation(); + + return ( + + ); +}; +export default SupportPage; diff --git a/dictation_client/src/pages/TermsPage/index.tsx b/dictation_client/src/pages/TermsPage/index.tsx index cb45b5d..777a115 100644 --- a/dictation_client/src/pages/TermsPage/index.tsx +++ b/dictation_client/src/pages/TermsPage/index.tsx @@ -29,9 +29,12 @@ const TermsPage: React.FC = (): JSX.Element => { const tier = useSelector(selectTier); const [isCheckedEula, setIsCheckedEula] = useState(false); + const [isCheckedPrivacyNotice, setIsCheckedPrivacyNotice] = useState(false); const [isCheckedDpa, setIsCheckedDpa] = useState(false); const [isClickedEulaLink, setIsClickedEulaLink] = useState(false); + const [isClickedPrivacyNoticeLink, setIsClickedPrivacyNoticeLink] = + useState(false); const [isClickedDpaLink, setIsClickedDpaLink] = useState(false); // 画面起動時 @@ -52,9 +55,9 @@ const TermsPage: React.FC = (): JSX.Element => { // ボタン押下可否判定ロジック const canClickButton = () => { if (isTier5()) { - return isCheckedEula; + return isCheckedEula && isCheckedPrivacyNotice; } - return isCheckedEula && isCheckedDpa; + return isCheckedEula && isCheckedPrivacyNotice && isCheckedDpa; }; // ボタン押下時処理 @@ -62,7 +65,8 @@ const TermsPage: React.FC = (): JSX.Element => { if ( localStorageKeyforIdToken && updateAccceptVersions.acceptedVerDPA !== "" && - updateAccceptVersions.acceptedVerEULA !== "" + updateAccceptVersions.acceptedVerEULA !== "" && + updateAccceptVersions.acceptedVerPrivacyNotice !== "" ) { const { meta } = await dispatch( updateAcceptedVersionAsync({ @@ -132,7 +136,42 @@ const TermsPage: React.FC = (): JSX.Element => {

        - {/* 第五階層以外の場合はEulaのリンクをあわせて表示する */} +
        +

        + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} + setIsClickedPrivacyNoticeLink(true)} + data-tag="open-pricacy-notice" + > + {t( + getTranslationID("termsPage.label.linkOfPrivacyNotice") + )} + + {` ${t(getTranslationID("termsPage.label.forOdds"))}`} +

        +

        + +

        +
        + {/* 第五階層以外の場合はDPAのリンクをあわせて表示する */} {!isTier5() && (

        diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index a1ff500..f5399f2 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -2306,8 +2306,7 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input + label:hover, .formChange ul.holdMember li input + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left - center; + background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center; background-size: 1.3rem; } .formChange ul.chooseMember li input:checked + label, @@ -2318,8 +2317,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input:checked + label:hover, .formChange ul.holdMember li input:checked + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat - right center; + background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right + center; background-size: 1.3rem; } .formChange > p { @@ -2472,8 +2471,7 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input + label:hover, .formChange ul.holdMember li input + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left - center; + background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center; background-size: 1.3rem; } .formChange ul.chooseMember li input:checked + label, @@ -2484,8 +2482,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input:checked + label:hover, .formChange ul.holdMember li input:checked + label:hover { - background: #e6e6e6 url(../images/arrow_circle_right.svg) no-repeat right - center; + background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat + right center; background-size: 1.3rem; } .formChange > p { diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 50957fc..71b532f 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -25,6 +25,7 @@ "headerDictations": "(de)Dictations", "headerWorkflow": "(de)Workflow", "headerPartners": "(de)Partners", + "headerSupport": "(de)Support", "tier1": "(de)Admin", "tier2": "(de)BC", "tier3": "(de)Distributor", @@ -250,7 +251,11 @@ "poolTranscriptionist": "Transkriptionsliste", "fileBackup": "(de)File Backup", "downloadForBackup": "(de)Download for backup", - "cancelDictation": "(de)Cancel Dictation" + "applications": "(de)Applications", + "cancelDictation": "(de)Cancel Dictation", + "general": "(de)General", + "job": "(de)Job", + "close": "(de)Close" } }, "cardLicenseIssuePopupPage": { @@ -527,9 +532,31 @@ "title": "(de)Terms of Use has updated. Please confirm again.", "linkOfEula": "(de)Click here to read the terms of use.", "linkOfDpa": "(de)Click here to read the terms of use.", + "linkOfPrivacyNotice": "(de)Click here to read the terms of use.", "checkBoxForConsent": "(de)Yes, I agree to the terms of use.", "forOdds": "(de)for ODDS.", - "button": "(de)Continue" + "button": "(de)Continue", + "linkOfPrivacyNotice": "(de)Click here to read the terms of use." + } + }, + "supportPage": { + "label": { + "title": "(de)Support", + "howToUse": "(de)How to use the system", + "supportPageEnglish": "OMDS Cloud User Guide", + "supportPageGerman": "OMDS Cloud Benutzerhandbuch", + "supportPageFrench": "OMDS Cloud Mode d'emploi", + "supportPageSpanish": "OMDS Cloud Guía del usuario" + }, + "text": { + "notResolved": "(de)If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + } + }, + "filePropertyPopup": { + "label": { + "general": "(de)General", + "job": "(de)Job", + "close": "(de)Close" } } } diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 0bde925..cbf9f66 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -25,6 +25,7 @@ "headerDictations": "Dictations", "headerWorkflow": "Workflow", "headerPartners": "Partners", + "headerSupport": "Support", "tier1": "Admin", "tier2": "BC", "tier3": "Distributor", @@ -250,7 +251,11 @@ "poolTranscriptionist": "Transcription List", "fileBackup": "File Backup", "downloadForBackup": "Download for backup", - "cancelDictation": "Cancel Dictation" + "applications": "Applications", + "cancelDictation": "Cancel Dictation", + "general": "General", + "job": "Job", + "close": "Close" } }, "cardLicenseIssuePopupPage": { @@ -527,9 +532,31 @@ "title": "Terms of Use has updated. Please confirm again.", "linkOfEula": "Click here to read the terms of use.", "linkOfDpa": "Click here to read the terms of use.", + "linkOfPrivacyNotice": "Click here to read the terms of use.", "checkBoxForConsent": "Yes, I agree to the terms of use.", "forOdds": "for ODDS.", - "button": "Continue" + "button": "Continue", + "linkOfPrivacyNotice": "Click here to read the terms of use." + } + }, + "supportPage": { + "label": { + "title": "Support", + "howToUse": "How to use the system", + "supportPageEnglish": "OMDS Cloud User Guide", + "supportPageGerman": "OMDS Cloud Benutzerhandbuch", + "supportPageFrench": "OMDS Cloud Mode d'emploi", + "supportPageSpanish": "OMDS Cloud Guía del usuario" + }, + "text": { + "notResolved": "If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + } + }, + "filePropertyPopup": { + "label": { + "general": "General", + "job": "Job", + "close": "Close" } } } diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 481c79d..552edb2 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -25,6 +25,7 @@ "headerDictations": "(es)Dictations", "headerWorkflow": "(es)Workflow", "headerPartners": "(es)Partners", + "headerSupport": "(es)Support", "tier1": "(es)Admin", "tier2": "(es)BC", "tier3": "(es)Distributor", @@ -250,7 +251,11 @@ "poolTranscriptionist": "Lista de transcriptor", "fileBackup": "(es)File Backup", "downloadForBackup": "(es)Download for backup", - "cancelDictation": "(es)Cancel Dictation" + "applications": "(es)Applications", + "cancelDictation": "(es)Cancel Dictation", + "general": "(es)General", + "job": "(es)Job", + "close": "(es)Close" } }, "cardLicenseIssuePopupPage": { @@ -527,9 +532,31 @@ "title": "(es)Terms of Use has updated. Please confirm again.", "linkOfEula": "(es)Click here to read the terms of use.", "linkOfDpa": "(es)Click here to read the terms of use.", + "linkOfPrivacyNotice": "(es)Click here to read the terms of use.", "checkBoxForConsent": "(es)Yes, I agree to the terms of use.", "forOdds": "(es)for ODDS.", - "button": "(es)Continue" + "button": "(es)Continue", + "linkOfPrivacyNotice": "(es)Click here to read the terms of use." + } + }, + "supportPage": { + "label": { + "title": "(es)Support", + "howToUse": "(es)How to use the system", + "supportPageEnglish": "OMDS Cloud User Guide", + "supportPageGerman": "OMDS Cloud Benutzerhandbuch", + "supportPageFrench": "OMDS Cloud Mode d'emploi", + "supportPageSpanish": "OMDS Cloud Guía del usuario" + }, + "text": { + "notResolved": "(es)If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + } + }, + "filePropertyPopup": { + "label": { + "general": "(es)General", + "job": "(es)Job", + "close": "(es)Close" } } } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 6ffc1bd..e3bf194 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -25,6 +25,7 @@ "headerDictations": "(fr)Dictations", "headerWorkflow": "(fr)Workflow", "headerPartners": "(fr)Partners", + "headerSupport": "(fr)Support", "tier1": "(fr)Admin", "tier2": "(fr)BC", "tier3": "(fr)Distributor", @@ -250,7 +251,11 @@ "poolTranscriptionist": "Liste de transcriptionniste", "fileBackup": "(fr)File Backup", "downloadForBackup": "(fr)Download for backup", - "cancelDictation": "(fr)Cancel Dictation" + "applications": "(fr)Applications", + "cancelDictation": "(fr)Cancel Dictation", + "general": "(fr)General", + "job": "(fr)Job", + "close": "(fr)Close" } }, "cardLicenseIssuePopupPage": { @@ -527,9 +532,31 @@ "title": "(fr)Terms of Use has updated. Please confirm again.", "linkOfEula": "(fr)Click here to read the terms of use.", "linkOfDpa": "(fr)Click here to read the terms of use.", + "linkOfPrivacyNotice": "(fr)Click here to read the terms of use.", "checkBoxForConsent": "(fr)Yes, I agree to the terms of use.", "forOdds": "(fr)for ODDS.", - "button": "(fr)Continue" + "button": "(fr)Continue", + "linkOfPrivacyNotice": "(fr)Click here to read the terms of use." + } + }, + "supportPage": { + "label": { + "title": "(fr)Support", + "howToUse": "(fr)How to use the system", + "supportPageEnglish": "OMDS Cloud User Guide", + "supportPageGerman": "OMDS Cloud Benutzerhandbuch", + "supportPageFrench": "OMDS Cloud Mode d'emploi", + "supportPageSpanish": "OMDS Cloud Guía del usuario" + }, + "text": { + "notResolved": "(fr)If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + } + }, + "filePropertyPopup": { + "label": { + "general": "(fr)General", + "job": "(fr)Job", + "close": "(fr)Close" } } } diff --git a/dictation_function/Dockerfile b/dictation_function/Dockerfile deleted file mode 100644 index 3dcb7b2..0000000 --- a/dictation_function/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -# To enable ssh & remote debugging on app service change the base image to the one below -# FROM mcr.microsoft.com/azure-functions/node:4-node18-appservice -FROM mcr.microsoft.com/azure-functions/node:4-node18 - -ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ - AzureFunctionsJobHost__Logging__Console__IsEnabled=true - -COPY . /home/site/wwwroot - -RUN cd /home/site/wwwroot && \ - npm install && \ - npm run build \ No newline at end of file diff --git a/dictation_function/host.json b/dictation_function/host.json index 9df9136..a75de3b 100644 --- a/dictation_function/host.json +++ b/dictation_function/host.json @@ -11,5 +11,10 @@ "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[4.*, 5.0.0)" + }, + "retry": { + "strategy": "fixedDelay", + "maxRetryCount": 3, + "delayInterval": "00:00:10" } -} \ No newline at end of file +} diff --git a/dictation_function/src/adb2c/adb2c.ts b/dictation_function/src/adb2c/adb2c.ts index f58f9b3..b9bde0c 100644 --- a/dictation_function/src/adb2c/adb2c.ts +++ b/dictation_function/src/adb2c/adb2c.ts @@ -5,8 +5,8 @@ import { AdB2cResponse, AdB2cUser } from "./types/types"; import { error } from "console"; import { makeADB2CKey, restoreAdB2cID } from "../common/cache"; import { promisify } from "util"; -import { createRedisClient } from "../redis/redis"; import { InvocationContext } from "@azure/functions"; +import { RedisClient } from "redis"; export class Adb2cTooManyRequestsError extends Error {} @@ -23,16 +23,24 @@ export class AdB2cService { ) { 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"], - }); + try { + 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 }); + this.graphClient = Client.initWithMiddleware({ authProvider }); + } catch (error) { + console.log(error); + throw error; + } } /** @@ -42,9 +50,9 @@ export class AdB2cService { */ async getUsers( context: InvocationContext, + redisClient: RedisClient, externalIds: string[] - ): Promise { - const redisClient = createRedisClient(); + ): Promise { try { const b2cUsers: AdB2cUser[] = []; const keys = externalIds.map((externalId) => makeADB2CKey(externalId)); @@ -123,7 +131,7 @@ export class AdB2cService { return [...cachedUsers, ...b2cUsers]; } else { - return undefined; + return b2cUsers; } } catch (e) { const { statusCode } = e; @@ -132,7 +140,6 @@ export class AdB2cService { } throw e; } finally { - redisClient.quit; } } } diff --git a/dictation_function/src/common/cache/constants.ts b/dictation_function/src/common/cache/constants.ts index da6c13e..ffa7fe7 100644 --- a/dictation_function/src/common/cache/constants.ts +++ b/dictation_function/src/common/cache/constants.ts @@ -1 +1,5 @@ -export const ADB2C_PREFIX = "adb2c-external-id:" \ No newline at end of file +export const ADB2C_PREFIX = "adb2c-external-id:"; +export const SEND_COMPLETE_PREFIX = "send-complete-id:"; +export const MAIL_U103 = "[U103]"; +export const MAIL_U104 = "[U104]"; +export const DONE = "Done"; // メール送信成功時にredisにキャッシュする値 diff --git a/dictation_function/src/common/cache/index.ts b/dictation_function/src/common/cache/index.ts index 067be25..27c0eab 100644 --- a/dictation_function/src/common/cache/index.ts +++ b/dictation_function/src/common/cache/index.ts @@ -1,4 +1,4 @@ -import { ADB2C_PREFIX } from './constants'; +import { ADB2C_PREFIX, SEND_COMPLETE_PREFIX } from "./constants"; /** * ADB2Cのユーザー格納用のキーを生成する @@ -6,8 +6,8 @@ import { ADB2C_PREFIX } from './constants'; * @returns キャッシュのキー */ export const makeADB2CKey = (externalId: string): string => { - return `${ADB2C_PREFIX}${externalId}`; -} + return `${ADB2C_PREFIX}${externalId}`; +}; /** * ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する @@ -15,5 +15,20 @@ export const makeADB2CKey = (externalId: string): string => { * @returns 外部ユーザーID */ export const restoreAdB2cID = (key: string): string => { - return key.replace(ADB2C_PREFIX, ''); -} \ No newline at end of file + return key.replace(ADB2C_PREFIX, ""); +}; + +/** + * ライセンスアラートメール送信履歴格納用のキーを生成する + * @param formattedDate 当日の日付(YYYY:MM:DD) + * @param externalId 外部ユーザーID + * @param mail メール種別 + * @returns キャッシュのキー + */ +export const makeSendCompKey = ( + formattedDate: string, + externalId: string, + mail: string +): string => { + return `${SEND_COMPLETE_PREFIX}${formattedDate}${mail}${externalId}`; +}; diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts index f8f2a71..bbab8b0 100644 --- a/dictation_function/src/functions/licenseAlert.ts +++ b/dictation_function/src/functions/licenseAlert.ts @@ -19,36 +19,166 @@ import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents import { AdB2cService } from "../adb2c/adb2c"; import { SendGridService } from "../sendgrid/sendgrid"; import { getMailFrom } from "../common/getEnv/getEnv"; +import { createRedisClient } from "../redis/redis"; +import { RedisClient } from "redis"; +import { promisify } from "util"; +import { makeSendCompKey } from "../common/cache"; +import { + MAIL_U103, + MAIL_U104, + SEND_COMPLETE_PREFIX, + DONE, +} from "../common/cache/constants"; export async function licenseAlertProcessing( context: InvocationContext, datasource: DataSource, + redisClient: RedisClient, sendgrid: SendGridService, adb2c: AdB2cService ) { - context.log("[IN]licenseAlertProcessing"); - const mailFrom = getMailFrom(); - const accountRepository = datasource.getRepository(Account); + try { + context.log("[IN]licenseAlertProcessing"); - // 第五のアカウントを取得 - const accounts = await accountRepository.find({ - where: { - tier: TIERS.TIER5, - }, - relations: { - primaryAdminUser: true, - secondaryAdminUser: true, - }, - }); + // redisのキー用 + const currentDate = new DateWithZeroTime(); + const formattedDate = `${currentDate.getFullYear()}-${( + currentDate.getMonth() + 1 + ).toString()}-${currentDate.getDate().toString()}`; + const keysAsync = promisify(redisClient.keys).bind(redisClient); - 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 sendTargetAccounts = await getAlertMailTargetAccount( + context, + datasource + ); - const counts = async () => { + // adb2cからメールアドレスを取得し、上記で取得したアカウントにマージする + const sendTargetAccountsMargedAdb2c = await createAccountInfo( + context, + redisClient, + adb2c, + sendTargetAccounts + ); + + // メール送信 + await sendAlertMail( + context, + redisClient, + sendgrid, + sendTargetAccountsMargedAdb2c, + formattedDate + ); + + // 最後まで処理が正常に通ったら、redisに保存した送信情報を削除する + try { + const delAsync = promisify(redisClient.del).bind(redisClient); + + const keys = await keysAsync(`${SEND_COMPLETE_PREFIX}${formattedDate}*`); + console.log(`delete terget:${keys}`); + if (keys.length > 0) { + const delResult = await delAsync(...keys); + console.log(`delete number:${delResult}`); + } + } catch (e) { + context.log("redis delete failed"); + throw e; + } + } catch (e) { + throw e; + } finally { + 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 }); + let datasource: DataSource; + try { + 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(); + } catch (e) { + context.log("database initialize failed."); + context.error(e); + throw e; + } + + let redisClient: RedisClient; + try { + // redis接続 + redisClient = createRedisClient(); + } catch (e) { + context.log("redis client create failed."); + context.error(e); + throw e; + } + + try { + const adb2c = new AdB2cService(); + const sendgrid = new SendGridService(); + await licenseAlertProcessing( + context, + datasource, + redisClient, + sendgrid, + adb2c + ); + } catch (e) { + context.log("licenseAlertProcessing failed."); + context.error(e); + throw e; + } finally { + await datasource.destroy(); + redisClient.quit; + context.log("[OUT]licenseAlert"); + } +} + +/** + * アラートメールを送信する対象のアカウントを取得する + * @param context + * @param datasource + * @returns accountInfo[] メール送信対象のアカウント情報 + */ +async function getAlertMailTargetAccount( + context: InvocationContext, + datasource: DataSource +): Promise { + try { + context.log("[IN]getAlertMailTargetAccount"); + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + const currentDateWithZeroTime = new DateWithZeroTime(); + const currentDateWithDayEndTime = new DateWithDayEndTime(); + + // 第五のアカウントを取得 + const accountRepository = datasource.getRepository(Account); + const accounts = await accountRepository.find({ + where: { + tier: TIERS.TIER5, + }, + relations: { + primaryAdminUser: true, + secondaryAdminUser: true, + }, + }); + + const sendTargetAccounts = [] as accountInfo[]; + const licenseRepository = datasource.getRepository(License); for (const account of accounts) { // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う const allocatableLicenseWithMargin = await licenseRepository.count({ @@ -109,6 +239,7 @@ export async function licenseAlertProcessing( let primaryAdminExternalId: string | undefined; let secondaryAdminExternalId: string | undefined; let parentCompanyName: string | undefined; + if (shortage !== 0 || userCount !== 0) { primaryAdminExternalId = account.primaryAdminUser ? account.primaryAdminUser.external_id @@ -143,12 +274,33 @@ export async function licenseAlertProcessing( secondaryAdminEmail: undefined, }); } - }; - await counts(); + return sendTargetAccounts; + } catch (e) { + context.error(e); + context.log("getAlertMailTargetAccount failed."); + throw e; + } finally { + context.log("[OUT]getAlertMailTargetAccount"); + } +} +/** + * Azure AD B2Cからユーザ情報を取得し、アカウント情報を作成する + * @param context + * @param redisClient + * @param adb2c + * @param sendTargetAccounts RDBから取得したアカウント情報 + * @returns accountInfo[] メール送信対象のアカウント情報 + */ +async function createAccountInfo( + context: InvocationContext, + redisClient: RedisClient, + adb2c: AdB2cService, + sendTargetAccounts: accountInfo[] +): Promise { // ADB2Cからユーザーを取得する用の外部ID配列を作成 const externalIds = [] as string[]; - sendTargetAccounts.map((x) => { + sendTargetAccounts.forEach((x) => { if (x.primaryAdminExternalId) { externalIds.push(x.primaryAdminExternalId); } @@ -156,11 +308,10 @@ export async function licenseAlertProcessing( externalIds.push(x.secondaryAdminExternalId); } }); - const adb2cUsers = await adb2c.getUsers(context, externalIds); - if (!adb2cUsers) { + const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds); + if (adb2cUsers.length === 0) { context.log("Target user not found"); - context.log("[OUT]licenseAlertProcessing"); - return; + return []; } // ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ sendTargetAccounts.map((info) => { @@ -188,17 +339,54 @@ export async function licenseAlertProcessing( } } }); + return sendTargetAccounts; +} + +/** + * アラートメールを送信する + * @param context + * @param redisClient + * @param sendgrid + * @param sendTargetAccounts メール送信対象のアカウント情報 + * @param formattedDate redisのキーに使用する日付 + * @returns ユーザ情報 + */ +async function sendAlertMail( + context: InvocationContext, + redisClient: RedisClient, + sendgrid: SendGridService, + sendTargetAccounts: accountInfo[], + formattedDate: string +): Promise { + try { + context.log("[IN]sendAlertMail"); + + // redis用 + const getAsync = promisify(redisClient.get).bind(redisClient); + const setexAsync = promisify(redisClient.setex).bind(redisClient); + const ttl = process.env.ADB2C_CACHE_TTL; + const mailFrom = getMailFrom(); - const sendMail = async () => { for (const targetAccount of sendTargetAccounts) { // プライマリ管理者が入っているかチェック // 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ if (targetAccount.primaryAdminExternalId) { // メール送信 // strictNullChecks対応 - if (targetAccount.primaryAdminEmail) { - // ライセンス不足メール - if (targetAccount.shortage !== 0) { + if (!targetAccount.primaryAdminEmail) { + continue; + } + // ライセンス不足メール + if (targetAccount.shortage !== 0) { + // redisに送信履歴がない場合のみ送信する + const mailResult = await getAsync( + makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U103 + ) + ); + if (mailResult !== DONE) { const { subject, text, html } = await createMailContentOfLicenseShortage( targetAccount.companyName, @@ -217,45 +405,107 @@ export async function licenseAlertProcessing( context.log( `Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}` ); - } catch { + // 送信成功時、成功履歴をredisに保存 + try { + const key = makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U103 + ); + await setexAsync(key, ttl, DONE); + context.log( + "setex Result:", + `key:${key},ttl:${ttl},value:Done` + ); + } catch (e) { + context.error(e); + context.log( + "setex failed.", + `target: ${targetAccount.primaryAdminEmail}` + ); + } + } catch (e) { + context.error(e); context.log( `Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}` ); - } - - // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 - if (targetAccount.secondaryAdminEmail) { - // ライセンス不足メール - if (targetAccount.shortage !== 0) { - const { subject, text, html } = - await createMailContentOfLicenseShortage( - targetAccount.companyName, - targetAccount.shortage, - targetAccount.parentCompanyName - ); - // メールを送信 - 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}` - ); - } - } + throw e; } } - // ライセンス失効警告メール - if (targetAccount.userCountOfLicenseExpiringSoon !== 0) { + // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 + if ( + targetAccount.secondaryAdminEmail && + targetAccount.secondaryAdminExternalId + ) { + // redisに送信履歴がない場合のみ送信する + const mailResult = await getAsync( + makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U103 + ) + ); + if (mailResult !== DONE) { + const { subject, text, html } = + await createMailContentOfLicenseShortage( + targetAccount.companyName, + targetAccount.shortage, + targetAccount.parentCompanyName + ); + // メールを送信 + try { + await sendgrid.sendMail( + targetAccount.secondaryAdminEmail, + mailFrom, + subject, + text, + html + ); + context.log( + `Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}` + ); + // 送信成功時、成功履歴をredisに保存 + try { + const key = makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U103 + ); + await setexAsync(key, ttl, DONE); + context.log( + "setex Result:", + `key:${key},ttl:${ttl},value:Done` + ); + } catch (e) { + context.error(e); + context.log( + "setex failed.", + `target: ${targetAccount.secondaryAdminEmail}` + ); + } + } catch (e) { + context.error(e); + context.log( + `Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}` + ); + throw e; + } + } + } + } + + // ライセンス失効警告メール + if (targetAccount.userCountOfLicenseExpiringSoon !== 0) { + // redisに送信履歴がない場合のみ送信する + const mailResult = await getAsync( + makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U104 + ) + ); + if (mailResult !== DONE) { const { subject, text, html } = await createMailContentOfLicenseExpiringSoon( targetAccount.companyName, @@ -274,80 +524,99 @@ export async function licenseAlertProcessing( context.log( `Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}` ); - } catch { + // 送信成功時、成功履歴をredisに保存 + try { + const key = makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U104 + ); + await setexAsync(key, ttl, DONE); + context.log( + "setex Result:", + `key:${key},ttl:${ttl},value:Done` + ); + } catch (e) { + context.error(e); + context.log( + "setex failed.", + `target: ${targetAccount.primaryAdminEmail}` + ); + } + } catch (e) { + context.error(e); context.log( `Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}` ); + throw e; } + } - // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 - if (targetAccount.secondaryAdminEmail) { - // ライセンス不足メール - if (targetAccount.shortage !== 0) { - const { subject, text, html } = - await createMailContentOfLicenseExpiringSoon( - targetAccount.companyName, - targetAccount.userCountOfLicenseExpiringSoon, - targetAccount.parentCompanyName - ); - // メールを送信 + // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 + if ( + targetAccount.secondaryAdminEmail && + targetAccount.secondaryAdminExternalId + ) { + // redisに送信履歴がない場合のみ送信する + const mailResult = makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U104 + ); + if (mailResult !== DONE) { + const { subject, text, html } = + await createMailContentOfLicenseExpiringSoon( + targetAccount.companyName, + targetAccount.userCountOfLicenseExpiringSoon, + targetAccount.parentCompanyName + ); + // メールを送信 + try { + await sendgrid.sendMail( + targetAccount.secondaryAdminEmail, + mailFrom, + subject, + text, + html + ); + context.log( + `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}` + ); try { - await sendgrid.sendMail( - targetAccount.secondaryAdminEmail, - mailFrom, - subject, - text, - html + const key = makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U104 ); + await setexAsync(key, ttl, DONE); context.log( - `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}` + "setex Result:", + `key:${key},ttl:${ttl},value:Done` ); - } catch { + } catch (e) { + context.error(e); context.log( - `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}` + "setex failed.", + `target: ${targetAccount.secondaryAdminEmail}` ); } + } catch (e) { + context.error(e); + context.log( + `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}` + ); + throw e; } } } } } } - }; - 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); + context.log("sendAlertMail failed."); + throw e; } finally { - await datasource.destroy(); - context.log("[OUT]licenseAlert"); + context.log("[OUT]sendAlertMail"); } } diff --git a/dictation_function/src/functions/redisTimerTest.ts b/dictation_function/src/functions/redisTimerTest.ts deleted file mode 100644 index 419079d..0000000 --- a/dictation_function/src/functions/redisTimerTest.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { app, InvocationContext, Timer } from "@azure/functions"; -import * as dotenv from "dotenv"; -import { promisify } from "util"; -import { createRedisClient } from "../redis/redis"; - -export async function redisTimerTest( - myTimer: Timer, - context: InvocationContext -): Promise { - context.log("---Timer function processed request."); - - dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); - - const redisClient = createRedisClient(); - const setAsync = promisify(redisClient.set).bind(redisClient); - const getAsync = promisify(redisClient.get).bind(redisClient); - - await setAsync("foo", "bar"); - const value = await getAsync("foo"); - context.log(`value=${value}`); // returns 'bar' - - await redisClient.quit; -} - -app.timer("redisTimerTest", { - schedule: "*/30 * * * * *", - handler: redisTimerTest, -}); diff --git a/dictation_function/src/redis/redis.ts b/dictation_function/src/redis/redis.ts index aedb573..c4ef775 100644 --- a/dictation_function/src/redis/redis.ts +++ b/dictation_function/src/redis/redis.ts @@ -20,12 +20,34 @@ export const createRedisClient = (): RedisClient => { host: host, port: port, password: password, + retry_strategy: (options) => { + if (options.attempt <= 3) { + console.log( + `Retrying connection to Redis. Attempt ${options.attempt}` + ); + return 10000; // ミリ秒単位でのリトライまでの間隔 + } else { + console.log("Exceeded maximum number of connection attempts."); + return undefined; // リトライを終了 + } + }, }); } else { client = createClient({ url: `rediss://${host}:${port}`, password: password, tls: {}, + retry_strategy: (options) => { + if (options.attempt <= 3) { + console.log( + `Retrying connection to Redis. Attempt ${options.attempt}` + ); + return 10000; // ミリ秒単位でのリトライまでの間隔 + } else { + console.log("Exceeded maximum number of connection attempts."); + return undefined; // リトライを終了 + } + }, }); } diff --git a/dictation_function/src/test/licenseAlert.spec.ts b/dictation_function/src/test/licenseAlert.spec.ts index a37e9d4..d790cf0 100644 --- a/dictation_function/src/test/licenseAlert.spec.ts +++ b/dictation_function/src/test/licenseAlert.spec.ts @@ -13,6 +13,8 @@ import { ADB2C_SIGN_IN_TYPE } from "../constants"; import { SendGridService } from "../sendgrid/sendgrid"; import { AdB2cService } from "../adb2c/adb2c"; import { InvocationContext } from "@azure/functions"; +import { RedisClient } from "redis"; +import { createRedisClient } from "../redis/redis"; describe("licenseAlert", () => { dotenv.config({ path: ".env" }); @@ -40,6 +42,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -63,8 +66,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(1); + redisClient.quit; }); it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => { @@ -72,6 +82,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -96,8 +107,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(2); + redisClient.quit; }); it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => { @@ -105,6 +123,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -142,8 +161,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(0); + redisClient.quit; }); it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => { @@ -151,6 +177,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -175,8 +202,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(1); + redisClient.quit; }); }); @@ -211,6 +245,7 @@ export class AdB2cServiceMock { */ async getUsers( context: InvocationContext, + redisClient: RedisClient, externalIds: string[] ): Promise { const AdB2cMockUsers: AdB2cUser[] = [ diff --git a/dictation_server/db/migrations/049-add_privacy_notice_column.sql b/dictation_server/db/migrations/049-add_privacy_notice_column.sql new file mode 100644 index 0000000..74cb8dd --- /dev/null +++ b/dictation_server/db/migrations/049-add_privacy_notice_column.sql @@ -0,0 +1,12 @@ + +-- +migrate Up +ALTER TABLE `users` ADD COLUMN `accepted_privacy_notice_version` VARCHAR(255) COMMENT '同意済みプライバシーポリシーバージョン' AFTER `accepted_eula_version`; +ALTER TABLE `users_archive` ADD COLUMN `accepted_privacy_notice_version` VARCHAR(255) COMMENT '同意済みプライバシーポリシーバージョン' AFTER `accepted_eula_version`; +insert into terms(terms.document_type, terms.version) values('PrivacyNotice', 'V0.1'); +commit; + +-- +migrate Down +ALTER TABLE `users` DROP COLUMN `accepted_privacy_notice_version`; +ALTER TABLE `users_archive` DROP COLUMN `accepted_privacy_notice_version`; +delete from terms where terms.document_type = 'PrivacyNotice' and terms.version = 'V0.1'; +commit; \ No newline at end of file diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index e112ae2..069d43c 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1455,6 +1455,52 @@ "tags": ["accounts"] } }, + "/accounts/company-name": { + "post": { + "operationId": "getCompanyName", + "summary": "", + "description": "指定したアカウントの会社名を取得します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetCompanyNameRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCompanyNameResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -3491,6 +3537,10 @@ "type": "string", "description": "同意済み利用規約のバージョン(EULA)" }, + "acceptedPrivacyNoticeVersion": { + "type": "string", + "description": "同意済みプライバシーポリシーのバージョン" + }, "acceptedDpaVersion": { "type": "string", "description": "同意済み利用規約のバージョン(DPA)" @@ -3504,6 +3554,7 @@ "adminMail", "adminPassword", "acceptedEulaVersion", + "acceptedPrivacyNoticeVersion", "acceptedDpaVersion", "token" ] @@ -4009,6 +4060,16 @@ "properties": { "tier": { "type": "number", "description": "階層" } }, "required": ["tier"] }, + "GetCompanyNameRequest": { + "type": "object", + "properties": { "accountId": { "type": "number" } }, + "required": ["accountId"] + }, + "GetCompanyNameResponse": { + "type": "object", + "properties": { "companyName": { "type": "string" } }, + "required": ["companyName"] + }, "ConfirmRequest": { "type": "object", "properties": { "token": { "type": "string" } }, @@ -4236,12 +4297,20 @@ "type": "string", "description": "更新バージョン(EULA)" }, + "acceptedPrivacyNoticeVersion": { + "type": "string", + "description": "更新バージョン(PrivacyNotice)" + }, "acceptedDPAVersion": { "type": "string", "description": "更新バージョン(DPA)" } }, - "required": ["idToken", "acceptedEULAVersion"] + "required": [ + "idToken", + "acceptedEULAVersion", + "acceptedPrivacyNoticeVersion" + ] }, "UpdateAcceptedVersionResponse": { "type": "object", "properties": {} }, "GetMyUserResponse": { diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index 00b19d7..18dda44 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -182,6 +182,8 @@ export const makeTestAccount = async ( role: d?.role ?? 'admin none', author_id: d?.author_id ?? undefined, accepted_eula_version: d?.accepted_eula_version ?? '1.0', + accepted_privacy_notice_version: + d?.accepted_privacy_notice_version ?? '1.0', accepted_dpa_version: d?.accepted_dpa_version ?? '1.0', email_verified: d?.email_verified ?? true, auto_renew: d?.auto_renew ?? true, diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 0f8a7eb..439c7ed 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -287,6 +287,7 @@ export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]'; export const TERM_TYPE = { EULA: 'EULA', DPA: 'DPA', + PRIVACY_NOTICE: 'PrivacyNotice', } as const; /** diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 571f4ae..619aa2b 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -68,6 +68,8 @@ import { GetAccountInfoMinimalAccessResponse, DeleteWorktypeRequestParam, DeleteWorktypeResponse, + GetCompanyNameRequest, + GetCompanyNameResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -116,6 +118,7 @@ export class AccountsController { adminPassword, adminName, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, } = body; const role = USER_ROLES.NONE; @@ -132,6 +135,7 @@ export class AccountsController { adminName, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); @@ -1550,4 +1554,56 @@ export class AccountsController { ); return { tier }; } + + @ApiResponse({ + status: HttpStatus.OK, + type: GetCompanyNameResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'getCompanyName', + description: '指定したアカウントの会社名を取得します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('company-name') + async getCompanyName( + @Req() req: Request, + @Body() body: GetCompanyNameRequest, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + const context = makeContext(userId); + const companyName = await this.accountService.getCompanyName( + context, + body.accountId, + ); + return companyName; + } } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index b0c2b47..27c3715 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -112,6 +112,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -144,6 +145,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); // 作成したアカウントのIDが返ってくるか確認 @@ -161,6 +163,9 @@ describe('createAccount', () => { expect(account?.primary_admin_user_id).toBe(user?.id); expect(account?.secondary_admin_user_id).toBe(null); expect(user?.accepted_eula_version).toBe(acceptedEulaVersion); + expect(user?.accepted_privacy_notice_version).toBe( + acceptedPrivacyNoticeVersion, + ); expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion); expect(user?.account_id).toBe(accountId); expect(user?.role).toBe(role); @@ -195,6 +200,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'admin none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -216,6 +222,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -264,6 +271,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'admin none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -286,6 +294,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -318,6 +327,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -345,6 +355,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -384,6 +395,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -411,6 +423,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -452,6 +465,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -480,6 +494,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -520,6 +535,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -551,6 +567,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -593,6 +610,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -641,6 +659,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -689,6 +708,7 @@ describe('createAccount', () => { const username = 'dummy_username'; const role = 'none'; const acceptedEulaVersion = '1.0.0'; + const acceptedPrivacyNoticeVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0'; overrideAdB2cService(service, { @@ -734,6 +754,7 @@ describe('createAccount', () => { username, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); } catch (e) { @@ -6694,3 +6715,60 @@ describe('getAccountInfoMinimalAccess', () => { } }); }); +describe('getCompanyName', () => { + 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('アカウントIDから会社名が取得できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'testCompany', + }); + const context = makeContext(admin.external_id); + const response = await service.getCompanyName(context, account.id); + expect({ companyName: 'testCompany' }).toEqual(response); + }); + + it('アカウントが存在しない場合、400エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'testCompany', + }); + const context = makeContext(admin.external_id); + try { + await service.getCompanyName(context, 123); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010501')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index fda08e0..668f815 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -34,6 +34,7 @@ import { PostWorktypeOptionItem, Author, Partner, + GetCompanyNameResponse, } from './types/types'; import { DateWithZeroTime, @@ -175,6 +176,7 @@ export class AccountsService { username: string, role: string, acceptedEulaVersion: string, + acceptedPrivacyNoticeVersion: string, acceptedDpaVersion: string, ): Promise<{ accountId: number; userId: number; externalUserId: string }> { this.logger.log( @@ -184,6 +186,7 @@ export class AccountsService { `dealerAccountId: ${dealerAccountId}, ` + `role: ${role}, ` + `acceptedEulaVersion: ${acceptedEulaVersion}, ` + + `acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` + `acceptedDpaVersion: ${acceptedDpaVersion} };`, ); try { @@ -232,6 +235,7 @@ export class AccountsService { externalUser.sub, role, acceptedEulaVersion, + acceptedPrivacyNoticeVersion, acceptedDpaVersion, ); account = newAccount; @@ -2151,4 +2155,51 @@ export class AccountsService { ); } } + /** + * 自アカウントの会社名を取得する + * @param accountId + * @returns CompanyName + */ + async getCompanyName( + context: Context, + accountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.getCompanyName.name + } | params: { accountId: ${accountId}, };`, + ); + + try { + const { company_name } = await this.accountRepository.findAccountById( + accountId, + ); + + return { companyName: company_name }; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.getCompanyName.name}`, + ); + } + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 921bd10..276fd0e 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -45,6 +45,8 @@ export class CreateAccountRequest { adminPassword: string; @ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' }) acceptedEulaVersion: string; + @ApiProperty({ description: '同意済みプライバシーポリシーのバージョン' }) + acceptedPrivacyNoticeVersion: string; @ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' }) acceptedDpaVersion: string; @ApiProperty({ description: 'reCAPTCHA Token' }) @@ -599,3 +601,13 @@ export class GetAccountInfoMinimalAccessResponse { @ApiProperty({ description: '階層' }) tier: number; } +export class GetCompanyNameRequest { + @ApiProperty() + @IsInt() + @Type(() => Number) + accountId: number; +} +export class GetCompanyNameResponse { + @ApiProperty() + companyName: string; +} diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts index 4451cb0..f978889 100644 --- a/dictation_server/src/features/auth/auth.service.spec.ts +++ b/dictation_server/src/features/auth/auth.service.spec.ts @@ -196,6 +196,7 @@ describe('checkIsAcceptedLatestVersion', () => { }; await createTermInfo(source, 'EULA', '1.0'); + await createTermInfo(source, 'PrivacyNotice', '1.0'); await createTermInfo(source, 'DPA', '1.0'); const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(true); @@ -219,6 +220,7 @@ describe('checkIsAcceptedLatestVersion', () => { }; await createTermInfo(source, 'EULA', '1.0'); + await createTermInfo(source, 'PrivacyNotice', '1.0'); await createTermInfo(source, 'DPA', '1.0'); const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(true); @@ -242,6 +244,7 @@ describe('checkIsAcceptedLatestVersion', () => { }; await createTermInfo(source, 'EULA', '1.1'); + await createTermInfo(source, 'PrivacyNotice', '1.0'); await createTermInfo(source, 'DPA', '1.0'); const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(false); @@ -265,6 +268,7 @@ describe('checkIsAcceptedLatestVersion', () => { }; await createTermInfo(source, 'EULA', '1.1'); + await createTermInfo(source, 'PrivacyNotice', '1.0'); await createTermInfo(source, 'DPA', '1.0'); const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(false); @@ -288,10 +292,35 @@ describe('checkIsAcceptedLatestVersion', () => { }; await createTermInfo(source, 'EULA', '1.0'); + await createTermInfo(source, 'PrivacyNotice', '1.0'); await createTermInfo(source, 'DPA', '1.1'); const result = await service.isAcceptedLatestVersion(context, idToken); expect(result).toBe(false); }); + + it('同意済みプライバシーポリシーが最新でないときにチェックが通らないこと(第一~第四)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AuthService); + const { admin } = await makeTestAccount(source, { + tier: 4, + }); + const context = makeContext(uuidv4()); + + const idToken = { + emails: [], + sub: admin.external_id, + exp: 0, + iat: 0, + }; + + await createTermInfo(source, 'EULA', '1.0'); + await createTermInfo(source, 'PrivacyNotice', '1.1'); + await createTermInfo(source, 'DPA', '1.0'); + const result = await service.isAcceptedLatestVersion(context, idToken); + expect(result).toBe(false); + }); }); describe('generateDelegationRefreshToken', () => { diff --git a/dictation_server/src/features/auth/auth.service.ts b/dictation_server/src/features/auth/auth.service.ts index 543ca12..dbcdfc3 100644 --- a/dictation_server/src/features/auth/auth.service.ts +++ b/dictation_server/src/features/auth/auth.service.ts @@ -689,28 +689,38 @@ export class AuthService { const { acceptedEulaVersion, latestEulaVersion, + acceptedPrivacyNoticeVersion, + latestPrivacyNoticeVersion, acceptedDpaVersion, latestDpaVersion, tier, } = await this.usersRepository.getAcceptedAndLatestVersion(idToken.sub); - // 第五階層はEULAのみ判定 + // 第五階層はEULAとPrivacyNoticeのみ判定 if (tier === TIERS.TIER5) { - if (!acceptedEulaVersion) { + if (!acceptedEulaVersion || !acceptedPrivacyNoticeVersion) { return false; } // 最新バージョンに同意済みか判定 const eulaAccepted = acceptedEulaVersion === latestEulaVersion; - return eulaAccepted; + const privacyNoticeAccepted = + acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion; + return eulaAccepted && privacyNoticeAccepted; } else { - // 第一~第四階層はEULA、DPAを判定 - if (!acceptedEulaVersion || !acceptedDpaVersion) { + // 第一~第四階層はEULA、PrivacyNotice、DPAを判定 + if ( + !acceptedEulaVersion || + !acceptedPrivacyNoticeVersion || + !acceptedDpaVersion + ) { return false; } // 最新バージョンに同意済みか判定 const eulaAccepted = acceptedEulaVersion === latestEulaVersion; + const privacyNoticeAccepted = + acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion; const dpaAccepted = acceptedDpaVersion === latestDpaVersion; - return eulaAccepted && dpaAccepted; + return eulaAccepted && privacyNoticeAccepted && dpaAccepted; } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); diff --git a/dictation_server/src/features/auth/types/types.ts b/dictation_server/src/features/auth/types/types.ts index 1be9570..c031508 100644 --- a/dictation_server/src/features/auth/types/types.ts +++ b/dictation_server/src/features/auth/types/types.ts @@ -22,8 +22,10 @@ export class AccessTokenRequest {} export type TermsCheckInfo = { tier: number; acceptedEulaVersion?: string; + acceptedPrivacyNoticeVersion?: string; acceptedDpaVersion?: string; latestEulaVersion: string; + latestPrivacyNoticeVersion: string; latestDpaVersion: string; }; diff --git a/dictation_server/src/features/files/test/files.service.mock.ts b/dictation_server/src/features/files/test/files.service.mock.ts index a5b8f06..c69e7bf 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -139,6 +139,7 @@ export const makeDefaultUsersRepositoryMockValue = role: 'none', author_id: '', accepted_eula_version: '1.0', + accepted_privacy_notice_version: '1.0', accepted_dpa_version: '1.0', email_verified: true, deleted_at: null, diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index cf5aa71..5d5d97c 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -470,6 +470,7 @@ const defaultTasksRepositoryMockValue: { external_id: 'userId', role: 'typist', accepted_eula_version: '', + accepted_privacy_notice_version: '', accepted_dpa_version: '', email_verified: true, auto_renew: true, diff --git a/dictation_server/src/features/terms/terms.service.spec.ts b/dictation_server/src/features/terms/terms.service.spec.ts index 772e9f5..6ff1176 100644 --- a/dictation_server/src/features/terms/terms.service.spec.ts +++ b/dictation_server/src/features/terms/terms.service.spec.ts @@ -34,6 +34,8 @@ describe('利用規約取得', () => { await createTermInfo(source, 'EULA', 'v1.0'); await createTermInfo(source, 'EULA', 'v1.1'); + await createTermInfo(source, 'PrivacyNotice', 'v1.0'); + await createTermInfo(source, 'PrivacyNotice', 'v1.1'); await createTermInfo(source, 'DPA', 'v1.0'); await createTermInfo(source, 'DPA', 'v1.2'); @@ -42,8 +44,10 @@ describe('利用規約取得', () => { expect(result[0].documentType).toBe('EULA'); expect(result[0].version).toBe('v1.1'); - expect(result[1].documentType).toBe('DPA'); - expect(result[1].version).toBe('v1.2'); + expect(result[1].documentType).toBe('PrivacyNotice'); + expect(result[1].version).toBe('v1.1'); + expect(result[2].documentType).toBe('DPA'); + expect(result[2].version).toBe('v1.2'); }); it('利用規約情報(EULA、DPA両方)が存在しない場合エラーとなる', async () => { @@ -75,6 +79,21 @@ describe('利用規約取得', () => { ); }); + it('利用規約情報(PrivacyNoticeのみ)が存在しない場合エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(TermsService); + await createTermInfo(source, 'PrivacyNotice', 'v1.0'); + const context = makeContext(uuidv4()); + await expect(service.getTermsInfo(context)).rejects.toEqual( + new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ), + ); + }); + it('利用規約情報(DPAのみ)が存在しない場合エラーとなる', async () => { if (!source) fail(); const module = await makeTestingModule(source); diff --git a/dictation_server/src/features/terms/terms.service.ts b/dictation_server/src/features/terms/terms.service.ts index 65137a1..ee52415 100644 --- a/dictation_server/src/features/terms/terms.service.ts +++ b/dictation_server/src/features/terms/terms.service.ts @@ -19,13 +19,17 @@ export class TermsService { `[IN] [${context.getTrackingId()}] ${this.getTermsInfo.name}`, ); try { - const { eulaVersion, dpaVersion } = + const { eulaVersion, privacyNoticeVersion, dpaVersion } = await this.termsRepository.getLatestTermsInfo(); return [ { documentType: TERM_TYPE.EULA, version: eulaVersion, }, + { + documentType: TERM_TYPE.PRIVACY_NOTICE, + version: privacyNoticeVersion, + }, { documentType: TERM_TYPE.DPA, version: dpaVersion, diff --git a/dictation_server/src/features/terms/types/types.ts b/dictation_server/src/features/terms/types/types.ts index 6a45eae..479960e 100644 --- a/dictation_server/src/features/terms/types/types.ts +++ b/dictation_server/src/features/terms/types/types.ts @@ -13,5 +13,6 @@ export class GetTermsInfoResponse { export type TermsVersion = { eulaVersion: string; + privacyNoticeVersion: string; dpaVersion: string; }; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 9f48418..d21f72d 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -263,6 +263,8 @@ export class UpdateAcceptedVersionRequest { idToken: string; @ApiProperty({ description: '更新バージョン(EULA)' }) acceptedEULAVersion: string; + @ApiProperty({ description: '更新バージョン(PrivacyNotice)' }) + acceptedPrivacyNoticeVersion: string; @ApiProperty({ description: '更新バージョン(DPA)', required: false }) acceptedDPAVersion?: string; } diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index a92bbd2..c8cdafc 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -4,6 +4,7 @@ import { Get, HttpException, HttpStatus, + Ip, Post, Query, Req, @@ -136,6 +137,7 @@ export class UsersController { @Get() async getUsers(@Req() req: Request): Promise { const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { throw new HttpException( makeErrorResponse('E000107'), @@ -627,7 +629,12 @@ export class UsersController { async updateAcceptedVersion( @Body() body: UpdateAcceptedVersionRequest, ): Promise { - const { idToken, acceptedEULAVersion, acceptedDPAVersion } = body; + const { + idToken, + acceptedEULAVersion, + acceptedPrivacyNoticeVersion, + acceptedDPAVersion, + } = body; const context = makeContext(uuidv4()); @@ -650,6 +657,7 @@ export class UsersController { context, verifiedIdToken.sub, acceptedEULAVersion, + acceptedPrivacyNoticeVersion, acceptedDPAVersion, ); return {}; diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 83894ee..cea6f31 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -208,6 +208,7 @@ describe('UsersService.confirmUserAndInitPassword', () => { account_id: 1, role: 'None', accepted_eula_version: 'string', + accepted_privacy_notice_version: 'string', accepted_dpa_version: 'string', email_verified: false, created_by: 'string;', @@ -259,6 +260,7 @@ describe('UsersService.confirmUserAndInitPassword', () => { account_id: 1, role: 'None', accepted_eula_version: 'string', + accepted_privacy_notice_version: 'string', accepted_dpa_version: 'string', email_verified: false, created_by: 'string;', @@ -306,6 +308,7 @@ describe('UsersService.confirmUserAndInitPassword', () => { account_id: 1, role: 'None', accepted_eula_version: 'string', + accepted_privacy_notice_version: 'string', accepted_dpa_version: 'string', email_verified: true, created_by: 'string;', @@ -358,6 +361,7 @@ describe('UsersService.confirmUserAndInitPassword', () => { account_id: 1, role: 'None', accepted_eula_version: 'string', + accepted_privacy_notice_version: 'string', accepted_dpa_version: 'string', email_verified: false, created_by: 'string;', @@ -2617,7 +2621,12 @@ describe('UsersService.updateAcceptedVersion', () => { const context = makeContext(uuidv4()); const service = module.get(UsersService); - await service.updateAcceptedVersion(context, admin.external_id, 'v2.0'); + await service.updateAcceptedVersion( + context, + admin.external_id, + 'v2.0', + 'v2.0', + ); const user = await getUser(source, admin.id); expect(user?.accepted_eula_version).toBe('v2.0'); @@ -2637,6 +2646,7 @@ describe('UsersService.updateAcceptedVersion', () => { context, admin.external_id, 'v2.0', + 'v2.0', 'v3.0', ); const user = await getUser(source, admin.id); @@ -2660,6 +2670,7 @@ describe('UsersService.updateAcceptedVersion', () => { context, admin.external_id, 'v2.0', + 'v2.0', undefined, ), ).rejects.toEqual( diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index cf82fd5..e26a1c1 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -403,6 +403,7 @@ export class UsersService { role, accepted_dpa_version: null, accepted_eula_version: null, + accepted_privacy_notice_version: null, encryption: false, encryption_password: null, prompt: false, @@ -422,6 +423,7 @@ export class UsersService { prompt: prompt ?? false, accepted_dpa_version: null, accepted_eula_version: null, + accepted_privacy_notice_version: null, }; default: //不正なroleが指定された場合はログを出力してエラーを返す @@ -538,6 +540,7 @@ export class UsersService { // DBから同一アカウントのユーザ一覧を取得する const dbUsers = await this.usersRepository.findSameAccountUsers( externalId, + context, ); // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する @@ -1044,12 +1047,14 @@ export class UsersService { * @param context * @param idToken * @param eulaVersion + * @param privacyNoticeVersion * @param dpaVersion */ async updateAcceptedVersion( context: Context, externalId: string, eulaVersion: string, + privacyNoticeVersion: string, dpaVersion?: string, ): Promise { this.logger.log( @@ -1058,6 +1063,7 @@ export class UsersService { } | params: { ` + `externalId: ${externalId}, ` + `eulaVersion: ${eulaVersion}, ` + + `privacyNoticeVersion: ${privacyNoticeVersion}, ` + `dpaVersion: ${dpaVersion}, };`, ); @@ -1065,6 +1071,7 @@ export class UsersService { await this.usersRepository.updateAcceptedTermsVersion( externalId, eulaVersion, + privacyNoticeVersion, dpaVersion, ); } catch (e) { diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 1df7d13..d7762bd 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -127,6 +127,7 @@ export class AccountsRepositoryService { adminExternalUserId: string, adminUserRole: string, adminUserAcceptedEulaVersion?: string, + adminUserAcceptedPrivacyNoticeVersion?: string, adminUserAcceptedDpaVersion?: string, ): Promise<{ newAccount: Account; adminUser: User }> { return await this.dataSource.transaction(async (entityManager) => { @@ -148,6 +149,8 @@ export class AccountsRepositoryService { user.external_id = adminExternalUserId; user.role = adminUserRole; user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null; + user.accepted_privacy_notice_version = + adminUserAcceptedPrivacyNoticeVersion ?? null; user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null; } const usersRepo = entityManager.getRepository(User); diff --git a/dictation_server/src/repositories/terms/terms.repository.service.ts b/dictation_server/src/repositories/terms/terms.repository.service.ts index 7c79f24..7deee0f 100644 --- a/dictation_server/src/repositories/terms/terms.repository.service.ts +++ b/dictation_server/src/repositories/terms/terms.repository.service.ts @@ -24,6 +24,14 @@ export class TermsRepositoryService { id: 'DESC', }, }); + const latestPrivacyNoticeInfo = await termRepo.findOne({ + where: { + document_type: TERM_TYPE.PRIVACY_NOTICE, + }, + order: { + id: 'DESC', + }, + }); const latestDpaInfo = await termRepo.findOne({ where: { document_type: TERM_TYPE.DPA, @@ -33,13 +41,16 @@ export class TermsRepositoryService { }, }); - if (!latestEulaInfo || !latestDpaInfo) { + if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) { throw new TermInfoNotFoundError( - `Terms info is not found. latestEulaInfo: ${latestEulaInfo}, latestDpaInfo: ${latestDpaInfo}`, + `Terms info is not found. latestEulaInfo: ${latestEulaInfo}, + latestPrivacyNoticeInfo: ${latestPrivacyNoticeInfo}, + latestDpaInfo: ${latestDpaInfo}`, ); } return { eulaVersion: latestEulaInfo.version, + privacyNoticeVersion: latestEulaInfo.version, dpaVersion: latestDpaInfo.version, }; }); diff --git a/dictation_server/src/repositories/users/entity/user.entity.ts b/dictation_server/src/repositories/users/entity/user.entity.ts index 34d0bca..0f4e57c 100644 --- a/dictation_server/src/repositories/users/entity/user.entity.ts +++ b/dictation_server/src/repositories/users/entity/user.entity.ts @@ -34,6 +34,9 @@ export class User { @Column({ nullable: true, type: 'varchar' }) accepted_eula_version: string | null; + @Column({ nullable: true, type: 'varchar' }) + accepted_privacy_notice_version: string | null; + @Column({ nullable: true, type: 'varchar' }) accepted_dpa_version: string | null; @@ -112,6 +115,9 @@ export class UserArchive { @Column({ nullable: true, type: 'varchar' }) accepted_eula_version: string | null; + @Column({ nullable: true, type: 'varchar' }) + accepted_privacy_notice_version: string | null; + @Column({ nullable: true, type: 'varchar' }) accepted_dpa_version: string | null; diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index af179e9..8894656 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -35,6 +35,7 @@ import { import { Account } from '../accounts/entity/account.entity'; import { Workflow } from '../workflows/entity/workflow.entity'; import { Worktype } from '../worktypes/entity/worktype.entity'; +import { Context } from '../../common/log'; @Injectable() export class UsersRepositoryService { @@ -340,7 +341,10 @@ export class UsersRepositoryService { * @param externalId * @returns User[] */ - async findSameAccountUsers(external_id: string): Promise { + async findSameAccountUsers( + external_id: string, + context: Context, + ): Promise { return await this.dataSource.transaction(async (entityManager) => { const repo = entityManager.getRepository(User); @@ -359,8 +363,9 @@ export class UsersRepositoryService { license: true, }, where: { account_id: accountId }, + comment: `${context.getTrackingId()}`, }); - + return dbUsers; }); } @@ -471,6 +476,14 @@ export class UsersRepositoryService { id: 'DESC', }, }); + const latestPrivacyNoticeInfo = await termRepo.findOne({ + where: { + document_type: TERM_TYPE.PRIVACY_NOTICE, + }, + order: { + id: 'DESC', + }, + }); const latestDpaInfo = await termRepo.findOne({ where: { document_type: TERM_TYPE.DPA, @@ -479,16 +492,18 @@ export class UsersRepositoryService { id: 'DESC', }, }); - - if (!latestEulaInfo || !latestDpaInfo) { + if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) { throw new TermInfoNotFoundError(`Terms info is not found.`); } return { tier: user.account.tier, acceptedEulaVersion: user.accepted_eula_version ?? undefined, + acceptedPrivacyNoticeVersion: + user.accepted_privacy_notice_version ?? undefined, acceptedDpaVersion: user.accepted_dpa_version ?? undefined, latestEulaVersion: latestEulaInfo.version, + latestPrivacyNoticeVersion: latestPrivacyNoticeInfo.version, latestDpaVersion: latestDpaInfo.version, }; }); @@ -498,12 +513,14 @@ export class UsersRepositoryService { * 同意済み利用規約のバージョンを更新する * @param externalId * @param eulaVersion + * @param privacyNoticeVersion * @param dpaVersion * @returns update */ async updateAcceptedTermsVersion( externalId: string, eulaVersion: string, + privacyNoticeVersion: string, dpaVersion: string | undefined, ): Promise { await this.dataSource.transaction(async (entityManager) => { @@ -531,6 +548,11 @@ export class UsersRepositoryService { if (!eulaVersion) { throw new UpdateTermsVersionNotSetError(`EULA version param not set.`); } + if (!privacyNoticeVersion) { + throw new UpdateTermsVersionNotSetError( + `PrivacyNotice version param not set.`, + ); + } if (user.account.tier !== TIERS.TIER5 && !dpaVersion) { throw new UpdateTermsVersionNotSetError( `DPA version param not set. User's tier: ${user.account.tier}`, @@ -538,6 +560,8 @@ export class UsersRepositoryService { } user.accepted_eula_version = eulaVersion; + user.accepted_privacy_notice_version = + privacyNoticeVersion ?? user.accepted_privacy_notice_version; user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version; await userRepo.update({ id: user.id }, user); });