Merge branch 'develop' into main

This commit is contained in:
maruyama.t 2023-12-07 11:35:01 +09:00
commit 20157341f6
61 changed files with 1635 additions and 264 deletions

View File

@ -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 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 \ ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
AzureFunctionsJobHost__Logging__Console__IsEnabled=true AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
BUILD_VERSION=${BUILD_VERSION}
COPY . /home/site/wwwroot
RUN cd /home/site/wwwroot && \
npm install && \
npm run build

View File

@ -83,9 +83,24 @@ jobs:
is_static_export: false is_static_export: false
verbose: false verbose: false
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN) azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
- job: smoke_test - job: function_deploy
dependsOn: frontend_deploy dependsOn: frontend_deploy
condition: succeeded('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' displayName: 'smoke test'
pool: pool:
name: odms-deploy-pipeline name: odms-deploy-pipeline

View File

@ -23,6 +23,7 @@ import AccountPage from "pages/AccountPage";
import AcceptToUsePage from "pages/TermsPage"; import AcceptToUsePage from "pages/TermsPage";
import { TemplateFilePage } from "pages/TemplateFilePage"; import { TemplateFilePage } from "pages/TemplateFilePage";
import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess"; import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess";
import SupportPage from "pages/SupportPage";
const AppRouter: React.FC = () => ( const AppRouter: React.FC = () => (
<Routes> <Routes>
@ -81,6 +82,10 @@ const AppRouter: React.FC = () => (
element={<RouteAuthGuard component={<PartnerPage />} />} element={<RouteAuthGuard component={<PartnerPage />} />}
/> />
<Route path="/accountDeleteSuccess" element={<AccountDeleteSuccess />} /> <Route path="/accountDeleteSuccess" element={<AccountDeleteSuccess />} />
<Route
path="/support"
element={<RouteAuthGuard component={<SupportPage />} />}
/>
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>

View File

@ -447,6 +447,12 @@ export interface CreateAccountRequest {
* @memberof CreateAccountRequest * @memberof CreateAccountRequest
*/ */
'acceptedEulaVersion': string; 'acceptedEulaVersion': string;
/**
*
* @type {string}
* @memberof CreateAccountRequest
*/
'acceptedPrivacyNoticeVersion': string;
/** /**
* (DPA) * (DPA)
* @type {string} * @type {string}
@ -746,6 +752,32 @@ export interface GetAuthorsResponse {
*/ */
'authors': Array<Author>; 'authors': Array<Author>;
} }
/**
*
* @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 * @export
@ -2000,6 +2032,12 @@ export interface UpdateAcceptedVersionRequest {
* @memberof UpdateAcceptedVersionRequest * @memberof UpdateAcceptedVersionRequest
*/ */
'acceptedEULAVersion': string; 'acceptedEULAVersion': string;
/**
* PrivacyNotice
* @type {string}
* @memberof UpdateAcceptedVersionRequest
*/
'acceptedPrivacyNoticeVersion': string;
/** /**
* DPA * DPA
* @type {string} * @type {string}
@ -2727,6 +2765,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @summary
* @param {GetCompanyNameRequest} getCompanyNameRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getCompanyName: async (getCompanyNameRequest: GetCompanyNameRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// 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 * @summary
@ -3488,6 +3566,19 @@ export const AccountsApiFp = function(configuration?: Configuration) {
const operationBasePath = operationServerMap['AccountsApi.getAuthors']?.[index]?.url; const operationBasePath = operationServerMap['AccountsApi.getAuthors']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); 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<GetCompanyNameResponse>> {
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 * @summary
@ -3804,6 +3895,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
getAuthors(options?: any): AxiosPromise<GetAuthorsResponse> { getAuthors(options?: any): AxiosPromise<GetAuthorsResponse> {
return localVarFp.getAuthors(options).then((request) => request(axios, basePath)); 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<GetCompanyNameResponse> {
return localVarFp.getCompanyName(getCompanyNameRequest, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @summary * @summary
@ -4092,6 +4193,18 @@ export class AccountsApi extends BaseAPI {
return AccountsApiFp(this.configuration).getAuthors(options).then((request) => request(this.axios, this.basePath)); 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 * @summary

View File

@ -7,6 +7,7 @@ export const HEADER_MENUS_LICENSE = "License";
export const HEADER_MENUS_DICTATIONS = "Dictations"; export const HEADER_MENUS_DICTATIONS = "Dictations";
export const HEADER_MENUS_WORKFLOW = "Workflow"; export const HEADER_MENUS_WORKFLOW = "Workflow";
export const HEADER_MENUS_PARTNER = "Partners"; export const HEADER_MENUS_PARTNER = "Partners";
export const HEADER_MENUS_SUPPORT = "Support";
export const HEADER_MENUS: { export const HEADER_MENUS: {
key: HeaderMenus; key: HeaderMenus;
@ -43,6 +44,11 @@ export const HEADER_MENUS: {
label: getTranslationID("common.label.headerPartners"), label: getTranslationID("common.label.headerPartners"),
path: "/partners", path: "/partners",
}, },
{
key: HEADER_MENUS_SUPPORT,
label: getTranslationID("common.label.headerSupport"),
path: "/support",
},
]; ];
export const HEADER_NAME = getTranslationID("common.label.headerName"); export const HEADER_NAME = getTranslationID("common.label.headerName");

View File

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

View File

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

View File

@ -131,6 +131,10 @@ export const dictationSlice = createSlice({
}); });
state.domain.backup.tasks = tasks; state.domain.backup.tasks = tasks;
}, },
openFilePropertyInfo: (state, action: PayloadAction<{ task: Task }>) => {
const { task } = action.payload;
state.apps.selectedFileTask = task;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(listTasksAsync.pending, (state) => { builder.addCase(listTasksAsync.pending, (state) => {
@ -225,6 +229,7 @@ export const {
changeAssignee, changeAssignee,
changeBackupTaskChecked, changeBackupTaskChecked,
changeBackupTaskAllCheched, changeBackupTaskAllCheched,
openFilePropertyInfo,
} = dictationSlice.actions; } = dictationSlice.actions;
export default dictationSlice.reducer; export default dictationSlice.reducer;

View File

@ -34,6 +34,9 @@ export const selectParamName = (state: RootState) =>
export const selectSelectedTask = (state: RootState) => export const selectSelectedTask = (state: RootState) =>
state.dictation.apps.selectedTask; state.dictation.apps.selectedTask;
export const selectSelectedFileTask = (state: RootState) =>
state.dictation.apps.selectedFileTask;
export const selectSelectedTranscriptionists = (state: RootState) => export const selectSelectedTranscriptionists = (state: RootState) =>
state.dictation.apps.assignee.selected; state.dictation.apps.assignee.selected;

View File

@ -26,6 +26,7 @@ export interface Apps {
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
selectedTask?: Task; selectedTask?: Task;
selectedFileTask?: Task;
assignee: { assignee: {
selected: Assignee[]; selected: Assignee[];
pool: Assignee[]; pool: Assignee[];

View File

@ -1,20 +1,25 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { LicenseSummaryState } from "./state"; import { LicenseSummaryState } from "./state";
import { getLicenseSummaryAsync } from "./operations"; import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations";
const initialState: LicenseSummaryState = { const initialState: LicenseSummaryState = {
domain: { domain: {
totalLicense: 0, licenseSummaryInfo: {
allocatedLicense: 0, totalLicense: 0,
reusableLicense: 0, allocatedLicense: 0,
freeLicense: 0, reusableLicense: 0,
expiringWithin14daysLicense: 0, freeLicense: 0,
issueRequesting: 0, expiringWithin14daysLicense: 0,
numberOfRequesting: 0, issueRequesting: 0,
shortage: 0, numberOfRequesting: 0,
storageSize: 0, shortage: 0,
usedSize: 0, storageSize: 0,
isStorageAvailable: false, usedSize: 0,
isStorageAvailable: false,
},
accountInfo: {
companyName: "",
},
}, },
apps: { apps: {
isLoading: false, isLoading: false,
@ -31,7 +36,10 @@ export const licenseSummarySlice = createSlice({
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => { 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;
}); });
}, },
}); });

View File

@ -5,6 +5,7 @@ import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth"; import { getAccessToken } from "features/auth";
import { import {
AccountsApi, AccountsApi,
GetCompanyNameResponse,
GetLicenseSummaryResponse, GetLicenseSummaryResponse,
PartnerLicenseInfo, PartnerLicenseInfo,
} from "../../../api/api"; } from "../../../api/api";
@ -66,3 +67,59 @@ export const getLicenseSummaryAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); 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 });
}
});

View File

@ -2,6 +2,9 @@ import { RootState } from "app/store";
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する // 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
export const selecLicenseSummaryInfo = (state: RootState) => 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; export const selectIsLoading = (state: RootState) => state.license;

View File

@ -4,17 +4,22 @@ export interface LicenseSummaryState {
} }
export interface Domain { export interface Domain {
totalLicense: number; licenseSummaryInfo: {
allocatedLicense: number; totalLicense: number;
reusableLicense: number; allocatedLicense: number;
freeLicense: number; reusableLicense: number;
expiringWithin14daysLicense: number; freeLicense: number;
issueRequesting: number; expiringWithin14daysLicense: number;
numberOfRequesting: number; issueRequesting: number;
shortage: number; numberOfRequesting: number;
storageSize: number; shortage: number;
usedSize: number; storageSize: number;
isStorageAvailable: boolean; usedSize: number;
isStorageAvailable: boolean;
};
accountInfo: {
companyName: string;
};
} }
export interface Apps { export interface Apps {

View File

@ -5,4 +5,5 @@
export const TERMS_DOCUMENT_TYPE = { export const TERMS_DOCUMENT_TYPE = {
DPA: "DPA", DPA: "DPA",
EULA: "EULA", EULA: "EULA",
PRIVACY_NOTICE: "PrivacyNotice",
} as const; } as const;

View File

@ -110,6 +110,7 @@ export const updateAcceptedVersionAsync = createAsyncThunk<
updateAccceptVersions: { updateAccceptVersions: {
acceptedVerDPA: string; acceptedVerDPA: string;
acceptedVerEULA: string; acceptedVerEULA: string;
acceptedVerPrivacyNotice: string;
}; };
}, },
{ {
@ -140,6 +141,8 @@ export const updateAcceptedVersionAsync = createAsyncThunk<
{ {
idToken, idToken,
acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA, acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA,
acceptedPrivacyNoticeVersion:
updateAccceptVersions.acceptedVerPrivacyNotice,
acceptedDPAVersion: !(TIERS.TIER5 === tier.toString()) acceptedDPAVersion: !(TIERS.TIER5 === tier.toString())
? updateAccceptVersions.acceptedVerDPA ? updateAccceptVersions.acceptedVerDPA
: undefined, : undefined,

View File

@ -14,7 +14,11 @@ export const selectTermVersions = (state: RootState) => {
(termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA (termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA
)?.version || ""; )?.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; export const selectTier = (state: RootState) => state.terms.domain.tier;

View File

@ -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<FilePropertyPopupProps> = (props) => {
const { onClose, isOpen } = props;
const [t] = useTranslation();
const isLoading = useSelector(selectIsLoading);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
onClose(false);
}, [onClose]);
const selectedFileTask = useSelector(selectSelectedFileTask);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("dictationPage.label.fileProperty"))}
<button
type="button"
onClick={closePopup}
style={{ pointerEvents: isLoading ? "none" : "auto" }}
>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<dl className={`${styles.formList} ${styles.property} ${styles.hasbg}`}>
<dt className={styles.formTitle}>
{t(getTranslationID("dictationPage.label.general"))}
</dt>
<dt>{t(getTranslationID("dictationPage.label.fileName"))}</dt>
<dd>{selectedFileTask?.fileName.replace(".zip", "") ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.fileSize"))}</dt>
<dd>{selectedFileTask?.fileSize ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.fileLength"))}</dt>
<dd>{selectedFileTask?.audioDuration ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.authorId"))}</dt>
<dd>{selectedFileTask?.authorId ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.workType"))}</dt>
<dd>{selectedFileTask?.workType ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.priority"))}</dt>
<dd>{selectedFileTask?.priority ?? ""}</dd>
<dt>
{t(getTranslationID("dictationPage.label.recordingStartedDate"))}
</dt>
<dd>{selectedFileTask?.audioCreatedDate ?? ""}</dd>
<dt>
{t(getTranslationID("dictationPage.label.recordingFinishedDate"))}
</dt>
<dd>{selectedFileTask?.audioFinishedDate ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.uploadDate"))}</dt>
<dd>{selectedFileTask?.audioUploadedDate ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.encryption"))}</dt>
<dd>
{selectedFileTask?.isEncrypted ? (
<img src={lock} alt="encrypted" />
) : (
""
)}
</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem1"))}</dt>
<dd>{selectedFileTask?.optionItemList[0].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem2"))}</dt>
<dd>{selectedFileTask?.optionItemList[1].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem3"))}</dt>
<dd>{selectedFileTask?.optionItemList[2].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem4"))}</dt>
<dd>{selectedFileTask?.optionItemList[3].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem5"))}</dt>
<dd>{selectedFileTask?.optionItemList[4].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem6"))}</dt>
<dd>{selectedFileTask?.optionItemList[5].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem7"))}</dt>
<dd>{selectedFileTask?.optionItemList[6].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem8"))}</dt>
<dd>{selectedFileTask?.optionItemList[7].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem9"))}</dt>
<dd>{selectedFileTask?.optionItemList[8].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.optionItem10"))}</dt>
<dd>{selectedFileTask?.optionItemList[9].optionItemValue}</dd>
<dt>{t(getTranslationID("dictationPage.label.comment"))}</dt>
<dd>{selectedFileTask?.comment ?? ""}</dd>
<dt className={styles.formTitle}>
{t(getTranslationID("dictationPage.label.job"))}
</dt>
<dt>{t(getTranslationID("dictationPage.label.jobNumber"))}</dt>
<dd>{selectedFileTask?.jobNumber ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.status"))}</dt>
<dd>{selectedFileTask?.status ?? ""}</dd>
<dt>
{t(
getTranslationID("dictationPage.label.transcriptionStartedDate")
)}
</dt>
<dd>{selectedFileTask?.transcriptionStartedDate ?? ""}</dd>
<dt>
{t(
getTranslationID("dictationPage.label.transcriptionFinishedDate")
)}
</dt>
<dd>{selectedFileTask?.transcriptionFinishedDate ?? ""}</dd>
<dt>{t(getTranslationID("dictationPage.label.transcriptionist"))}</dt>
<dd>{selectedFileTask?.typist?.name ?? ""}</dd>
<dd className={`${styles.full} ${styles.alignRight}`}>
<a href="" className={`${styles.buttonText}`}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
{t(getTranslationID("dictationPage.label.close"))}
</a>
</dd>
</dl>
</div>
</div>
);
};

View File

@ -23,6 +23,7 @@ import {
changeParamName, changeParamName,
changeDirection, changeDirection,
changeSelectedTask, changeSelectedTask,
openFilePropertyInfo,
SortableColumnType, SortableColumnType,
changeAssignee, changeAssignee,
listTypistsAsync, listTypistsAsync,
@ -48,6 +49,7 @@ import open_in_new from "../../assets/images/open_in_new.svg";
import { DisPlayInfo } from "./displayInfo"; import { DisPlayInfo } from "./displayInfo";
import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup"; import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup";
import { BackupPopup } from "./backupPopup"; import { BackupPopup } from "./backupPopup";
import { FilePropertyPopup } from "./filePropertyPopup";
const DictationPage: React.FC = (): JSX.Element => { const DictationPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
@ -63,6 +65,7 @@ const DictationPage: React.FC = (): JSX.Element => {
isChangeTranscriptionistPopupOpen, isChangeTranscriptionistPopupOpen,
setIsChangeTranscriptionistPopupOpen, setIsChangeTranscriptionistPopupOpen,
] = useState(false); ] = useState(false);
const [isFilePropertyPopupOpen, setIsFilePropertyPopupOpen] = useState(false);
const [isBackupPopupOpen, setIsBackupPopupOpen] = useState(false); const [isBackupPopupOpen, setIsBackupPopupOpen] = useState(false);
const onChangeTranscriptionistPopupOpen = useCallback( const onChangeTranscriptionistPopupOpen = useCallback(
@ -74,6 +77,13 @@ const DictationPage: React.FC = (): JSX.Element => {
[dispatch, setIsChangeTranscriptionistPopupOpen] [dispatch, setIsChangeTranscriptionistPopupOpen]
); );
const onClickFileProperty = useCallback(
(task: Task) => {
dispatch(openFilePropertyInfo({ task }));
setIsFilePropertyPopupOpen(true);
},
[dispatch, setIsFilePropertyPopupOpen]
);
// 各カラムの表示/非表示 // 各カラムの表示/非表示
const displayColumn = useSelector(selectDisplayInfo); const displayColumn = useSelector(selectDisplayInfo);
@ -477,6 +487,10 @@ const DictationPage: React.FC = (): JSX.Element => {
setIsBackupPopupOpen(true); setIsBackupPopupOpen(true);
}, []); }, []);
const onCloseFilePropertyPopup = useCallback(() => {
setIsFilePropertyPopupOpen(false);
}, []);
const sortIconClass = ( const sortIconClass = (
currentParam: SortableColumnType, currentParam: SortableColumnType,
currentDirection: DirectionType, currentDirection: DirectionType,
@ -532,6 +546,10 @@ const DictationPage: React.FC = (): JSX.Element => {
return ( return (
<> <>
<BackupPopup isOpen={isBackupPopupOpen} onClose={onCloseBackupPopup} /> <BackupPopup isOpen={isBackupPopupOpen} onClose={onCloseBackupPopup} />
<FilePropertyPopup
isOpen={isFilePropertyPopupOpen}
onClose={onCloseFilePropertyPopup}
/>
<ChangeTranscriptionistPopup <ChangeTranscriptionistPopup
isOpen={isChangeTranscriptionistPopupOpen} isOpen={isChangeTranscriptionistPopupOpen}
onClose={onClosePopup} onClose={onClosePopup}
@ -1080,7 +1098,8 @@ const DictationPage: React.FC = (): JSX.Element => {
</a> </a>
</li> </li>
<li> <li>
<a> {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a onClick={() => onClickFileProperty(x)}>
{t( {t(
getTranslationID( getTranslationID(
"dictationPage.label.fileProperty" "dictationPage.label.fileProperty"
@ -1360,11 +1379,12 @@ const DictationPage: React.FC = (): JSX.Element => {
<ul className={`${styles.menuAction} ${styles.alignRight}`}> <ul className={`${styles.menuAction} ${styles.alignRight}`}>
<li className={styles.alignLeft}> <li className={styles.alignLeft}>
<a <a
href="" // TODO: 将来的に正式なURLに変更する
href="/dictations"
className={`${styles.menuLink} ${styles.isActive}`} className={`${styles.menuLink} ${styles.isActive}`}
target="_blank" target="_blank"
> >
Applications {t(getTranslationID("dictationPage.label.applications"))}
<img <img
src={open_in_new} src={open_in_new}
alt="" alt=""

View File

@ -8,8 +8,10 @@ import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store"; import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
getCompanyNameAsync,
getLicenseSummaryAsync, getLicenseSummaryAsync,
selecLicenseSummaryInfo, selecLicenseSummaryInfo,
selectCompanyName,
} from "features/license/licenseSummary"; } from "features/license/licenseSummary";
import { selectSelectedRow } from "features/license/partnerLicense"; import { selectSelectedRow } from "features/license/partnerLicense";
import { selectDelegationAccessToken } from "features/auth/selectors"; import { selectDelegationAccessToken } from "features/auth/selectors";
@ -61,9 +63,11 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
// apiからの値取得関係 // apiからの値取得関係
const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo); const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo);
const companyName = useSelector(selectCompanyName);
useEffect(() => { useEffect(() => {
dispatch(getLicenseSummaryAsync({ selectedRow })); dispatch(getLicenseSummaryAsync({ selectedRow }));
dispatch(getCompanyNameAsync({ selectedRow }));
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch]); }, [dispatch]);
@ -118,7 +122,7 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
</div> </div>
<section className={styles.license}> <section className={styles.license}>
<div> <div>
<h2 className="">{"会社名" /* TODO 会社名を表示する */}</h2> <h2 className="">{companyName}</h2>
<ul className={styles.menuAction}> <ul className={styles.menuAction}>
<li> <li>
{/* 他アカウントのライセンス情報を見ている場合は、前画面に戻る用のreturnボタンを表示 */} {/* 他アカウントのライセンス情報を見ている場合は、前画面に戻る用のreturnボタンを表示 */}

View File

@ -40,6 +40,7 @@ const SignupConfirm: React.FC = (): JSX.Element => {
adminMail, adminMail,
adminPassword, adminPassword,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion: "",
acceptedDpaVersion: "", acceptedDpaVersion: "",
token: "", token: "",
}) })

View File

@ -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 (
<div className={styles.wrap}>
<Header />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("supportPage.label.title"))}
</h1>
</div>
<section className={styles.support}>
<div>
<h2>{t(getTranslationID("supportPage.label.howToUse"))}</h2>
<div className={styles.txContents}>
<ul className={styles.listDocument}>
<li>
<a
// TODO: 将来的に正式なURLに変更する
href="/support"
target="_blank"
className={styles.linkTx}
>
{t(
getTranslationID("supportPage.label.supportPageEnglish")
)}
</a>
</li>
<li>
<a
// TODO: 将来的に正式なURLに変更する
href="/support"
target="_blank"
className={styles.linkTx}
>
{t(
getTranslationID("supportPage.label.supportPageGerman")
)}
</a>
</li>
<li>
<a
// TODO: 将来的に正式なURLに変更する
href="/support"
target="_blank"
className={styles.linkTx}
>
{t(
getTranslationID("supportPage.label.supportPageFrench")
)}
</a>
</li>
<li>
<a
// TODO: 将来的に正式なURLに変更する
href="/support"
target="_blank"
className={styles.linkTx}
>
{t(
getTranslationID("supportPage.label.supportPageSpanish")
)}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(getTranslationID("supportPage.text.notResolved"))}
</p>
</div>
</div>
</section>
</div>
</main>
</div>
);
};
export default SupportPage;

View File

@ -29,9 +29,12 @@ const TermsPage: React.FC = (): JSX.Element => {
const tier = useSelector(selectTier); const tier = useSelector(selectTier);
const [isCheckedEula, setIsCheckedEula] = useState(false); const [isCheckedEula, setIsCheckedEula] = useState(false);
const [isCheckedPrivacyNotice, setIsCheckedPrivacyNotice] = useState(false);
const [isCheckedDpa, setIsCheckedDpa] = useState(false); const [isCheckedDpa, setIsCheckedDpa] = useState(false);
const [isClickedEulaLink, setIsClickedEulaLink] = useState(false); const [isClickedEulaLink, setIsClickedEulaLink] = useState(false);
const [isClickedPrivacyNoticeLink, setIsClickedPrivacyNoticeLink] =
useState(false);
const [isClickedDpaLink, setIsClickedDpaLink] = useState(false); const [isClickedDpaLink, setIsClickedDpaLink] = useState(false);
// 画面起動時 // 画面起動時
@ -52,9 +55,9 @@ const TermsPage: React.FC = (): JSX.Element => {
// ボタン押下可否判定ロジック // ボタン押下可否判定ロジック
const canClickButton = () => { const canClickButton = () => {
if (isTier5()) { 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 ( if (
localStorageKeyforIdToken && localStorageKeyforIdToken &&
updateAccceptVersions.acceptedVerDPA !== "" && updateAccceptVersions.acceptedVerDPA !== "" &&
updateAccceptVersions.acceptedVerEULA !== "" updateAccceptVersions.acceptedVerEULA !== "" &&
updateAccceptVersions.acceptedVerPrivacyNotice !== ""
) { ) {
const { meta } = await dispatch( const { meta } = await dispatch(
updateAcceptedVersionAsync({ updateAcceptedVersionAsync({
@ -132,7 +136,42 @@ const TermsPage: React.FC = (): JSX.Element => {
</label> </label>
</p> </p>
</dd> </dd>
{/* 第五階層以外の場合はEulaのリンクをあわせて表示する */} <dd className={`${styles.full} ${styles.alignCenter}`}>
<p>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
href="/" /* TODO PrivacyNotice用の利用規約リンクが決定したら設定を行う */
target="_blank"
className={styles.linkTx}
onClick={() => setIsClickedPrivacyNoticeLink(true)}
data-tag="open-pricacy-notice"
>
{t(
getTranslationID("termsPage.label.linkOfPrivacyNotice")
)}
</a>
{` ${t(getTranslationID("termsPage.label.forOdds"))}`}
</p>
<p>
<label>
<input
type="checkbox"
checked={isCheckedPrivacyNotice}
className={styles.formCheck}
value=""
onChange={(e) =>
setIsCheckedPrivacyNotice(e.target.checked)
}
disabled={!isClickedPrivacyNoticeLink}
data-tag="accept-privacy-notice"
/>
{t(
getTranslationID("termsPage.label.checkBoxForConsent")
)}
</label>
</p>
</dd>
{/* 第五階層以外の場合はDPAのリンクをあわせて表示する */}
{!isTier5() && ( {!isTier5() && (
<dd className={`${styles.full} ${styles.alignCenter}`}> <dd className={`${styles.full} ${styles.alignCenter}`}>
<p> <p>

View File

@ -2306,8 +2306,7 @@ tr.isSelected .menuInTable li a.isDisable {
} }
.formChange ul.chooseMember li input + label:hover, .formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover { .formChange ul.holdMember li input + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
center;
background-size: 1.3rem; background-size: 1.3rem;
} }
.formChange ul.chooseMember li input:checked + label, .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.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover { .formChange ul.holdMember li input:checked + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
right center; center;
background-size: 1.3rem; background-size: 1.3rem;
} }
.formChange > p { .formChange > p {
@ -2472,8 +2471,7 @@ tr.isSelected .menuInTable li a.isDisable {
} }
.formChange ul.chooseMember li input + label:hover, .formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover { .formChange ul.holdMember li input + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
center;
background-size: 1.3rem; background-size: 1.3rem;
} }
.formChange ul.chooseMember li input:checked + label, .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.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover { .formChange ul.holdMember li input:checked + label:hover {
background: #e6e6e6 url(../images/arrow_circle_right.svg) no-repeat right background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
center; right center;
background-size: 1.3rem; background-size: 1.3rem;
} }
.formChange > p { .formChange > p {

View File

@ -25,6 +25,7 @@
"headerDictations": "(de)Dictations", "headerDictations": "(de)Dictations",
"headerWorkflow": "(de)Workflow", "headerWorkflow": "(de)Workflow",
"headerPartners": "(de)Partners", "headerPartners": "(de)Partners",
"headerSupport": "(de)Support",
"tier1": "(de)Admin", "tier1": "(de)Admin",
"tier2": "(de)BC", "tier2": "(de)BC",
"tier3": "(de)Distributor", "tier3": "(de)Distributor",
@ -250,7 +251,11 @@
"poolTranscriptionist": "Transkriptionsliste", "poolTranscriptionist": "Transkriptionsliste",
"fileBackup": "(de)File Backup", "fileBackup": "(de)File Backup",
"downloadForBackup": "(de)Download for 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": { "cardLicenseIssuePopupPage": {
@ -527,9 +532,31 @@
"title": "(de)Terms of Use has updated. Please confirm again.", "title": "(de)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(de)Click here to read the terms of use.", "linkOfEula": "(de)Click here to read the terms of use.",
"linkOfDpa": "(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.", "checkBoxForConsent": "(de)Yes, I agree to the terms of use.",
"forOdds": "(de)for ODDS.", "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"
} }
} }
} }

View File

@ -25,6 +25,7 @@
"headerDictations": "Dictations", "headerDictations": "Dictations",
"headerWorkflow": "Workflow", "headerWorkflow": "Workflow",
"headerPartners": "Partners", "headerPartners": "Partners",
"headerSupport": "Support",
"tier1": "Admin", "tier1": "Admin",
"tier2": "BC", "tier2": "BC",
"tier3": "Distributor", "tier3": "Distributor",
@ -250,7 +251,11 @@
"poolTranscriptionist": "Transcription List", "poolTranscriptionist": "Transcription List",
"fileBackup": "File Backup", "fileBackup": "File Backup",
"downloadForBackup": "Download for backup", "downloadForBackup": "Download for backup",
"cancelDictation": "Cancel Dictation" "applications": "Applications",
"cancelDictation": "Cancel Dictation",
"general": "General",
"job": "Job",
"close": "Close"
} }
}, },
"cardLicenseIssuePopupPage": { "cardLicenseIssuePopupPage": {
@ -527,9 +532,31 @@
"title": "Terms of Use has updated. Please confirm again.", "title": "Terms of Use has updated. Please confirm again.",
"linkOfEula": "Click here to read the terms of use.", "linkOfEula": "Click here to read the terms of use.",
"linkOfDpa": "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.", "checkBoxForConsent": "Yes, I agree to the terms of use.",
"forOdds": "for ODDS.", "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"
} }
} }
} }

View File

@ -25,6 +25,7 @@
"headerDictations": "(es)Dictations", "headerDictations": "(es)Dictations",
"headerWorkflow": "(es)Workflow", "headerWorkflow": "(es)Workflow",
"headerPartners": "(es)Partners", "headerPartners": "(es)Partners",
"headerSupport": "(es)Support",
"tier1": "(es)Admin", "tier1": "(es)Admin",
"tier2": "(es)BC", "tier2": "(es)BC",
"tier3": "(es)Distributor", "tier3": "(es)Distributor",
@ -250,7 +251,11 @@
"poolTranscriptionist": "Lista de transcriptor", "poolTranscriptionist": "Lista de transcriptor",
"fileBackup": "(es)File Backup", "fileBackup": "(es)File Backup",
"downloadForBackup": "(es)Download for 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": { "cardLicenseIssuePopupPage": {
@ -527,9 +532,31 @@
"title": "(es)Terms of Use has updated. Please confirm again.", "title": "(es)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(es)Click here to read the terms of use.", "linkOfEula": "(es)Click here to read the terms of use.",
"linkOfDpa": "(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.", "checkBoxForConsent": "(es)Yes, I agree to the terms of use.",
"forOdds": "(es)for ODDS.", "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"
} }
} }
} }

View File

@ -25,6 +25,7 @@
"headerDictations": "(fr)Dictations", "headerDictations": "(fr)Dictations",
"headerWorkflow": "(fr)Workflow", "headerWorkflow": "(fr)Workflow",
"headerPartners": "(fr)Partners", "headerPartners": "(fr)Partners",
"headerSupport": "(fr)Support",
"tier1": "(fr)Admin", "tier1": "(fr)Admin",
"tier2": "(fr)BC", "tier2": "(fr)BC",
"tier3": "(fr)Distributor", "tier3": "(fr)Distributor",
@ -250,7 +251,11 @@
"poolTranscriptionist": "Liste de transcriptionniste", "poolTranscriptionist": "Liste de transcriptionniste",
"fileBackup": "(fr)File Backup", "fileBackup": "(fr)File Backup",
"downloadForBackup": "(fr)Download for 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": { "cardLicenseIssuePopupPage": {
@ -527,9 +532,31 @@
"title": "(fr)Terms of Use has updated. Please confirm again.", "title": "(fr)Terms of Use has updated. Please confirm again.",
"linkOfEula": "(fr)Click here to read the terms of use.", "linkOfEula": "(fr)Click here to read the terms of use.",
"linkOfDpa": "(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.", "checkBoxForConsent": "(fr)Yes, I agree to the terms of use.",
"forOdds": "(fr)for ODDS.", "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"
} }
} }
} }

View File

@ -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

View File

@ -11,5 +11,10 @@
"extensionBundle": { "extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle", "id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)" "version": "[4.*, 5.0.0)"
},
"retry": {
"strategy": "fixedDelay",
"maxRetryCount": 3,
"delayInterval": "00:00:10"
} }
} }

View File

@ -5,8 +5,8 @@ import { AdB2cResponse, AdB2cUser } from "./types/types";
import { error } from "console"; import { error } from "console";
import { makeADB2CKey, restoreAdB2cID } from "../common/cache"; import { makeADB2CKey, restoreAdB2cID } from "../common/cache";
import { promisify } from "util"; import { promisify } from "util";
import { createRedisClient } from "../redis/redis";
import { InvocationContext } from "@azure/functions"; import { InvocationContext } from "@azure/functions";
import { RedisClient } from "redis";
export class Adb2cTooManyRequestsError extends Error {} export class Adb2cTooManyRequestsError extends Error {}
@ -23,16 +23,24 @@ export class AdB2cService {
) { ) {
throw error; throw error;
} }
const credential = new ClientSecretCredential( try {
process.env.ADB2C_TENANT_ID, const credential = new ClientSecretCredential(
process.env.ADB2C_CLIENT_ID, process.env.ADB2C_TENANT_ID,
process.env.ADB2C_CLIENT_SECRET process.env.ADB2C_CLIENT_ID,
); process.env.ADB2C_CLIENT_SECRET
const authProvider = new TokenCredentialAuthenticationProvider(credential, { );
scopes: ["https://graph.microsoft.com/.default"], 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( async getUsers(
context: InvocationContext, context: InvocationContext,
redisClient: RedisClient,
externalIds: string[] externalIds: string[]
): Promise<AdB2cUser[] | undefined> { ): Promise<AdB2cUser[]> {
const redisClient = createRedisClient();
try { try {
const b2cUsers: AdB2cUser[] = []; const b2cUsers: AdB2cUser[] = [];
const keys = externalIds.map((externalId) => makeADB2CKey(externalId)); const keys = externalIds.map((externalId) => makeADB2CKey(externalId));
@ -123,7 +131,7 @@ export class AdB2cService {
return [...cachedUsers, ...b2cUsers]; return [...cachedUsers, ...b2cUsers];
} else { } else {
return undefined; return b2cUsers;
} }
} catch (e) { } catch (e) {
const { statusCode } = e; const { statusCode } = e;
@ -132,7 +140,6 @@ export class AdB2cService {
} }
throw e; throw e;
} finally { } finally {
redisClient.quit;
} }
} }
} }

View File

@ -1 +1,5 @@
export const ADB2C_PREFIX = "adb2c-external-id:" 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にキャッシュする値

View File

@ -1,4 +1,4 @@
import { ADB2C_PREFIX } from './constants'; import { ADB2C_PREFIX, SEND_COMPLETE_PREFIX } from "./constants";
/** /**
* ADB2Cのユーザー格納用のキーを生成する * ADB2Cのユーザー格納用のキーを生成する
@ -6,8 +6,8 @@ import { ADB2C_PREFIX } from './constants';
* @returns * @returns
*/ */
export const makeADB2CKey = (externalId: string): string => { export const makeADB2CKey = (externalId: string): string => {
return `${ADB2C_PREFIX}${externalId}`; return `${ADB2C_PREFIX}${externalId}`;
} };
/** /**
* ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する * ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する
@ -15,5 +15,20 @@ export const makeADB2CKey = (externalId: string): string => {
* @returns ID * @returns ID
*/ */
export const restoreAdB2cID = (key: string): string => { export const restoreAdB2cID = (key: string): string => {
return key.replace(ADB2C_PREFIX, ''); 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}`;
};

View File

@ -19,36 +19,166 @@ import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents
import { AdB2cService } from "../adb2c/adb2c"; import { AdB2cService } from "../adb2c/adb2c";
import { SendGridService } from "../sendgrid/sendgrid"; import { SendGridService } from "../sendgrid/sendgrid";
import { getMailFrom } from "../common/getEnv/getEnv"; 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( export async function licenseAlertProcessing(
context: InvocationContext, context: InvocationContext,
datasource: DataSource, datasource: DataSource,
redisClient: RedisClient,
sendgrid: SendGridService, sendgrid: SendGridService,
adb2c: AdB2cService adb2c: AdB2cService
) { ) {
context.log("[IN]licenseAlertProcessing"); try {
const mailFrom = getMailFrom(); context.log("[IN]licenseAlertProcessing");
const accountRepository = datasource.getRepository(Account);
// 第五のアカウントを取得 // redisのキー用
const accounts = await accountRepository.find({ const currentDate = new DateWithZeroTime();
where: { const formattedDate = `${currentDate.getFullYear()}-${(
tier: TIERS.TIER5, currentDate.getMonth() + 1
}, ).toString()}-${currentDate.getDate().toString()}`;
relations: { const keysAsync = promisify(redisClient.keys).bind(redisClient);
primaryAdminUser: true,
secondaryAdminUser: true,
},
});
const licenseRepository = datasource.getRepository(License); // メール送信対象のアカウント情報を取得
const currentDate = new DateWithZeroTime(); const sendTargetAccounts = await getAlertMailTargetAccount(
const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); context,
const currentDateWithZeroTime = new DateWithZeroTime(); datasource
const currentDateWithDayEndTime = new DateWithDayEndTime(); );
const sendTargetAccounts = [] as accountInfo[];
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<void> {
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<accountInfo[]> {
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) { for (const account of accounts) {
// 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
const allocatableLicenseWithMargin = await licenseRepository.count({ const allocatableLicenseWithMargin = await licenseRepository.count({
@ -109,6 +239,7 @@ export async function licenseAlertProcessing(
let primaryAdminExternalId: string | undefined; let primaryAdminExternalId: string | undefined;
let secondaryAdminExternalId: string | undefined; let secondaryAdminExternalId: string | undefined;
let parentCompanyName: string | undefined; let parentCompanyName: string | undefined;
if (shortage !== 0 || userCount !== 0) { if (shortage !== 0 || userCount !== 0) {
primaryAdminExternalId = account.primaryAdminUser primaryAdminExternalId = account.primaryAdminUser
? account.primaryAdminUser.external_id ? account.primaryAdminUser.external_id
@ -143,12 +274,33 @@ export async function licenseAlertProcessing(
secondaryAdminEmail: undefined, secondaryAdminEmail: undefined,
}); });
} }
}; return sendTargetAccounts;
await counts(); } 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<accountInfo[]> {
// ADB2Cからユーザーを取得する用の外部ID配列を作成 // ADB2Cからユーザーを取得する用の外部ID配列を作成
const externalIds = [] as string[]; const externalIds = [] as string[];
sendTargetAccounts.map((x) => { sendTargetAccounts.forEach((x) => {
if (x.primaryAdminExternalId) { if (x.primaryAdminExternalId) {
externalIds.push(x.primaryAdminExternalId); externalIds.push(x.primaryAdminExternalId);
} }
@ -156,11 +308,10 @@ export async function licenseAlertProcessing(
externalIds.push(x.secondaryAdminExternalId); externalIds.push(x.secondaryAdminExternalId);
} }
}); });
const adb2cUsers = await adb2c.getUsers(context, externalIds); const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds);
if (!adb2cUsers) { if (adb2cUsers.length === 0) {
context.log("Target user not found"); context.log("Target user not found");
context.log("[OUT]licenseAlertProcessing"); return [];
return;
} }
// ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ // ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ
sendTargetAccounts.map((info) => { 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<void> {
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) { for (const targetAccount of sendTargetAccounts) {
// プライマリ管理者が入っているかチェック // プライマリ管理者が入っているかチェック
// 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ // 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ
if (targetAccount.primaryAdminExternalId) { if (targetAccount.primaryAdminExternalId) {
// メール送信 // メール送信
// strictNullChecks対応 // strictNullChecks対応
if (targetAccount.primaryAdminEmail) { if (!targetAccount.primaryAdminEmail) {
// ライセンス不足メール continue;
if (targetAccount.shortage !== 0) { }
// ライセンス不足メール
if (targetAccount.shortage !== 0) {
// redisに送信履歴がない場合のみ送信する
const mailResult = await getAsync(
makeSendCompKey(
formattedDate,
targetAccount.primaryAdminExternalId,
MAIL_U103
)
);
if (mailResult !== DONE) {
const { subject, text, html } = const { subject, text, html } =
await createMailContentOfLicenseShortage( await createMailContentOfLicenseShortage(
targetAccount.companyName, targetAccount.companyName,
@ -217,45 +405,107 @@ export async function licenseAlertProcessing(
context.log( context.log(
`Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}` `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( context.log(
`Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}` `Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}`
); );
} throw e;
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
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}`
);
}
}
} }
} }
// ライセンス失効警告メール // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
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 } = const { subject, text, html } =
await createMailContentOfLicenseExpiringSoon( await createMailContentOfLicenseExpiringSoon(
targetAccount.companyName, targetAccount.companyName,
@ -274,80 +524,99 @@ export async function licenseAlertProcessing(
context.log( context.log(
`Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}` `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( context.log(
`Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}` `Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}`
); );
throw e;
} }
}
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
if (targetAccount.secondaryAdminEmail) { if (
// ライセンス不足メール targetAccount.secondaryAdminEmail &&
if (targetAccount.shortage !== 0) { targetAccount.secondaryAdminExternalId
const { subject, text, html } = ) {
await createMailContentOfLicenseExpiringSoon( // redisに送信履歴がない場合のみ送信する
targetAccount.companyName, const mailResult = makeSendCompKey(
targetAccount.userCountOfLicenseExpiringSoon, formattedDate,
targetAccount.parentCompanyName 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 { try {
await sendgrid.sendMail( const key = makeSendCompKey(
targetAccount.secondaryAdminEmail, formattedDate,
mailFrom, targetAccount.secondaryAdminExternalId,
subject, MAIL_U104
text,
html
); );
await setexAsync(key, ttl, DONE);
context.log( 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( 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<void> {
context.log("[IN]licenseAlert");
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
const datasource = new DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [User, Account, License],
});
await datasource.initialize();
const adb2c = new AdB2cService();
const sendgrid = new SendGridService();
try {
await licenseAlertProcessing(context, datasource, sendgrid, adb2c);
} catch (e) { } catch (e) {
context.log("licenseAlertProcessing failed"); context.log("sendAlertMail failed.");
context.error(e); throw e;
} finally { } finally {
await datasource.destroy(); context.log("[OUT]sendAlertMail");
context.log("[OUT]licenseAlert");
} }
} }

View File

@ -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<void> {
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,
});

View File

@ -20,12 +20,34 @@ export const createRedisClient = (): RedisClient => {
host: host, host: host,
port: port, port: port,
password: password, 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 { } else {
client = createClient({ client = createClient({
url: `rediss://${host}:${port}`, url: `rediss://${host}:${port}`,
password: password, password: password,
tls: {}, 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; // リトライを終了
}
},
}); });
} }

View File

@ -13,6 +13,8 @@ import { ADB2C_SIGN_IN_TYPE } from "../constants";
import { SendGridService } from "../sendgrid/sendgrid"; import { SendGridService } from "../sendgrid/sendgrid";
import { AdB2cService } from "../adb2c/adb2c"; import { AdB2cService } from "../adb2c/adb2c";
import { InvocationContext } from "@azure/functions"; import { InvocationContext } from "@azure/functions";
import { RedisClient } from "redis";
import { createRedisClient } from "../redis/redis";
describe("licenseAlert", () => { describe("licenseAlert", () => {
dotenv.config({ path: ".env" }); dotenv.config({ path: ".env" });
@ -40,6 +42,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext(); const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService; const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService; const adb2cMock = new AdB2cServiceMock() as AdB2cService;
const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定 // 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail"); const spySend = jest.spyOn(sendgridMock, "sendMail");
@ -63,8 +66,15 @@ describe("licenseAlert", () => {
null null
); );
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); await licenseAlertProcessing(
context,
source,
redisClient,
sendgridMock,
adb2cMock
);
expect(spySend.mock.calls).toHaveLength(1); expect(spySend.mock.calls).toHaveLength(1);
redisClient.quit;
}); });
it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => { it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => {
@ -72,6 +82,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext(); const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService; const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService; const adb2cMock = new AdB2cServiceMock() as AdB2cService;
const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定 // 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail"); const spySend = jest.spyOn(sendgridMock, "sendMail");
@ -96,8 +107,15 @@ describe("licenseAlert", () => {
null null
); );
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); await licenseAlertProcessing(
context,
source,
redisClient,
sendgridMock,
adb2cMock
);
expect(spySend.mock.calls).toHaveLength(2); expect(spySend.mock.calls).toHaveLength(2);
redisClient.quit;
}); });
it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => { it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => {
@ -105,6 +123,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext(); const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService; const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService; const adb2cMock = new AdB2cServiceMock() as AdB2cService;
const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定 // 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail"); const spySend = jest.spyOn(sendgridMock, "sendMail");
@ -142,8 +161,15 @@ describe("licenseAlert", () => {
null null
); );
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); await licenseAlertProcessing(
context,
source,
redisClient,
sendgridMock,
adb2cMock
);
expect(spySend.mock.calls).toHaveLength(0); expect(spySend.mock.calls).toHaveLength(0);
redisClient.quit;
}); });
it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => { it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => {
@ -151,6 +177,7 @@ describe("licenseAlert", () => {
const context = new InvocationContext(); const context = new InvocationContext();
const sendgridMock = new SendGridServiceMock() as SendGridService; const sendgridMock = new SendGridServiceMock() as SendGridService;
const adb2cMock = new AdB2cServiceMock() as AdB2cService; const adb2cMock = new AdB2cServiceMock() as AdB2cService;
const redisClient = createRedisClient();
// 呼び出し回数でテスト成否を判定 // 呼び出し回数でテスト成否を判定
const spySend = jest.spyOn(sendgridMock, "sendMail"); const spySend = jest.spyOn(sendgridMock, "sendMail");
@ -175,8 +202,15 @@ describe("licenseAlert", () => {
null null
); );
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); await licenseAlertProcessing(
context,
source,
redisClient,
sendgridMock,
adb2cMock
);
expect(spySend.mock.calls).toHaveLength(1); expect(spySend.mock.calls).toHaveLength(1);
redisClient.quit;
}); });
}); });
@ -211,6 +245,7 @@ export class AdB2cServiceMock {
*/ */
async getUsers( async getUsers(
context: InvocationContext, context: InvocationContext,
redisClient: RedisClient,
externalIds: string[] externalIds: string[]
): Promise<AdB2cUser[]> { ): Promise<AdB2cUser[]> {
const AdB2cMockUsers: AdB2cUser[] = [ const AdB2cMockUsers: AdB2cUser[] = [

View File

@ -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;

View File

@ -1455,6 +1455,52 @@
"tags": ["accounts"] "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": { "/users/confirm": {
"post": { "post": {
"operationId": "confirmUser", "operationId": "confirmUser",
@ -3491,6 +3537,10 @@
"type": "string", "type": "string",
"description": "同意済み利用規約のバージョン(EULA)" "description": "同意済み利用規約のバージョン(EULA)"
}, },
"acceptedPrivacyNoticeVersion": {
"type": "string",
"description": "同意済みプライバシーポリシーのバージョン"
},
"acceptedDpaVersion": { "acceptedDpaVersion": {
"type": "string", "type": "string",
"description": "同意済み利用規約のバージョン(DPA)" "description": "同意済み利用規約のバージョン(DPA)"
@ -3504,6 +3554,7 @@
"adminMail", "adminMail",
"adminPassword", "adminPassword",
"acceptedEulaVersion", "acceptedEulaVersion",
"acceptedPrivacyNoticeVersion",
"acceptedDpaVersion", "acceptedDpaVersion",
"token" "token"
] ]
@ -4009,6 +4060,16 @@
"properties": { "tier": { "type": "number", "description": "階層" } }, "properties": { "tier": { "type": "number", "description": "階層" } },
"required": ["tier"] "required": ["tier"]
}, },
"GetCompanyNameRequest": {
"type": "object",
"properties": { "accountId": { "type": "number" } },
"required": ["accountId"]
},
"GetCompanyNameResponse": {
"type": "object",
"properties": { "companyName": { "type": "string" } },
"required": ["companyName"]
},
"ConfirmRequest": { "ConfirmRequest": {
"type": "object", "type": "object",
"properties": { "token": { "type": "string" } }, "properties": { "token": { "type": "string" } },
@ -4236,12 +4297,20 @@
"type": "string", "type": "string",
"description": "更新バージョンEULA" "description": "更新バージョンEULA"
}, },
"acceptedPrivacyNoticeVersion": {
"type": "string",
"description": "更新バージョンPrivacyNotice"
},
"acceptedDPAVersion": { "acceptedDPAVersion": {
"type": "string", "type": "string",
"description": "更新バージョンDPA" "description": "更新バージョンDPA"
} }
}, },
"required": ["idToken", "acceptedEULAVersion"] "required": [
"idToken",
"acceptedEULAVersion",
"acceptedPrivacyNoticeVersion"
]
}, },
"UpdateAcceptedVersionResponse": { "type": "object", "properties": {} }, "UpdateAcceptedVersionResponse": { "type": "object", "properties": {} },
"GetMyUserResponse": { "GetMyUserResponse": {

View File

@ -182,6 +182,8 @@ export const makeTestAccount = async (
role: d?.role ?? 'admin none', role: d?.role ?? 'admin none',
author_id: d?.author_id ?? undefined, author_id: d?.author_id ?? undefined,
accepted_eula_version: d?.accepted_eula_version ?? '1.0', 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', accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
email_verified: d?.email_verified ?? true, email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true, auto_renew: d?.auto_renew ?? true,

View File

@ -287,6 +287,7 @@ export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
export const TERM_TYPE = { export const TERM_TYPE = {
EULA: 'EULA', EULA: 'EULA',
DPA: 'DPA', DPA: 'DPA',
PRIVACY_NOTICE: 'PrivacyNotice',
} as const; } as const;
/** /**

View File

@ -68,6 +68,8 @@ import {
GetAccountInfoMinimalAccessResponse, GetAccountInfoMinimalAccessResponse,
DeleteWorktypeRequestParam, DeleteWorktypeRequestParam,
DeleteWorktypeResponse, DeleteWorktypeResponse,
GetCompanyNameRequest,
GetCompanyNameResponse,
} from './types/types'; } from './types/types';
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
import { AuthGuard } from '../../common/guards/auth/authguards'; import { AuthGuard } from '../../common/guards/auth/authguards';
@ -116,6 +118,7 @@ export class AccountsController {
adminPassword, adminPassword,
adminName, adminName,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
} = body; } = body;
const role = USER_ROLES.NONE; const role = USER_ROLES.NONE;
@ -132,6 +135,7 @@ export class AccountsController {
adminName, adminName,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
@ -1550,4 +1554,56 @@ export class AccountsController {
); );
return { tier }; 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<GetCompanyNameResponse> {
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;
}
} }

View File

@ -112,6 +112,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'none'; const role = 'none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -144,6 +145,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
// 作成したアカウントのIDが返ってくるか確認 // 作成したアカウントのIDが返ってくるか確認
@ -161,6 +163,9 @@ describe('createAccount', () => {
expect(account?.primary_admin_user_id).toBe(user?.id); expect(account?.primary_admin_user_id).toBe(user?.id);
expect(account?.secondary_admin_user_id).toBe(null); expect(account?.secondary_admin_user_id).toBe(null);
expect(user?.accepted_eula_version).toBe(acceptedEulaVersion); expect(user?.accepted_eula_version).toBe(acceptedEulaVersion);
expect(user?.accepted_privacy_notice_version).toBe(
acceptedPrivacyNoticeVersion,
);
expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion); expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion);
expect(user?.account_id).toBe(accountId); expect(user?.account_id).toBe(accountId);
expect(user?.role).toBe(role); expect(user?.role).toBe(role);
@ -195,6 +200,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'admin none'; const role = 'admin none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -216,6 +222,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } catch (e) {
@ -264,6 +271,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'admin none'; const role = 'admin none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -286,6 +294,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } catch (e) {
@ -318,6 +327,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'none'; const role = 'none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -345,6 +355,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } catch (e) {
@ -384,6 +395,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'none'; const role = 'none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -411,6 +423,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } catch (e) {
@ -452,6 +465,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'none'; const role = 'none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -480,6 +494,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } catch (e) {
@ -520,6 +535,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'none'; const role = 'none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -551,6 +567,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } catch (e) {
@ -593,6 +610,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'none'; const role = 'none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -641,6 +659,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } catch (e) {
@ -689,6 +708,7 @@ describe('createAccount', () => {
const username = 'dummy_username'; const username = 'dummy_username';
const role = 'none'; const role = 'none';
const acceptedEulaVersion = '1.0.0'; const acceptedEulaVersion = '1.0.0';
const acceptedPrivacyNoticeVersion = '1.0.0';
const acceptedDpaVersion = '1.0.0'; const acceptedDpaVersion = '1.0.0';
overrideAdB2cService(service, { overrideAdB2cService(service, {
@ -734,6 +754,7 @@ describe('createAccount', () => {
username, username,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
} catch (e) { } 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>(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>(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();
}
}
});
});

View File

@ -34,6 +34,7 @@ import {
PostWorktypeOptionItem, PostWorktypeOptionItem,
Author, Author,
Partner, Partner,
GetCompanyNameResponse,
} from './types/types'; } from './types/types';
import { import {
DateWithZeroTime, DateWithZeroTime,
@ -175,6 +176,7 @@ export class AccountsService {
username: string, username: string,
role: string, role: string,
acceptedEulaVersion: string, acceptedEulaVersion: string,
acceptedPrivacyNoticeVersion: string,
acceptedDpaVersion: string, acceptedDpaVersion: string,
): Promise<{ accountId: number; userId: number; externalUserId: string }> { ): Promise<{ accountId: number; userId: number; externalUserId: string }> {
this.logger.log( this.logger.log(
@ -184,6 +186,7 @@ export class AccountsService {
`dealerAccountId: ${dealerAccountId}, ` + `dealerAccountId: ${dealerAccountId}, ` +
`role: ${role}, ` + `role: ${role}, ` +
`acceptedEulaVersion: ${acceptedEulaVersion}, ` + `acceptedEulaVersion: ${acceptedEulaVersion}, ` +
`acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` +
`acceptedDpaVersion: ${acceptedDpaVersion} };`, `acceptedDpaVersion: ${acceptedDpaVersion} };`,
); );
try { try {
@ -232,6 +235,7 @@ export class AccountsService {
externalUser.sub, externalUser.sub,
role, role,
acceptedEulaVersion, acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
); );
account = newAccount; account = newAccount;
@ -2151,4 +2155,51 @@ export class AccountsService {
); );
} }
} }
/**
*
* @param accountId
* @returns CompanyName
*/
async getCompanyName(
context: Context,
accountId: number,
): Promise<GetCompanyNameResponse> {
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}`,
);
}
}
} }

View File

@ -45,6 +45,8 @@ export class CreateAccountRequest {
adminPassword: string; adminPassword: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' }) @ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' })
acceptedEulaVersion: string; acceptedEulaVersion: string;
@ApiProperty({ description: '同意済みプライバシーポリシーのバージョン' })
acceptedPrivacyNoticeVersion: string;
@ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' }) @ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' })
acceptedDpaVersion: string; acceptedDpaVersion: string;
@ApiProperty({ description: 'reCAPTCHA Token' }) @ApiProperty({ description: 'reCAPTCHA Token' })
@ -599,3 +601,13 @@ export class GetAccountInfoMinimalAccessResponse {
@ApiProperty({ description: '階層' }) @ApiProperty({ description: '階層' })
tier: number; tier: number;
} }
export class GetCompanyNameRequest {
@ApiProperty()
@IsInt()
@Type(() => Number)
accountId: number;
}
export class GetCompanyNameResponse {
@ApiProperty()
companyName: string;
}

View File

@ -196,6 +196,7 @@ describe('checkIsAcceptedLatestVersion', () => {
}; };
await createTermInfo(source, 'EULA', '1.0'); await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0'); await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken); const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true); expect(result).toBe(true);
@ -219,6 +220,7 @@ describe('checkIsAcceptedLatestVersion', () => {
}; };
await createTermInfo(source, 'EULA', '1.0'); await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0'); await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken); const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(true); expect(result).toBe(true);
@ -242,6 +244,7 @@ describe('checkIsAcceptedLatestVersion', () => {
}; };
await createTermInfo(source, 'EULA', '1.1'); await createTermInfo(source, 'EULA', '1.1');
await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0'); await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken); const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false); expect(result).toBe(false);
@ -265,6 +268,7 @@ describe('checkIsAcceptedLatestVersion', () => {
}; };
await createTermInfo(source, 'EULA', '1.1'); await createTermInfo(source, 'EULA', '1.1');
await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.0'); await createTermInfo(source, 'DPA', '1.0');
const result = await service.isAcceptedLatestVersion(context, idToken); const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false); expect(result).toBe(false);
@ -288,10 +292,35 @@ describe('checkIsAcceptedLatestVersion', () => {
}; };
await createTermInfo(source, 'EULA', '1.0'); await createTermInfo(source, 'EULA', '1.0');
await createTermInfo(source, 'PrivacyNotice', '1.0');
await createTermInfo(source, 'DPA', '1.1'); await createTermInfo(source, 'DPA', '1.1');
const result = await service.isAcceptedLatestVersion(context, idToken); const result = await service.isAcceptedLatestVersion(context, idToken);
expect(result).toBe(false); expect(result).toBe(false);
}); });
it('同意済みプライバシーポリシーが最新でないときにチェックが通らないこと(第一~第四)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AuthService>(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', () => { describe('generateDelegationRefreshToken', () => {

View File

@ -689,28 +689,38 @@ export class AuthService {
const { const {
acceptedEulaVersion, acceptedEulaVersion,
latestEulaVersion, latestEulaVersion,
acceptedPrivacyNoticeVersion,
latestPrivacyNoticeVersion,
acceptedDpaVersion, acceptedDpaVersion,
latestDpaVersion, latestDpaVersion,
tier, tier,
} = await this.usersRepository.getAcceptedAndLatestVersion(idToken.sub); } = await this.usersRepository.getAcceptedAndLatestVersion(idToken.sub);
// 第五階層はEULAのみ判定 // 第五階層はEULAとPrivacyNoticeのみ判定
if (tier === TIERS.TIER5) { if (tier === TIERS.TIER5) {
if (!acceptedEulaVersion) { if (!acceptedEulaVersion || !acceptedPrivacyNoticeVersion) {
return false; return false;
} }
// 最新バージョンに同意済みか判定 // 最新バージョンに同意済みか判定
const eulaAccepted = acceptedEulaVersion === latestEulaVersion; const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
return eulaAccepted; const privacyNoticeAccepted =
acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion;
return eulaAccepted && privacyNoticeAccepted;
} else { } else {
// 第一第四階層はEULA、DPAを判定 // 第一第四階層はEULA、PrivacyNotice、DPAを判定
if (!acceptedEulaVersion || !acceptedDpaVersion) { if (
!acceptedEulaVersion ||
!acceptedPrivacyNoticeVersion ||
!acceptedDpaVersion
) {
return false; return false;
} }
// 最新バージョンに同意済みか判定 // 最新バージョンに同意済みか判定
const eulaAccepted = acceptedEulaVersion === latestEulaVersion; const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
const privacyNoticeAccepted =
acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion;
const dpaAccepted = acceptedDpaVersion === latestDpaVersion; const dpaAccepted = acceptedDpaVersion === latestDpaVersion;
return eulaAccepted && dpaAccepted; return eulaAccepted && privacyNoticeAccepted && dpaAccepted;
} }
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);

View File

@ -22,8 +22,10 @@ export class AccessTokenRequest {}
export type TermsCheckInfo = { export type TermsCheckInfo = {
tier: number; tier: number;
acceptedEulaVersion?: string; acceptedEulaVersion?: string;
acceptedPrivacyNoticeVersion?: string;
acceptedDpaVersion?: string; acceptedDpaVersion?: string;
latestEulaVersion: string; latestEulaVersion: string;
latestPrivacyNoticeVersion: string;
latestDpaVersion: string; latestDpaVersion: string;
}; };

View File

@ -139,6 +139,7 @@ export const makeDefaultUsersRepositoryMockValue =
role: 'none', role: 'none',
author_id: '', author_id: '',
accepted_eula_version: '1.0', accepted_eula_version: '1.0',
accepted_privacy_notice_version: '1.0',
accepted_dpa_version: '1.0', accepted_dpa_version: '1.0',
email_verified: true, email_verified: true,
deleted_at: null, deleted_at: null,

View File

@ -470,6 +470,7 @@ const defaultTasksRepositoryMockValue: {
external_id: 'userId', external_id: 'userId',
role: 'typist', role: 'typist',
accepted_eula_version: '', accepted_eula_version: '',
accepted_privacy_notice_version: '',
accepted_dpa_version: '', accepted_dpa_version: '',
email_verified: true, email_verified: true,
auto_renew: true, auto_renew: true,

View File

@ -34,6 +34,8 @@ describe('利用規約取得', () => {
await createTermInfo(source, 'EULA', 'v1.0'); await createTermInfo(source, 'EULA', 'v1.0');
await createTermInfo(source, 'EULA', 'v1.1'); 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.0');
await createTermInfo(source, 'DPA', 'v1.2'); await createTermInfo(source, 'DPA', 'v1.2');
@ -42,8 +44,10 @@ describe('利用規約取得', () => {
expect(result[0].documentType).toBe('EULA'); expect(result[0].documentType).toBe('EULA');
expect(result[0].version).toBe('v1.1'); expect(result[0].version).toBe('v1.1');
expect(result[1].documentType).toBe('DPA'); expect(result[1].documentType).toBe('PrivacyNotice');
expect(result[1].version).toBe('v1.2'); expect(result[1].version).toBe('v1.1');
expect(result[2].documentType).toBe('DPA');
expect(result[2].version).toBe('v1.2');
}); });
it('利用規約情報(EULA、DPA両方)が存在しない場合エラーとなる', async () => { 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>(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 () => { it('利用規約情報(DPAのみ)が存在しない場合エラーとなる', async () => {
if (!source) fail(); if (!source) fail();
const module = await makeTestingModule(source); const module = await makeTestingModule(source);

View File

@ -19,13 +19,17 @@ export class TermsService {
`[IN] [${context.getTrackingId()}] ${this.getTermsInfo.name}`, `[IN] [${context.getTrackingId()}] ${this.getTermsInfo.name}`,
); );
try { try {
const { eulaVersion, dpaVersion } = const { eulaVersion, privacyNoticeVersion, dpaVersion } =
await this.termsRepository.getLatestTermsInfo(); await this.termsRepository.getLatestTermsInfo();
return [ return [
{ {
documentType: TERM_TYPE.EULA, documentType: TERM_TYPE.EULA,
version: eulaVersion, version: eulaVersion,
}, },
{
documentType: TERM_TYPE.PRIVACY_NOTICE,
version: privacyNoticeVersion,
},
{ {
documentType: TERM_TYPE.DPA, documentType: TERM_TYPE.DPA,
version: dpaVersion, version: dpaVersion,

View File

@ -13,5 +13,6 @@ export class GetTermsInfoResponse {
export type TermsVersion = { export type TermsVersion = {
eulaVersion: string; eulaVersion: string;
privacyNoticeVersion: string;
dpaVersion: string; dpaVersion: string;
}; };

View File

@ -263,6 +263,8 @@ export class UpdateAcceptedVersionRequest {
idToken: string; idToken: string;
@ApiProperty({ description: '更新バージョンEULA' }) @ApiProperty({ description: '更新バージョンEULA' })
acceptedEULAVersion: string; acceptedEULAVersion: string;
@ApiProperty({ description: '更新バージョンPrivacyNotice' })
acceptedPrivacyNoticeVersion: string;
@ApiProperty({ description: '更新バージョンDPA', required: false }) @ApiProperty({ description: '更新バージョンDPA', required: false })
acceptedDPAVersion?: string; acceptedDPAVersion?: string;
} }

View File

@ -4,6 +4,7 @@ import {
Get, Get,
HttpException, HttpException,
HttpStatus, HttpStatus,
Ip,
Post, Post,
Query, Query,
Req, Req,
@ -136,6 +137,7 @@ export class UsersController {
@Get() @Get()
async getUsers(@Req() req: Request): Promise<GetUsersResponse> { async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
const accessToken = retrieveAuthorizationToken(req); const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) { if (!accessToken) {
throw new HttpException( throw new HttpException(
makeErrorResponse('E000107'), makeErrorResponse('E000107'),
@ -627,7 +629,12 @@ export class UsersController {
async updateAcceptedVersion( async updateAcceptedVersion(
@Body() body: UpdateAcceptedVersionRequest, @Body() body: UpdateAcceptedVersionRequest,
): Promise<UpdateAcceptedVersionResponse> { ): Promise<UpdateAcceptedVersionResponse> {
const { idToken, acceptedEULAVersion, acceptedDPAVersion } = body; const {
idToken,
acceptedEULAVersion,
acceptedPrivacyNoticeVersion,
acceptedDPAVersion,
} = body;
const context = makeContext(uuidv4()); const context = makeContext(uuidv4());
@ -650,6 +657,7 @@ export class UsersController {
context, context,
verifiedIdToken.sub, verifiedIdToken.sub,
acceptedEULAVersion, acceptedEULAVersion,
acceptedPrivacyNoticeVersion,
acceptedDPAVersion, acceptedDPAVersion,
); );
return {}; return {};

View File

@ -208,6 +208,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1, account_id: 1,
role: 'None', role: 'None',
accepted_eula_version: 'string', accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string', accepted_dpa_version: 'string',
email_verified: false, email_verified: false,
created_by: 'string;', created_by: 'string;',
@ -259,6 +260,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1, account_id: 1,
role: 'None', role: 'None',
accepted_eula_version: 'string', accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string', accepted_dpa_version: 'string',
email_verified: false, email_verified: false,
created_by: 'string;', created_by: 'string;',
@ -306,6 +308,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1, account_id: 1,
role: 'None', role: 'None',
accepted_eula_version: 'string', accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string', accepted_dpa_version: 'string',
email_verified: true, email_verified: true,
created_by: 'string;', created_by: 'string;',
@ -358,6 +361,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
account_id: 1, account_id: 1,
role: 'None', role: 'None',
accepted_eula_version: 'string', accepted_eula_version: 'string',
accepted_privacy_notice_version: 'string',
accepted_dpa_version: 'string', accepted_dpa_version: 'string',
email_verified: false, email_verified: false,
created_by: 'string;', created_by: 'string;',
@ -2617,7 +2621,12 @@ describe('UsersService.updateAcceptedVersion', () => {
const context = makeContext(uuidv4()); const context = makeContext(uuidv4());
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(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); const user = await getUser(source, admin.id);
expect(user?.accepted_eula_version).toBe('v2.0'); expect(user?.accepted_eula_version).toBe('v2.0');
@ -2637,6 +2646,7 @@ describe('UsersService.updateAcceptedVersion', () => {
context, context,
admin.external_id, admin.external_id,
'v2.0', 'v2.0',
'v2.0',
'v3.0', 'v3.0',
); );
const user = await getUser(source, admin.id); const user = await getUser(source, admin.id);
@ -2660,6 +2670,7 @@ describe('UsersService.updateAcceptedVersion', () => {
context, context,
admin.external_id, admin.external_id,
'v2.0', 'v2.0',
'v2.0',
undefined, undefined,
), ),
).rejects.toEqual( ).rejects.toEqual(

View File

@ -403,6 +403,7 @@ export class UsersService {
role, role,
accepted_dpa_version: null, accepted_dpa_version: null,
accepted_eula_version: null, accepted_eula_version: null,
accepted_privacy_notice_version: null,
encryption: false, encryption: false,
encryption_password: null, encryption_password: null,
prompt: false, prompt: false,
@ -422,6 +423,7 @@ export class UsersService {
prompt: prompt ?? false, prompt: prompt ?? false,
accepted_dpa_version: null, accepted_dpa_version: null,
accepted_eula_version: null, accepted_eula_version: null,
accepted_privacy_notice_version: null,
}; };
default: default:
//不正なroleが指定された場合はログを出力してエラーを返す //不正なroleが指定された場合はログを出力してエラーを返す
@ -538,6 +540,7 @@ export class UsersService {
// DBから同一アカウントのユーザ一覧を取得する // DBから同一アカウントのユーザ一覧を取得する
const dbUsers = await this.usersRepository.findSameAccountUsers( const dbUsers = await this.usersRepository.findSameAccountUsers(
externalId, externalId,
context,
); );
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
@ -1044,12 +1047,14 @@ export class UsersService {
* @param context * @param context
* @param idToken * @param idToken
* @param eulaVersion * @param eulaVersion
* @param privacyNoticeVersion
* @param dpaVersion * @param dpaVersion
*/ */
async updateAcceptedVersion( async updateAcceptedVersion(
context: Context, context: Context,
externalId: string, externalId: string,
eulaVersion: string, eulaVersion: string,
privacyNoticeVersion: string,
dpaVersion?: string, dpaVersion?: string,
): Promise<void> { ): Promise<void> {
this.logger.log( this.logger.log(
@ -1058,6 +1063,7 @@ export class UsersService {
} | params: { ` + } | params: { ` +
`externalId: ${externalId}, ` + `externalId: ${externalId}, ` +
`eulaVersion: ${eulaVersion}, ` + `eulaVersion: ${eulaVersion}, ` +
`privacyNoticeVersion: ${privacyNoticeVersion}, ` +
`dpaVersion: ${dpaVersion}, };`, `dpaVersion: ${dpaVersion}, };`,
); );
@ -1065,6 +1071,7 @@ export class UsersService {
await this.usersRepository.updateAcceptedTermsVersion( await this.usersRepository.updateAcceptedTermsVersion(
externalId, externalId,
eulaVersion, eulaVersion,
privacyNoticeVersion,
dpaVersion, dpaVersion,
); );
} catch (e) { } catch (e) {

View File

@ -127,6 +127,7 @@ export class AccountsRepositoryService {
adminExternalUserId: string, adminExternalUserId: string,
adminUserRole: string, adminUserRole: string,
adminUserAcceptedEulaVersion?: string, adminUserAcceptedEulaVersion?: string,
adminUserAcceptedPrivacyNoticeVersion?: string,
adminUserAcceptedDpaVersion?: string, adminUserAcceptedDpaVersion?: string,
): Promise<{ newAccount: Account; adminUser: User }> { ): Promise<{ newAccount: Account; adminUser: User }> {
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
@ -148,6 +149,8 @@ export class AccountsRepositoryService {
user.external_id = adminExternalUserId; user.external_id = adminExternalUserId;
user.role = adminUserRole; user.role = adminUserRole;
user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null; user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
user.accepted_privacy_notice_version =
adminUserAcceptedPrivacyNoticeVersion ?? null;
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null; user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
} }
const usersRepo = entityManager.getRepository(User); const usersRepo = entityManager.getRepository(User);

View File

@ -24,6 +24,14 @@ export class TermsRepositoryService {
id: 'DESC', id: 'DESC',
}, },
}); });
const latestPrivacyNoticeInfo = await termRepo.findOne({
where: {
document_type: TERM_TYPE.PRIVACY_NOTICE,
},
order: {
id: 'DESC',
},
});
const latestDpaInfo = await termRepo.findOne({ const latestDpaInfo = await termRepo.findOne({
where: { where: {
document_type: TERM_TYPE.DPA, document_type: TERM_TYPE.DPA,
@ -33,13 +41,16 @@ export class TermsRepositoryService {
}, },
}); });
if (!latestEulaInfo || !latestDpaInfo) { if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) {
throw new TermInfoNotFoundError( throw new TermInfoNotFoundError(
`Terms info is not found. latestEulaInfo: ${latestEulaInfo}, latestDpaInfo: ${latestDpaInfo}`, `Terms info is not found. latestEulaInfo: ${latestEulaInfo},
latestPrivacyNoticeInfo: ${latestPrivacyNoticeInfo},
latestDpaInfo: ${latestDpaInfo}`,
); );
} }
return { return {
eulaVersion: latestEulaInfo.version, eulaVersion: latestEulaInfo.version,
privacyNoticeVersion: latestEulaInfo.version,
dpaVersion: latestDpaInfo.version, dpaVersion: latestDpaInfo.version,
}; };
}); });

View File

@ -34,6 +34,9 @@ export class User {
@Column({ nullable: true, type: 'varchar' }) @Column({ nullable: true, type: 'varchar' })
accepted_eula_version: string | null; accepted_eula_version: string | null;
@Column({ nullable: true, type: 'varchar' })
accepted_privacy_notice_version: string | null;
@Column({ nullable: true, type: 'varchar' }) @Column({ nullable: true, type: 'varchar' })
accepted_dpa_version: string | null; accepted_dpa_version: string | null;
@ -112,6 +115,9 @@ export class UserArchive {
@Column({ nullable: true, type: 'varchar' }) @Column({ nullable: true, type: 'varchar' })
accepted_eula_version: string | null; accepted_eula_version: string | null;
@Column({ nullable: true, type: 'varchar' })
accepted_privacy_notice_version: string | null;
@Column({ nullable: true, type: 'varchar' }) @Column({ nullable: true, type: 'varchar' })
accepted_dpa_version: string | null; accepted_dpa_version: string | null;

View File

@ -35,6 +35,7 @@ import {
import { Account } from '../accounts/entity/account.entity'; import { Account } from '../accounts/entity/account.entity';
import { Workflow } from '../workflows/entity/workflow.entity'; import { Workflow } from '../workflows/entity/workflow.entity';
import { Worktype } from '../worktypes/entity/worktype.entity'; import { Worktype } from '../worktypes/entity/worktype.entity';
import { Context } from '../../common/log';
@Injectable() @Injectable()
export class UsersRepositoryService { export class UsersRepositoryService {
@ -340,7 +341,10 @@ export class UsersRepositoryService {
* @param externalId * @param externalId
* @returns User[] * @returns User[]
*/ */
async findSameAccountUsers(external_id: string): Promise<User[]> { async findSameAccountUsers(
external_id: string,
context: Context,
): Promise<User[]> {
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
const repo = entityManager.getRepository(User); const repo = entityManager.getRepository(User);
@ -359,8 +363,9 @@ export class UsersRepositoryService {
license: true, license: true,
}, },
where: { account_id: accountId }, where: { account_id: accountId },
comment: `${context.getTrackingId()}`,
}); });
return dbUsers; return dbUsers;
}); });
} }
@ -471,6 +476,14 @@ export class UsersRepositoryService {
id: 'DESC', id: 'DESC',
}, },
}); });
const latestPrivacyNoticeInfo = await termRepo.findOne({
where: {
document_type: TERM_TYPE.PRIVACY_NOTICE,
},
order: {
id: 'DESC',
},
});
const latestDpaInfo = await termRepo.findOne({ const latestDpaInfo = await termRepo.findOne({
where: { where: {
document_type: TERM_TYPE.DPA, document_type: TERM_TYPE.DPA,
@ -479,16 +492,18 @@ export class UsersRepositoryService {
id: 'DESC', id: 'DESC',
}, },
}); });
if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) {
if (!latestEulaInfo || !latestDpaInfo) {
throw new TermInfoNotFoundError(`Terms info is not found.`); throw new TermInfoNotFoundError(`Terms info is not found.`);
} }
return { return {
tier: user.account.tier, tier: user.account.tier,
acceptedEulaVersion: user.accepted_eula_version ?? undefined, acceptedEulaVersion: user.accepted_eula_version ?? undefined,
acceptedPrivacyNoticeVersion:
user.accepted_privacy_notice_version ?? undefined,
acceptedDpaVersion: user.accepted_dpa_version ?? undefined, acceptedDpaVersion: user.accepted_dpa_version ?? undefined,
latestEulaVersion: latestEulaInfo.version, latestEulaVersion: latestEulaInfo.version,
latestPrivacyNoticeVersion: latestPrivacyNoticeInfo.version,
latestDpaVersion: latestDpaInfo.version, latestDpaVersion: latestDpaInfo.version,
}; };
}); });
@ -498,12 +513,14 @@ export class UsersRepositoryService {
* *
* @param externalId * @param externalId
* @param eulaVersion * @param eulaVersion
* @param privacyNoticeVersion
* @param dpaVersion * @param dpaVersion
* @returns update * @returns update
*/ */
async updateAcceptedTermsVersion( async updateAcceptedTermsVersion(
externalId: string, externalId: string,
eulaVersion: string, eulaVersion: string,
privacyNoticeVersion: string,
dpaVersion: string | undefined, dpaVersion: string | undefined,
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { await this.dataSource.transaction(async (entityManager) => {
@ -531,6 +548,11 @@ export class UsersRepositoryService {
if (!eulaVersion) { if (!eulaVersion) {
throw new UpdateTermsVersionNotSetError(`EULA version param not set.`); 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) { if (user.account.tier !== TIERS.TIER5 && !dpaVersion) {
throw new UpdateTermsVersionNotSetError( throw new UpdateTermsVersionNotSetError(
`DPA version param not set. User's tier: ${user.account.tier}`, `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_eula_version = eulaVersion;
user.accepted_privacy_notice_version =
privacyNoticeVersion ?? user.accepted_privacy_notice_version;
user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version; user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version;
await userRepo.update({ id: user.id }, user); await userRepo.update({ id: user.id }, user);
}); });