Merge branch 'develop' into main
This commit is contained in:
commit
20157341f6
@ -1,12 +1,29 @@
|
||||
# To enable ssh & remote debugging on app service change the base image to the one below
|
||||
# FROM mcr.microsoft.com/azure-functions/node:4-node18-appservice
|
||||
#ビルドイメージ
|
||||
FROM node:18.17.1-buster AS build-container
|
||||
WORKDIR /app
|
||||
RUN mkdir dictation_function
|
||||
COPY dictation_function/ dictation_function/
|
||||
RUN npm install --force -g n && n 18.17.1 \
|
||||
&& cd dictation_function \
|
||||
&& npm ci \
|
||||
&& npm run build \
|
||||
&& cd ..
|
||||
|
||||
# 成果物イメージ
|
||||
FROM mcr.microsoft.com/azure-functions/node:4-node18
|
||||
|
||||
WORKDIR /home/site/wwwroot
|
||||
RUN mkdir build \
|
||||
&& mkdir dist \
|
||||
&& mkdir node_modules
|
||||
|
||||
COPY --from=build-container app/dictation_function/dist/ dist/
|
||||
COPY --from=build-container app/dictation_function/node_modules/ node_modules/
|
||||
COPY --from=build-container app/dictation_function/.env ./
|
||||
COPY --from=build-container app/dictation_function/host.json ./
|
||||
COPY --from=build-container app/dictation_function/package.json ./
|
||||
|
||||
ARG BUILD_VERSION
|
||||
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
|
||||
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
|
||||
|
||||
COPY . /home/site/wwwroot
|
||||
|
||||
RUN cd /home/site/wwwroot && \
|
||||
npm install && \
|
||||
npm run build
|
||||
AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
|
||||
BUILD_VERSION=${BUILD_VERSION}
|
||||
@ -83,9 +83,24 @@ jobs:
|
||||
is_static_export: false
|
||||
verbose: false
|
||||
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
|
||||
- job: smoke_test
|
||||
- job: function_deploy
|
||||
dependsOn: frontend_deploy
|
||||
condition: succeeded('frontend_deploy')
|
||||
displayName: function Deploy
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: AzureFunctionAppContainer@1
|
||||
inputs:
|
||||
azureSubscription: 'omds-service-connection-prod'
|
||||
appName: 'func-odms-dictation-prod'
|
||||
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
|
||||
- job: smoke_test
|
||||
dependsOn: function_deploy
|
||||
condition: succeeded('function_deploy')
|
||||
displayName: 'smoke test'
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
|
||||
@ -23,6 +23,7 @@ import AccountPage from "pages/AccountPage";
|
||||
import AcceptToUsePage from "pages/TermsPage";
|
||||
import { TemplateFilePage } from "pages/TemplateFilePage";
|
||||
import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess";
|
||||
import SupportPage from "pages/SupportPage";
|
||||
|
||||
const AppRouter: React.FC = () => (
|
||||
<Routes>
|
||||
@ -81,6 +82,10 @@ const AppRouter: React.FC = () => (
|
||||
element={<RouteAuthGuard component={<PartnerPage />} />}
|
||||
/>
|
||||
<Route path="/accountDeleteSuccess" element={<AccountDeleteSuccess />} />
|
||||
<Route
|
||||
path="/support"
|
||||
element={<RouteAuthGuard component={<SupportPage />} />}
|
||||
/>
|
||||
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
|
||||
@ -447,6 +447,12 @@ export interface CreateAccountRequest {
|
||||
* @memberof CreateAccountRequest
|
||||
*/
|
||||
'acceptedEulaVersion': string;
|
||||
/**
|
||||
* 同意済みプライバシーポリシーのバージョン
|
||||
* @type {string}
|
||||
* @memberof CreateAccountRequest
|
||||
*/
|
||||
'acceptedPrivacyNoticeVersion': string;
|
||||
/**
|
||||
* 同意済み利用規約のバージョン(DPA)
|
||||
* @type {string}
|
||||
@ -746,6 +752,32 @@ export interface GetAuthorsResponse {
|
||||
*/
|
||||
'authors': Array<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
|
||||
@ -2000,6 +2032,12 @@ export interface UpdateAcceptedVersionRequest {
|
||||
* @memberof UpdateAcceptedVersionRequest
|
||||
*/
|
||||
'acceptedEULAVersion': string;
|
||||
/**
|
||||
* 更新バージョン(PrivacyNotice)
|
||||
* @type {string}
|
||||
* @memberof UpdateAcceptedVersionRequest
|
||||
*/
|
||||
'acceptedPrivacyNoticeVersion': string;
|
||||
/**
|
||||
* 更新バージョン(DPA)
|
||||
* @type {string}
|
||||
@ -2727,6 +2765,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 指定したアカウントの会社名を取得します
|
||||
* @summary
|
||||
* @param {GetCompanyNameRequest} getCompanyNameRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getCompanyName: async (getCompanyNameRequest: GetCompanyNameRequest, options: AxiosRequestConfig = {}): Promise<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
|
||||
@ -3488,6 +3566,19 @@ export const AccountsApiFp = function(configuration?: Configuration) {
|
||||
const operationBasePath = operationServerMap['AccountsApi.getAuthors']?.[index]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* 指定したアカウントの会社名を取得します
|
||||
* @summary
|
||||
* @param {GetCompanyNameRequest} getCompanyNameRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<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
|
||||
@ -3804,6 +3895,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
|
||||
getAuthors(options?: any): AxiosPromise<GetAuthorsResponse> {
|
||||
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
|
||||
@ -4092,6 +4193,18 @@ export class AccountsApi extends BaseAPI {
|
||||
return AccountsApiFp(this.configuration).getAuthors(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したアカウントの会社名を取得します
|
||||
* @summary
|
||||
* @param {GetCompanyNameRequest} getCompanyNameRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AccountsApi
|
||||
*/
|
||||
public getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig) {
|
||||
return AccountsApiFp(this.configuration).getCompanyName(getCompanyNameRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary
|
||||
|
||||
@ -7,6 +7,7 @@ export const HEADER_MENUS_LICENSE = "License";
|
||||
export const HEADER_MENUS_DICTATIONS = "Dictations";
|
||||
export const HEADER_MENUS_WORKFLOW = "Workflow";
|
||||
export const HEADER_MENUS_PARTNER = "Partners";
|
||||
export const HEADER_MENUS_SUPPORT = "Support";
|
||||
|
||||
export const HEADER_MENUS: {
|
||||
key: HeaderMenus;
|
||||
@ -43,6 +44,11 @@ export const HEADER_MENUS: {
|
||||
label: getTranslationID("common.label.headerPartners"),
|
||||
path: "/partners",
|
||||
},
|
||||
{
|
||||
key: HEADER_MENUS_SUPPORT,
|
||||
label: getTranslationID("common.label.headerSupport"),
|
||||
path: "/support",
|
||||
},
|
||||
];
|
||||
|
||||
export const HEADER_NAME = getTranslationID("common.label.headerName");
|
||||
|
||||
@ -8,7 +8,8 @@ export type HeaderMenus =
|
||||
| "License"
|
||||
| "Dictations"
|
||||
| "Workflow"
|
||||
| "Partners";
|
||||
| "Partners"
|
||||
| "Support";
|
||||
|
||||
// ログイン後に遷移しうるパス
|
||||
export type LoginedPaths =
|
||||
@ -17,4 +18,5 @@ export type LoginedPaths =
|
||||
| "/license"
|
||||
| "/dictations"
|
||||
| "/workflow"
|
||||
| "/partners";
|
||||
| "/partners"
|
||||
| "/support";
|
||||
|
||||
@ -20,6 +20,7 @@ export const isLoginPaths = (d: string): d is LoginedPaths => {
|
||||
case "/dictations":
|
||||
case "/workflow":
|
||||
case "/partners":
|
||||
case "/support":
|
||||
return true;
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@ -131,6 +131,10 @@ export const dictationSlice = createSlice({
|
||||
});
|
||||
state.domain.backup.tasks = tasks;
|
||||
},
|
||||
openFilePropertyInfo: (state, action: PayloadAction<{ task: Task }>) => {
|
||||
const { task } = action.payload;
|
||||
state.apps.selectedFileTask = task;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(listTasksAsync.pending, (state) => {
|
||||
@ -225,6 +229,7 @@ export const {
|
||||
changeAssignee,
|
||||
changeBackupTaskChecked,
|
||||
changeBackupTaskAllCheched,
|
||||
openFilePropertyInfo,
|
||||
} = dictationSlice.actions;
|
||||
|
||||
export default dictationSlice.reducer;
|
||||
|
||||
@ -34,6 +34,9 @@ export const selectParamName = (state: RootState) =>
|
||||
export const selectSelectedTask = (state: RootState) =>
|
||||
state.dictation.apps.selectedTask;
|
||||
|
||||
export const selectSelectedFileTask = (state: RootState) =>
|
||||
state.dictation.apps.selectedFileTask;
|
||||
|
||||
export const selectSelectedTranscriptionists = (state: RootState) =>
|
||||
state.dictation.apps.assignee.selected;
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ export interface Apps {
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
selectedTask?: Task;
|
||||
selectedFileTask?: Task;
|
||||
assignee: {
|
||||
selected: Assignee[];
|
||||
pool: Assignee[];
|
||||
|
||||
@ -1,20 +1,25 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { LicenseSummaryState } from "./state";
|
||||
import { getLicenseSummaryAsync } from "./operations";
|
||||
import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations";
|
||||
|
||||
const initialState: LicenseSummaryState = {
|
||||
domain: {
|
||||
totalLicense: 0,
|
||||
allocatedLicense: 0,
|
||||
reusableLicense: 0,
|
||||
freeLicense: 0,
|
||||
expiringWithin14daysLicense: 0,
|
||||
issueRequesting: 0,
|
||||
numberOfRequesting: 0,
|
||||
shortage: 0,
|
||||
storageSize: 0,
|
||||
usedSize: 0,
|
||||
isStorageAvailable: false,
|
||||
licenseSummaryInfo: {
|
||||
totalLicense: 0,
|
||||
allocatedLicense: 0,
|
||||
reusableLicense: 0,
|
||||
freeLicense: 0,
|
||||
expiringWithin14daysLicense: 0,
|
||||
issueRequesting: 0,
|
||||
numberOfRequesting: 0,
|
||||
shortage: 0,
|
||||
storageSize: 0,
|
||||
usedSize: 0,
|
||||
isStorageAvailable: false,
|
||||
},
|
||||
accountInfo: {
|
||||
companyName: "",
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
isLoading: false,
|
||||
@ -31,7 +36,10 @@ export const licenseSummarySlice = createSlice({
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => {
|
||||
state.domain = action.payload;
|
||||
state.domain.licenseSummaryInfo = action.payload;
|
||||
});
|
||||
builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => {
|
||||
state.domain.accountInfo.companyName = action.payload.companyName;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -5,6 +5,7 @@ import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getAccessToken } from "features/auth";
|
||||
import {
|
||||
AccountsApi,
|
||||
GetCompanyNameResponse,
|
||||
GetLicenseSummaryResponse,
|
||||
PartnerLicenseInfo,
|
||||
} from "../../../api/api";
|
||||
@ -66,3 +67,59 @@ export const getLicenseSummaryAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getCompanyNameAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetCompanyNameResponse,
|
||||
// 引数
|
||||
{ selectedRow?: PartnerLicenseInfo },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/getCompanyNameAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
try {
|
||||
const getMyAccountResponse = await accountsApi.getMyAccount({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
const { selectedRow } = args;
|
||||
// 引数がない場合は自分のアカウントID取得
|
||||
const accountId =
|
||||
selectedRow?.accountId ?? getMyAccountResponse?.data?.account?.accountId;
|
||||
|
||||
if (accountId !== undefined) {
|
||||
const getCompanyNameResponse = await accountsApi.getCompanyName(
|
||||
{ accountId },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
return getCompanyNameResponse.data;
|
||||
}
|
||||
throw new Error("accountId is undefined");
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
const errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,6 +2,9 @@ import { RootState } from "app/store";
|
||||
|
||||
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
|
||||
export const selecLicenseSummaryInfo = (state: RootState) =>
|
||||
state.licenseSummary.domain;
|
||||
state.licenseSummary.domain.licenseSummaryInfo;
|
||||
|
||||
export const selectCompanyName = (state: RootState) =>
|
||||
state.licenseSummary.domain.accountInfo.companyName;
|
||||
|
||||
export const selectIsLoading = (state: RootState) => state.license;
|
||||
|
||||
@ -4,17 +4,22 @@ export interface LicenseSummaryState {
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
totalLicense: number;
|
||||
allocatedLicense: number;
|
||||
reusableLicense: number;
|
||||
freeLicense: number;
|
||||
expiringWithin14daysLicense: number;
|
||||
issueRequesting: number;
|
||||
numberOfRequesting: number;
|
||||
shortage: number;
|
||||
storageSize: number;
|
||||
usedSize: number;
|
||||
isStorageAvailable: boolean;
|
||||
licenseSummaryInfo: {
|
||||
totalLicense: number;
|
||||
allocatedLicense: number;
|
||||
reusableLicense: number;
|
||||
freeLicense: number;
|
||||
expiringWithin14daysLicense: number;
|
||||
issueRequesting: number;
|
||||
numberOfRequesting: number;
|
||||
shortage: number;
|
||||
storageSize: number;
|
||||
usedSize: number;
|
||||
isStorageAvailable: boolean;
|
||||
};
|
||||
accountInfo: {
|
||||
companyName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
|
||||
@ -5,4 +5,5 @@
|
||||
export const TERMS_DOCUMENT_TYPE = {
|
||||
DPA: "DPA",
|
||||
EULA: "EULA",
|
||||
PRIVACY_NOTICE: "PrivacyNotice",
|
||||
} as const;
|
||||
|
||||
@ -110,6 +110,7 @@ export const updateAcceptedVersionAsync = createAsyncThunk<
|
||||
updateAccceptVersions: {
|
||||
acceptedVerDPA: string;
|
||||
acceptedVerEULA: string;
|
||||
acceptedVerPrivacyNotice: string;
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -140,6 +141,8 @@ export const updateAcceptedVersionAsync = createAsyncThunk<
|
||||
{
|
||||
idToken,
|
||||
acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA,
|
||||
acceptedPrivacyNoticeVersion:
|
||||
updateAccceptVersions.acceptedVerPrivacyNotice,
|
||||
acceptedDPAVersion: !(TIERS.TIER5 === tier.toString())
|
||||
? updateAccceptVersions.acceptedVerDPA
|
||||
: undefined,
|
||||
|
||||
@ -14,7 +14,11 @@ export const selectTermVersions = (state: RootState) => {
|
||||
(termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA
|
||||
)?.version || "";
|
||||
|
||||
return { acceptedVerDPA, acceptedVerEULA };
|
||||
const acceptedVerPrivacyNotice =
|
||||
termsInfo.find(
|
||||
(termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.PRIVACY_NOTICE
|
||||
)?.version || "";
|
||||
return { acceptedVerDPA, acceptedVerEULA, acceptedVerPrivacyNotice };
|
||||
};
|
||||
|
||||
export const selectTier = (state: RootState) => state.terms.domain.tier;
|
||||
|
||||
126
dictation_client/src/pages/DictationPage/filePropertyPopup.tsx
Normal file
126
dictation_client/src/pages/DictationPage/filePropertyPopup.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -23,6 +23,7 @@ import {
|
||||
changeParamName,
|
||||
changeDirection,
|
||||
changeSelectedTask,
|
||||
openFilePropertyInfo,
|
||||
SortableColumnType,
|
||||
changeAssignee,
|
||||
listTypistsAsync,
|
||||
@ -48,6 +49,7 @@ import open_in_new from "../../assets/images/open_in_new.svg";
|
||||
import { DisPlayInfo } from "./displayInfo";
|
||||
import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup";
|
||||
import { BackupPopup } from "./backupPopup";
|
||||
import { FilePropertyPopup } from "./filePropertyPopup";
|
||||
|
||||
const DictationPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -63,6 +65,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
isChangeTranscriptionistPopupOpen,
|
||||
setIsChangeTranscriptionistPopupOpen,
|
||||
] = useState(false);
|
||||
const [isFilePropertyPopupOpen, setIsFilePropertyPopupOpen] = useState(false);
|
||||
const [isBackupPopupOpen, setIsBackupPopupOpen] = useState(false);
|
||||
|
||||
const onChangeTranscriptionistPopupOpen = useCallback(
|
||||
@ -74,6 +77,13 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
[dispatch, setIsChangeTranscriptionistPopupOpen]
|
||||
);
|
||||
|
||||
const onClickFileProperty = useCallback(
|
||||
(task: Task) => {
|
||||
dispatch(openFilePropertyInfo({ task }));
|
||||
setIsFilePropertyPopupOpen(true);
|
||||
},
|
||||
[dispatch, setIsFilePropertyPopupOpen]
|
||||
);
|
||||
// 各カラムの表示/非表示
|
||||
const displayColumn = useSelector(selectDisplayInfo);
|
||||
|
||||
@ -477,6 +487,10 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
setIsBackupPopupOpen(true);
|
||||
}, []);
|
||||
|
||||
const onCloseFilePropertyPopup = useCallback(() => {
|
||||
setIsFilePropertyPopupOpen(false);
|
||||
}, []);
|
||||
|
||||
const sortIconClass = (
|
||||
currentParam: SortableColumnType,
|
||||
currentDirection: DirectionType,
|
||||
@ -532,6 +546,10 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<BackupPopup isOpen={isBackupPopupOpen} onClose={onCloseBackupPopup} />
|
||||
<FilePropertyPopup
|
||||
isOpen={isFilePropertyPopupOpen}
|
||||
onClose={onCloseFilePropertyPopup}
|
||||
/>
|
||||
<ChangeTranscriptionistPopup
|
||||
isOpen={isChangeTranscriptionistPopupOpen}
|
||||
onClose={onClosePopup}
|
||||
@ -1080,7 +1098,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
</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(
|
||||
getTranslationID(
|
||||
"dictationPage.label.fileProperty"
|
||||
@ -1360,11 +1379,12 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
<ul className={`${styles.menuAction} ${styles.alignRight}`}>
|
||||
<li className={styles.alignLeft}>
|
||||
<a
|
||||
href=""
|
||||
// TODO: 将来的に正式なURLに変更する
|
||||
href="/dictations"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
target="_blank"
|
||||
>
|
||||
Applications
|
||||
{t(getTranslationID("dictationPage.label.applications"))}
|
||||
<img
|
||||
src={open_in_new}
|
||||
alt=""
|
||||
|
||||
@ -8,8 +8,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
getCompanyNameAsync,
|
||||
getLicenseSummaryAsync,
|
||||
selecLicenseSummaryInfo,
|
||||
selectCompanyName,
|
||||
} from "features/license/licenseSummary";
|
||||
import { selectSelectedRow } from "features/license/partnerLicense";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
@ -61,9 +63,11 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
|
||||
// apiからの値取得関係
|
||||
const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo);
|
||||
const companyName = useSelector(selectCompanyName);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getLicenseSummaryAsync({ selectedRow }));
|
||||
dispatch(getCompanyNameAsync({ selectedRow }));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch]);
|
||||
|
||||
@ -118,7 +122,7 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
</div>
|
||||
<section className={styles.license}>
|
||||
<div>
|
||||
<h2 className="">{"会社名" /* TODO 会社名を表示する */}</h2>
|
||||
<h2 className="">{companyName}</h2>
|
||||
<ul className={styles.menuAction}>
|
||||
<li>
|
||||
{/* 他アカウントのライセンス情報を見ている場合は、前画面に戻る用のreturnボタンを表示 */}
|
||||
|
||||
@ -40,6 +40,7 @@ const SignupConfirm: React.FC = (): JSX.Element => {
|
||||
adminMail,
|
||||
adminPassword,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion: "",
|
||||
acceptedDpaVersion: "",
|
||||
token: "",
|
||||
})
|
||||
|
||||
90
dictation_client/src/pages/SupportPage/index.tsx
Normal file
90
dictation_client/src/pages/SupportPage/index.tsx
Normal 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;
|
||||
@ -29,9 +29,12 @@ const TermsPage: React.FC = (): JSX.Element => {
|
||||
const tier = useSelector(selectTier);
|
||||
|
||||
const [isCheckedEula, setIsCheckedEula] = useState(false);
|
||||
const [isCheckedPrivacyNotice, setIsCheckedPrivacyNotice] = useState(false);
|
||||
const [isCheckedDpa, setIsCheckedDpa] = useState(false);
|
||||
|
||||
const [isClickedEulaLink, setIsClickedEulaLink] = useState(false);
|
||||
const [isClickedPrivacyNoticeLink, setIsClickedPrivacyNoticeLink] =
|
||||
useState(false);
|
||||
const [isClickedDpaLink, setIsClickedDpaLink] = useState(false);
|
||||
|
||||
// 画面起動時
|
||||
@ -52,9 +55,9 @@ const TermsPage: React.FC = (): JSX.Element => {
|
||||
// ボタン押下可否判定ロジック
|
||||
const canClickButton = () => {
|
||||
if (isTier5()) {
|
||||
return isCheckedEula;
|
||||
return isCheckedEula && isCheckedPrivacyNotice;
|
||||
}
|
||||
return isCheckedEula && isCheckedDpa;
|
||||
return isCheckedEula && isCheckedPrivacyNotice && isCheckedDpa;
|
||||
};
|
||||
|
||||
// ボタン押下時処理
|
||||
@ -62,7 +65,8 @@ const TermsPage: React.FC = (): JSX.Element => {
|
||||
if (
|
||||
localStorageKeyforIdToken &&
|
||||
updateAccceptVersions.acceptedVerDPA !== "" &&
|
||||
updateAccceptVersions.acceptedVerEULA !== ""
|
||||
updateAccceptVersions.acceptedVerEULA !== "" &&
|
||||
updateAccceptVersions.acceptedVerPrivacyNotice !== ""
|
||||
) {
|
||||
const { meta } = await dispatch(
|
||||
updateAcceptedVersionAsync({
|
||||
@ -132,7 +136,42 @@ const TermsPage: React.FC = (): JSX.Element => {
|
||||
</label>
|
||||
</p>
|
||||
</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() && (
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<p>
|
||||
|
||||
@ -2306,8 +2306,7 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
}
|
||||
.formChange ul.chooseMember li input + label:hover,
|
||||
.formChange ul.holdMember li input + label:hover {
|
||||
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
|
||||
center;
|
||||
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
|
||||
background-size: 1.3rem;
|
||||
}
|
||||
.formChange ul.chooseMember li input:checked + label,
|
||||
@ -2318,8 +2317,8 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
}
|
||||
.formChange ul.chooseMember li input:checked + label:hover,
|
||||
.formChange ul.holdMember li input:checked + label:hover {
|
||||
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
|
||||
right center;
|
||||
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
|
||||
center;
|
||||
background-size: 1.3rem;
|
||||
}
|
||||
.formChange > p {
|
||||
@ -2472,8 +2471,7 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
}
|
||||
.formChange ul.chooseMember li input + label:hover,
|
||||
.formChange ul.holdMember li input + label:hover {
|
||||
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
|
||||
center;
|
||||
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
|
||||
background-size: 1.3rem;
|
||||
}
|
||||
.formChange ul.chooseMember li input:checked + label,
|
||||
@ -2484,8 +2482,8 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
}
|
||||
.formChange ul.chooseMember li input:checked + label:hover,
|
||||
.formChange ul.holdMember li input:checked + label:hover {
|
||||
background: #e6e6e6 url(../images/arrow_circle_right.svg) no-repeat right
|
||||
center;
|
||||
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
|
||||
right center;
|
||||
background-size: 1.3rem;
|
||||
}
|
||||
.formChange > p {
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"headerDictations": "(de)Dictations",
|
||||
"headerWorkflow": "(de)Workflow",
|
||||
"headerPartners": "(de)Partners",
|
||||
"headerSupport": "(de)Support",
|
||||
"tier1": "(de)Admin",
|
||||
"tier2": "(de)BC",
|
||||
"tier3": "(de)Distributor",
|
||||
@ -250,7 +251,11 @@
|
||||
"poolTranscriptionist": "Transkriptionsliste",
|
||||
"fileBackup": "(de)File Backup",
|
||||
"downloadForBackup": "(de)Download for backup",
|
||||
"cancelDictation": "(de)Cancel Dictation"
|
||||
"applications": "(de)Applications",
|
||||
"cancelDictation": "(de)Cancel Dictation",
|
||||
"general": "(de)General",
|
||||
"job": "(de)Job",
|
||||
"close": "(de)Close"
|
||||
}
|
||||
},
|
||||
"cardLicenseIssuePopupPage": {
|
||||
@ -527,9 +532,31 @@
|
||||
"title": "(de)Terms of Use has updated. Please confirm again.",
|
||||
"linkOfEula": "(de)Click here to read the terms of use.",
|
||||
"linkOfDpa": "(de)Click here to read the terms of use.",
|
||||
"linkOfPrivacyNotice": "(de)Click here to read the terms of use.",
|
||||
"checkBoxForConsent": "(de)Yes, I agree to the terms of use.",
|
||||
"forOdds": "(de)for ODDS.",
|
||||
"button": "(de)Continue"
|
||||
"button": "(de)Continue",
|
||||
"linkOfPrivacyNotice": "(de)Click here to read the terms of use."
|
||||
}
|
||||
},
|
||||
"supportPage": {
|
||||
"label": {
|
||||
"title": "(de)Support",
|
||||
"howToUse": "(de)How to use the system",
|
||||
"supportPageEnglish": "OMDS Cloud User Guide",
|
||||
"supportPageGerman": "OMDS Cloud Benutzerhandbuch",
|
||||
"supportPageFrench": "OMDS Cloud Mode d'emploi",
|
||||
"supportPageSpanish": "OMDS Cloud Guía del usuario"
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "(de)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
|
||||
}
|
||||
},
|
||||
"filePropertyPopup": {
|
||||
"label": {
|
||||
"general": "(de)General",
|
||||
"job": "(de)Job",
|
||||
"close": "(de)Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"headerDictations": "Dictations",
|
||||
"headerWorkflow": "Workflow",
|
||||
"headerPartners": "Partners",
|
||||
"headerSupport": "Support",
|
||||
"tier1": "Admin",
|
||||
"tier2": "BC",
|
||||
"tier3": "Distributor",
|
||||
@ -250,7 +251,11 @@
|
||||
"poolTranscriptionist": "Transcription List",
|
||||
"fileBackup": "File Backup",
|
||||
"downloadForBackup": "Download for backup",
|
||||
"cancelDictation": "Cancel Dictation"
|
||||
"applications": "Applications",
|
||||
"cancelDictation": "Cancel Dictation",
|
||||
"general": "General",
|
||||
"job": "Job",
|
||||
"close": "Close"
|
||||
}
|
||||
},
|
||||
"cardLicenseIssuePopupPage": {
|
||||
@ -527,9 +532,31 @@
|
||||
"title": "Terms of Use has updated. Please confirm again.",
|
||||
"linkOfEula": "Click here to read the terms of use.",
|
||||
"linkOfDpa": "Click here to read the terms of use.",
|
||||
"linkOfPrivacyNotice": "Click here to read the terms of use.",
|
||||
"checkBoxForConsent": "Yes, I agree to the terms of use.",
|
||||
"forOdds": "for ODDS.",
|
||||
"button": "Continue"
|
||||
"button": "Continue",
|
||||
"linkOfPrivacyNotice": "Click here to read the terms of use."
|
||||
}
|
||||
},
|
||||
"supportPage": {
|
||||
"label": {
|
||||
"title": "Support",
|
||||
"howToUse": "How to use the system",
|
||||
"supportPageEnglish": "OMDS Cloud User Guide",
|
||||
"supportPageGerman": "OMDS Cloud Benutzerhandbuch",
|
||||
"supportPageFrench": "OMDS Cloud Mode d'emploi",
|
||||
"supportPageSpanish": "OMDS Cloud Guía del usuario"
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
|
||||
}
|
||||
},
|
||||
"filePropertyPopup": {
|
||||
"label": {
|
||||
"general": "General",
|
||||
"job": "Job",
|
||||
"close": "Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"headerDictations": "(es)Dictations",
|
||||
"headerWorkflow": "(es)Workflow",
|
||||
"headerPartners": "(es)Partners",
|
||||
"headerSupport": "(es)Support",
|
||||
"tier1": "(es)Admin",
|
||||
"tier2": "(es)BC",
|
||||
"tier3": "(es)Distributor",
|
||||
@ -250,7 +251,11 @@
|
||||
"poolTranscriptionist": "Lista de transcriptor",
|
||||
"fileBackup": "(es)File Backup",
|
||||
"downloadForBackup": "(es)Download for backup",
|
||||
"cancelDictation": "(es)Cancel Dictation"
|
||||
"applications": "(es)Applications",
|
||||
"cancelDictation": "(es)Cancel Dictation",
|
||||
"general": "(es)General",
|
||||
"job": "(es)Job",
|
||||
"close": "(es)Close"
|
||||
}
|
||||
},
|
||||
"cardLicenseIssuePopupPage": {
|
||||
@ -527,9 +532,31 @@
|
||||
"title": "(es)Terms of Use has updated. Please confirm again.",
|
||||
"linkOfEula": "(es)Click here to read the terms of use.",
|
||||
"linkOfDpa": "(es)Click here to read the terms of use.",
|
||||
"linkOfPrivacyNotice": "(es)Click here to read the terms of use.",
|
||||
"checkBoxForConsent": "(es)Yes, I agree to the terms of use.",
|
||||
"forOdds": "(es)for ODDS.",
|
||||
"button": "(es)Continue"
|
||||
"button": "(es)Continue",
|
||||
"linkOfPrivacyNotice": "(es)Click here to read the terms of use."
|
||||
}
|
||||
},
|
||||
"supportPage": {
|
||||
"label": {
|
||||
"title": "(es)Support",
|
||||
"howToUse": "(es)How to use the system",
|
||||
"supportPageEnglish": "OMDS Cloud User Guide",
|
||||
"supportPageGerman": "OMDS Cloud Benutzerhandbuch",
|
||||
"supportPageFrench": "OMDS Cloud Mode d'emploi",
|
||||
"supportPageSpanish": "OMDS Cloud Guía del usuario"
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "(es)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
|
||||
}
|
||||
},
|
||||
"filePropertyPopup": {
|
||||
"label": {
|
||||
"general": "(es)General",
|
||||
"job": "(es)Job",
|
||||
"close": "(es)Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"headerDictations": "(fr)Dictations",
|
||||
"headerWorkflow": "(fr)Workflow",
|
||||
"headerPartners": "(fr)Partners",
|
||||
"headerSupport": "(fr)Support",
|
||||
"tier1": "(fr)Admin",
|
||||
"tier2": "(fr)BC",
|
||||
"tier3": "(fr)Distributor",
|
||||
@ -250,7 +251,11 @@
|
||||
"poolTranscriptionist": "Liste de transcriptionniste",
|
||||
"fileBackup": "(fr)File Backup",
|
||||
"downloadForBackup": "(fr)Download for backup",
|
||||
"cancelDictation": "(fr)Cancel Dictation"
|
||||
"applications": "(fr)Applications",
|
||||
"cancelDictation": "(fr)Cancel Dictation",
|
||||
"general": "(fr)General",
|
||||
"job": "(fr)Job",
|
||||
"close": "(fr)Close"
|
||||
}
|
||||
},
|
||||
"cardLicenseIssuePopupPage": {
|
||||
@ -527,9 +532,31 @@
|
||||
"title": "(fr)Terms of Use has updated. Please confirm again.",
|
||||
"linkOfEula": "(fr)Click here to read the terms of use.",
|
||||
"linkOfDpa": "(fr)Click here to read the terms of use.",
|
||||
"linkOfPrivacyNotice": "(fr)Click here to read the terms of use.",
|
||||
"checkBoxForConsent": "(fr)Yes, I agree to the terms of use.",
|
||||
"forOdds": "(fr)for ODDS.",
|
||||
"button": "(fr)Continue"
|
||||
"button": "(fr)Continue",
|
||||
"linkOfPrivacyNotice": "(fr)Click here to read the terms of use."
|
||||
}
|
||||
},
|
||||
"supportPage": {
|
||||
"label": {
|
||||
"title": "(fr)Support",
|
||||
"howToUse": "(fr)How to use the system",
|
||||
"supportPageEnglish": "OMDS Cloud User Guide",
|
||||
"supportPageGerman": "OMDS Cloud Benutzerhandbuch",
|
||||
"supportPageFrench": "OMDS Cloud Mode d'emploi",
|
||||
"supportPageSpanish": "OMDS Cloud Guía del usuario"
|
||||
},
|
||||
"text": {
|
||||
"notResolved": "(fr)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
|
||||
}
|
||||
},
|
||||
"filePropertyPopup": {
|
||||
"label": {
|
||||
"general": "(fr)General",
|
||||
"job": "(fr)Job",
|
||||
"close": "(fr)Close"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -11,5 +11,10 @@
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
},
|
||||
"retry": {
|
||||
"strategy": "fixedDelay",
|
||||
"maxRetryCount": 3,
|
||||
"delayInterval": "00:00:10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,8 @@ import { AdB2cResponse, AdB2cUser } from "./types/types";
|
||||
import { error } from "console";
|
||||
import { makeADB2CKey, restoreAdB2cID } from "../common/cache";
|
||||
import { promisify } from "util";
|
||||
import { createRedisClient } from "../redis/redis";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
import { RedisClient } from "redis";
|
||||
|
||||
export class Adb2cTooManyRequestsError extends Error {}
|
||||
|
||||
@ -23,16 +23,24 @@ export class AdB2cService {
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
const credential = new ClientSecretCredential(
|
||||
process.env.ADB2C_TENANT_ID,
|
||||
process.env.ADB2C_CLIENT_ID,
|
||||
process.env.ADB2C_CLIENT_SECRET
|
||||
);
|
||||
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
|
||||
scopes: ["https://graph.microsoft.com/.default"],
|
||||
});
|
||||
try {
|
||||
const credential = new ClientSecretCredential(
|
||||
process.env.ADB2C_TENANT_ID,
|
||||
process.env.ADB2C_CLIENT_ID,
|
||||
process.env.ADB2C_CLIENT_SECRET
|
||||
);
|
||||
const authProvider = new TokenCredentialAuthenticationProvider(
|
||||
credential,
|
||||
{
|
||||
scopes: ["https://graph.microsoft.com/.default"],
|
||||
}
|
||||
);
|
||||
|
||||
this.graphClient = Client.initWithMiddleware({ authProvider });
|
||||
this.graphClient = Client.initWithMiddleware({ authProvider });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,9 +50,9 @@ export class AdB2cService {
|
||||
*/
|
||||
async getUsers(
|
||||
context: InvocationContext,
|
||||
redisClient: RedisClient,
|
||||
externalIds: string[]
|
||||
): Promise<AdB2cUser[] | undefined> {
|
||||
const redisClient = createRedisClient();
|
||||
): Promise<AdB2cUser[]> {
|
||||
try {
|
||||
const b2cUsers: AdB2cUser[] = [];
|
||||
const keys = externalIds.map((externalId) => makeADB2CKey(externalId));
|
||||
@ -123,7 +131,7 @@ export class AdB2cService {
|
||||
|
||||
return [...cachedUsers, ...b2cUsers];
|
||||
} else {
|
||||
return undefined;
|
||||
return b2cUsers;
|
||||
}
|
||||
} catch (e) {
|
||||
const { statusCode } = e;
|
||||
@ -132,7 +140,6 @@ export class AdB2cService {
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
redisClient.quit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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にキャッシュする値
|
||||
|
||||
25
dictation_function/src/common/cache/index.ts
vendored
25
dictation_function/src/common/cache/index.ts
vendored
@ -1,4 +1,4 @@
|
||||
import { ADB2C_PREFIX } from './constants';
|
||||
import { ADB2C_PREFIX, SEND_COMPLETE_PREFIX } from "./constants";
|
||||
|
||||
/**
|
||||
* ADB2Cのユーザー格納用のキーを生成する
|
||||
@ -6,8 +6,8 @@ import { ADB2C_PREFIX } from './constants';
|
||||
* @returns キャッシュのキー
|
||||
*/
|
||||
export const makeADB2CKey = (externalId: string): string => {
|
||||
return `${ADB2C_PREFIX}${externalId}`;
|
||||
}
|
||||
return `${ADB2C_PREFIX}${externalId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する
|
||||
@ -15,5 +15,20 @@ export const makeADB2CKey = (externalId: string): string => {
|
||||
* @returns 外部ユーザーID
|
||||
*/
|
||||
export const restoreAdB2cID = (key: string): string => {
|
||||
return key.replace(ADB2C_PREFIX, '');
|
||||
}
|
||||
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}`;
|
||||
};
|
||||
|
||||
@ -19,36 +19,166 @@ import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents
|
||||
import { AdB2cService } from "../adb2c/adb2c";
|
||||
import { SendGridService } from "../sendgrid/sendgrid";
|
||||
import { getMailFrom } from "../common/getEnv/getEnv";
|
||||
import { createRedisClient } from "../redis/redis";
|
||||
import { RedisClient } from "redis";
|
||||
import { promisify } from "util";
|
||||
import { makeSendCompKey } from "../common/cache";
|
||||
import {
|
||||
MAIL_U103,
|
||||
MAIL_U104,
|
||||
SEND_COMPLETE_PREFIX,
|
||||
DONE,
|
||||
} from "../common/cache/constants";
|
||||
|
||||
export async function licenseAlertProcessing(
|
||||
context: InvocationContext,
|
||||
datasource: DataSource,
|
||||
redisClient: RedisClient,
|
||||
sendgrid: SendGridService,
|
||||
adb2c: AdB2cService
|
||||
) {
|
||||
context.log("[IN]licenseAlertProcessing");
|
||||
const mailFrom = getMailFrom();
|
||||
const accountRepository = datasource.getRepository(Account);
|
||||
try {
|
||||
context.log("[IN]licenseAlertProcessing");
|
||||
|
||||
// 第五のアカウントを取得
|
||||
const accounts = await accountRepository.find({
|
||||
where: {
|
||||
tier: TIERS.TIER5,
|
||||
},
|
||||
relations: {
|
||||
primaryAdminUser: true,
|
||||
secondaryAdminUser: true,
|
||||
},
|
||||
});
|
||||
// redisのキー用
|
||||
const currentDate = new DateWithZeroTime();
|
||||
const formattedDate = `${currentDate.getFullYear()}-${(
|
||||
currentDate.getMonth() + 1
|
||||
).toString()}-${currentDate.getDate().toString()}`;
|
||||
const keysAsync = promisify(redisClient.keys).bind(redisClient);
|
||||
|
||||
const licenseRepository = datasource.getRepository(License);
|
||||
const currentDate = new DateWithZeroTime();
|
||||
const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
|
||||
const currentDateWithZeroTime = new DateWithZeroTime();
|
||||
const currentDateWithDayEndTime = new DateWithDayEndTime();
|
||||
const sendTargetAccounts = [] as accountInfo[];
|
||||
// メール送信対象のアカウント情報を取得
|
||||
const sendTargetAccounts = await getAlertMailTargetAccount(
|
||||
context,
|
||||
datasource
|
||||
);
|
||||
|
||||
const counts = async () => {
|
||||
// adb2cからメールアドレスを取得し、上記で取得したアカウントにマージする
|
||||
const sendTargetAccountsMargedAdb2c = await createAccountInfo(
|
||||
context,
|
||||
redisClient,
|
||||
adb2c,
|
||||
sendTargetAccounts
|
||||
);
|
||||
|
||||
// メール送信
|
||||
await sendAlertMail(
|
||||
context,
|
||||
redisClient,
|
||||
sendgrid,
|
||||
sendTargetAccountsMargedAdb2c,
|
||||
formattedDate
|
||||
);
|
||||
|
||||
// 最後まで処理が正常に通ったら、redisに保存した送信情報を削除する
|
||||
try {
|
||||
const delAsync = promisify(redisClient.del).bind(redisClient);
|
||||
|
||||
const keys = await keysAsync(`${SEND_COMPLETE_PREFIX}${formattedDate}*`);
|
||||
console.log(`delete terget:${keys}`);
|
||||
if (keys.length > 0) {
|
||||
const delResult = await delAsync(...keys);
|
||||
console.log(`delete number:${delResult}`);
|
||||
}
|
||||
} catch (e) {
|
||||
context.log("redis delete failed");
|
||||
throw e;
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
context.log("[OUT]licenseAlertProcessing");
|
||||
}
|
||||
}
|
||||
|
||||
export async function licenseAlert(
|
||||
myTimer: Timer,
|
||||
context: InvocationContext
|
||||
): Promise<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) {
|
||||
// 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
|
||||
const allocatableLicenseWithMargin = await licenseRepository.count({
|
||||
@ -109,6 +239,7 @@ export async function licenseAlertProcessing(
|
||||
let primaryAdminExternalId: string | undefined;
|
||||
let secondaryAdminExternalId: string | undefined;
|
||||
let parentCompanyName: string | undefined;
|
||||
|
||||
if (shortage !== 0 || userCount !== 0) {
|
||||
primaryAdminExternalId = account.primaryAdminUser
|
||||
? account.primaryAdminUser.external_id
|
||||
@ -143,12 +274,33 @@ export async function licenseAlertProcessing(
|
||||
secondaryAdminEmail: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
await counts();
|
||||
return sendTargetAccounts;
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log("getAlertMailTargetAccount failed.");
|
||||
throw e;
|
||||
} finally {
|
||||
context.log("[OUT]getAlertMailTargetAccount");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を取得し、アカウント情報を作成する
|
||||
* @param context
|
||||
* @param redisClient
|
||||
* @param adb2c
|
||||
* @param sendTargetAccounts RDBから取得したアカウント情報
|
||||
* @returns accountInfo[] メール送信対象のアカウント情報
|
||||
*/
|
||||
async function createAccountInfo(
|
||||
context: InvocationContext,
|
||||
redisClient: RedisClient,
|
||||
adb2c: AdB2cService,
|
||||
sendTargetAccounts: accountInfo[]
|
||||
): Promise<accountInfo[]> {
|
||||
// ADB2Cからユーザーを取得する用の外部ID配列を作成
|
||||
const externalIds = [] as string[];
|
||||
sendTargetAccounts.map((x) => {
|
||||
sendTargetAccounts.forEach((x) => {
|
||||
if (x.primaryAdminExternalId) {
|
||||
externalIds.push(x.primaryAdminExternalId);
|
||||
}
|
||||
@ -156,11 +308,10 @@ export async function licenseAlertProcessing(
|
||||
externalIds.push(x.secondaryAdminExternalId);
|
||||
}
|
||||
});
|
||||
const adb2cUsers = await adb2c.getUsers(context, externalIds);
|
||||
if (!adb2cUsers) {
|
||||
const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds);
|
||||
if (adb2cUsers.length === 0) {
|
||||
context.log("Target user not found");
|
||||
context.log("[OUT]licenseAlertProcessing");
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
// ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ
|
||||
sendTargetAccounts.map((info) => {
|
||||
@ -188,17 +339,54 @@ export async function licenseAlertProcessing(
|
||||
}
|
||||
}
|
||||
});
|
||||
return sendTargetAccounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* アラートメールを送信する
|
||||
* @param context
|
||||
* @param redisClient
|
||||
* @param sendgrid
|
||||
* @param sendTargetAccounts メール送信対象のアカウント情報
|
||||
* @param formattedDate redisのキーに使用する日付
|
||||
* @returns ユーザ情報
|
||||
*/
|
||||
async function sendAlertMail(
|
||||
context: InvocationContext,
|
||||
redisClient: RedisClient,
|
||||
sendgrid: SendGridService,
|
||||
sendTargetAccounts: accountInfo[],
|
||||
formattedDate: string
|
||||
): Promise<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) {
|
||||
// プライマリ管理者が入っているかチェック
|
||||
// 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ
|
||||
if (targetAccount.primaryAdminExternalId) {
|
||||
// メール送信
|
||||
// strictNullChecks対応
|
||||
if (targetAccount.primaryAdminEmail) {
|
||||
// ライセンス不足メール
|
||||
if (targetAccount.shortage !== 0) {
|
||||
if (!targetAccount.primaryAdminEmail) {
|
||||
continue;
|
||||
}
|
||||
// ライセンス不足メール
|
||||
if (targetAccount.shortage !== 0) {
|
||||
// redisに送信履歴がない場合のみ送信する
|
||||
const mailResult = await getAsync(
|
||||
makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.primaryAdminExternalId,
|
||||
MAIL_U103
|
||||
)
|
||||
);
|
||||
if (mailResult !== DONE) {
|
||||
const { subject, text, html } =
|
||||
await createMailContentOfLicenseShortage(
|
||||
targetAccount.companyName,
|
||||
@ -217,45 +405,107 @@ export async function licenseAlertProcessing(
|
||||
context.log(
|
||||
`Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}`
|
||||
);
|
||||
} catch {
|
||||
// 送信成功時、成功履歴をredisに保存
|
||||
try {
|
||||
const key = makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.primaryAdminExternalId,
|
||||
MAIL_U103
|
||||
);
|
||||
await setexAsync(key, ttl, DONE);
|
||||
context.log(
|
||||
"setex Result:",
|
||||
`key:${key},ttl:${ttl},value:Done`
|
||||
);
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
"setex failed.",
|
||||
`target: ${targetAccount.primaryAdminEmail}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
`Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}`
|
||||
);
|
||||
}
|
||||
|
||||
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
|
||||
if (targetAccount.secondaryAdminEmail) {
|
||||
// ライセンス不足メール
|
||||
if (targetAccount.shortage !== 0) {
|
||||
const { subject, text, html } =
|
||||
await createMailContentOfLicenseShortage(
|
||||
targetAccount.companyName,
|
||||
targetAccount.shortage,
|
||||
targetAccount.parentCompanyName
|
||||
);
|
||||
// メールを送信
|
||||
try {
|
||||
await sendgrid.sendMail(
|
||||
targetAccount.secondaryAdminEmail,
|
||||
mailFrom,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
);
|
||||
context.log(
|
||||
`Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
} catch {
|
||||
context.log(
|
||||
`Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス失効警告メール
|
||||
if (targetAccount.userCountOfLicenseExpiringSoon !== 0) {
|
||||
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
|
||||
if (
|
||||
targetAccount.secondaryAdminEmail &&
|
||||
targetAccount.secondaryAdminExternalId
|
||||
) {
|
||||
// redisに送信履歴がない場合のみ送信する
|
||||
const mailResult = await getAsync(
|
||||
makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.secondaryAdminExternalId,
|
||||
MAIL_U103
|
||||
)
|
||||
);
|
||||
if (mailResult !== DONE) {
|
||||
const { subject, text, html } =
|
||||
await createMailContentOfLicenseShortage(
|
||||
targetAccount.companyName,
|
||||
targetAccount.shortage,
|
||||
targetAccount.parentCompanyName
|
||||
);
|
||||
// メールを送信
|
||||
try {
|
||||
await sendgrid.sendMail(
|
||||
targetAccount.secondaryAdminEmail,
|
||||
mailFrom,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
);
|
||||
context.log(
|
||||
`Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
// 送信成功時、成功履歴をredisに保存
|
||||
try {
|
||||
const key = makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.secondaryAdminExternalId,
|
||||
MAIL_U103
|
||||
);
|
||||
await setexAsync(key, ttl, DONE);
|
||||
context.log(
|
||||
"setex Result:",
|
||||
`key:${key},ttl:${ttl},value:Done`
|
||||
);
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
"setex failed.",
|
||||
`target: ${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
`Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ライセンス失効警告メール
|
||||
if (targetAccount.userCountOfLicenseExpiringSoon !== 0) {
|
||||
// redisに送信履歴がない場合のみ送信する
|
||||
const mailResult = await getAsync(
|
||||
makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.primaryAdminExternalId,
|
||||
MAIL_U104
|
||||
)
|
||||
);
|
||||
if (mailResult !== DONE) {
|
||||
const { subject, text, html } =
|
||||
await createMailContentOfLicenseExpiringSoon(
|
||||
targetAccount.companyName,
|
||||
@ -274,80 +524,99 @@ export async function licenseAlertProcessing(
|
||||
context.log(
|
||||
`Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}`
|
||||
);
|
||||
} catch {
|
||||
// 送信成功時、成功履歴をredisに保存
|
||||
try {
|
||||
const key = makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.primaryAdminExternalId,
|
||||
MAIL_U104
|
||||
);
|
||||
await setexAsync(key, ttl, DONE);
|
||||
context.log(
|
||||
"setex Result:",
|
||||
`key:${key},ttl:${ttl},value:Done`
|
||||
);
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
"setex failed.",
|
||||
`target: ${targetAccount.primaryAdminEmail}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
`Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
|
||||
if (targetAccount.secondaryAdminEmail) {
|
||||
// ライセンス不足メール
|
||||
if (targetAccount.shortage !== 0) {
|
||||
const { subject, text, html } =
|
||||
await createMailContentOfLicenseExpiringSoon(
|
||||
targetAccount.companyName,
|
||||
targetAccount.userCountOfLicenseExpiringSoon,
|
||||
targetAccount.parentCompanyName
|
||||
);
|
||||
// メールを送信
|
||||
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
|
||||
if (
|
||||
targetAccount.secondaryAdminEmail &&
|
||||
targetAccount.secondaryAdminExternalId
|
||||
) {
|
||||
// redisに送信履歴がない場合のみ送信する
|
||||
const mailResult = makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.secondaryAdminExternalId,
|
||||
MAIL_U104
|
||||
);
|
||||
if (mailResult !== DONE) {
|
||||
const { subject, text, html } =
|
||||
await createMailContentOfLicenseExpiringSoon(
|
||||
targetAccount.companyName,
|
||||
targetAccount.userCountOfLicenseExpiringSoon,
|
||||
targetAccount.parentCompanyName
|
||||
);
|
||||
// メールを送信
|
||||
try {
|
||||
await sendgrid.sendMail(
|
||||
targetAccount.secondaryAdminEmail,
|
||||
mailFrom,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
);
|
||||
context.log(
|
||||
`Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
try {
|
||||
await sendgrid.sendMail(
|
||||
targetAccount.secondaryAdminEmail,
|
||||
mailFrom,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
const key = makeSendCompKey(
|
||||
formattedDate,
|
||||
targetAccount.secondaryAdminExternalId,
|
||||
MAIL_U104
|
||||
);
|
||||
await setexAsync(key, ttl, DONE);
|
||||
context.log(
|
||||
`Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
"setex Result:",
|
||||
`key:${key},ttl:${ttl},value:Done`
|
||||
);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
`Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
"setex failed.",
|
||||
`target: ${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
context.log(
|
||||
`Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
await sendMail();
|
||||
|
||||
context.log("[OUT]licenseAlertProcessing");
|
||||
}
|
||||
|
||||
export async function licenseAlert(
|
||||
myTimer: Timer,
|
||||
context: InvocationContext
|
||||
): Promise<void> {
|
||||
context.log("[IN]licenseAlert");
|
||||
|
||||
dotenv.config({ path: ".env" });
|
||||
dotenv.config({ path: ".env.local", override: true });
|
||||
const datasource = new DataSource({
|
||||
type: "mysql",
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
entities: [User, Account, License],
|
||||
});
|
||||
await datasource.initialize();
|
||||
|
||||
const adb2c = new AdB2cService();
|
||||
const sendgrid = new SendGridService();
|
||||
try {
|
||||
await licenseAlertProcessing(context, datasource, sendgrid, adb2c);
|
||||
} catch (e) {
|
||||
context.log("licenseAlertProcessing failed");
|
||||
context.error(e);
|
||||
context.log("sendAlertMail failed.");
|
||||
throw e;
|
||||
} finally {
|
||||
await datasource.destroy();
|
||||
context.log("[OUT]licenseAlert");
|
||||
context.log("[OUT]sendAlertMail");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
@ -20,12 +20,34 @@ export const createRedisClient = (): RedisClient => {
|
||||
host: host,
|
||||
port: port,
|
||||
password: password,
|
||||
retry_strategy: (options) => {
|
||||
if (options.attempt <= 3) {
|
||||
console.log(
|
||||
`Retrying connection to Redis. Attempt ${options.attempt}`
|
||||
);
|
||||
return 10000; // ミリ秒単位でのリトライまでの間隔
|
||||
} else {
|
||||
console.log("Exceeded maximum number of connection attempts.");
|
||||
return undefined; // リトライを終了
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
client = createClient({
|
||||
url: `rediss://${host}:${port}`,
|
||||
password: password,
|
||||
tls: {},
|
||||
retry_strategy: (options) => {
|
||||
if (options.attempt <= 3) {
|
||||
console.log(
|
||||
`Retrying connection to Redis. Attempt ${options.attempt}`
|
||||
);
|
||||
return 10000; // ミリ秒単位でのリトライまでの間隔
|
||||
} else {
|
||||
console.log("Exceeded maximum number of connection attempts.");
|
||||
return undefined; // リトライを終了
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ import { ADB2C_SIGN_IN_TYPE } from "../constants";
|
||||
import { SendGridService } from "../sendgrid/sendgrid";
|
||||
import { AdB2cService } from "../adb2c/adb2c";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
import { RedisClient } from "redis";
|
||||
import { createRedisClient } from "../redis/redis";
|
||||
|
||||
describe("licenseAlert", () => {
|
||||
dotenv.config({ path: ".env" });
|
||||
@ -40,6 +42,7 @@ describe("licenseAlert", () => {
|
||||
const context = new InvocationContext();
|
||||
const sendgridMock = new SendGridServiceMock() as SendGridService;
|
||||
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
|
||||
const redisClient = createRedisClient();
|
||||
// 呼び出し回数でテスト成否を判定
|
||||
const spySend = jest.spyOn(sendgridMock, "sendMail");
|
||||
|
||||
@ -63,8 +66,15 @@ describe("licenseAlert", () => {
|
||||
null
|
||||
);
|
||||
|
||||
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
|
||||
await licenseAlertProcessing(
|
||||
context,
|
||||
source,
|
||||
redisClient,
|
||||
sendgridMock,
|
||||
adb2cMock
|
||||
);
|
||||
expect(spySend.mock.calls).toHaveLength(1);
|
||||
redisClient.quit;
|
||||
});
|
||||
|
||||
it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => {
|
||||
@ -72,6 +82,7 @@ describe("licenseAlert", () => {
|
||||
const context = new InvocationContext();
|
||||
const sendgridMock = new SendGridServiceMock() as SendGridService;
|
||||
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
|
||||
const redisClient = createRedisClient();
|
||||
|
||||
// 呼び出し回数でテスト成否を判定
|
||||
const spySend = jest.spyOn(sendgridMock, "sendMail");
|
||||
@ -96,8 +107,15 @@ describe("licenseAlert", () => {
|
||||
null
|
||||
);
|
||||
|
||||
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
|
||||
await licenseAlertProcessing(
|
||||
context,
|
||||
source,
|
||||
redisClient,
|
||||
sendgridMock,
|
||||
adb2cMock
|
||||
);
|
||||
expect(spySend.mock.calls).toHaveLength(2);
|
||||
redisClient.quit;
|
||||
});
|
||||
|
||||
it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => {
|
||||
@ -105,6 +123,7 @@ describe("licenseAlert", () => {
|
||||
const context = new InvocationContext();
|
||||
const sendgridMock = new SendGridServiceMock() as SendGridService;
|
||||
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
|
||||
const redisClient = createRedisClient();
|
||||
|
||||
// 呼び出し回数でテスト成否を判定
|
||||
const spySend = jest.spyOn(sendgridMock, "sendMail");
|
||||
@ -142,8 +161,15 @@ describe("licenseAlert", () => {
|
||||
null
|
||||
);
|
||||
|
||||
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
|
||||
await licenseAlertProcessing(
|
||||
context,
|
||||
source,
|
||||
redisClient,
|
||||
sendgridMock,
|
||||
adb2cMock
|
||||
);
|
||||
expect(spySend.mock.calls).toHaveLength(0);
|
||||
redisClient.quit;
|
||||
});
|
||||
|
||||
it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => {
|
||||
@ -151,6 +177,7 @@ describe("licenseAlert", () => {
|
||||
const context = new InvocationContext();
|
||||
const sendgridMock = new SendGridServiceMock() as SendGridService;
|
||||
const adb2cMock = new AdB2cServiceMock() as AdB2cService;
|
||||
const redisClient = createRedisClient();
|
||||
|
||||
// 呼び出し回数でテスト成否を判定
|
||||
const spySend = jest.spyOn(sendgridMock, "sendMail");
|
||||
@ -175,8 +202,15 @@ describe("licenseAlert", () => {
|
||||
null
|
||||
);
|
||||
|
||||
await licenseAlertProcessing(context, source, sendgridMock, adb2cMock);
|
||||
await licenseAlertProcessing(
|
||||
context,
|
||||
source,
|
||||
redisClient,
|
||||
sendgridMock,
|
||||
adb2cMock
|
||||
);
|
||||
expect(spySend.mock.calls).toHaveLength(1);
|
||||
redisClient.quit;
|
||||
});
|
||||
});
|
||||
|
||||
@ -211,6 +245,7 @@ export class AdB2cServiceMock {
|
||||
*/
|
||||
async getUsers(
|
||||
context: InvocationContext,
|
||||
redisClient: RedisClient,
|
||||
externalIds: string[]
|
||||
): Promise<AdB2cUser[]> {
|
||||
const AdB2cMockUsers: AdB2cUser[] = [
|
||||
|
||||
@ -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;
|
||||
@ -1455,6 +1455,52 @@
|
||||
"tags": ["accounts"]
|
||||
}
|
||||
},
|
||||
"/accounts/company-name": {
|
||||
"post": {
|
||||
"operationId": "getCompanyName",
|
||||
"summary": "",
|
||||
"description": "指定したアカウントの会社名を取得します",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/GetCompanyNameRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetCompanyNameResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["accounts"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/confirm": {
|
||||
"post": {
|
||||
"operationId": "confirmUser",
|
||||
@ -3491,6 +3537,10 @@
|
||||
"type": "string",
|
||||
"description": "同意済み利用規約のバージョン(EULA)"
|
||||
},
|
||||
"acceptedPrivacyNoticeVersion": {
|
||||
"type": "string",
|
||||
"description": "同意済みプライバシーポリシーのバージョン"
|
||||
},
|
||||
"acceptedDpaVersion": {
|
||||
"type": "string",
|
||||
"description": "同意済み利用規約のバージョン(DPA)"
|
||||
@ -3504,6 +3554,7 @@
|
||||
"adminMail",
|
||||
"adminPassword",
|
||||
"acceptedEulaVersion",
|
||||
"acceptedPrivacyNoticeVersion",
|
||||
"acceptedDpaVersion",
|
||||
"token"
|
||||
]
|
||||
@ -4009,6 +4060,16 @@
|
||||
"properties": { "tier": { "type": "number", "description": "階層" } },
|
||||
"required": ["tier"]
|
||||
},
|
||||
"GetCompanyNameRequest": {
|
||||
"type": "object",
|
||||
"properties": { "accountId": { "type": "number" } },
|
||||
"required": ["accountId"]
|
||||
},
|
||||
"GetCompanyNameResponse": {
|
||||
"type": "object",
|
||||
"properties": { "companyName": { "type": "string" } },
|
||||
"required": ["companyName"]
|
||||
},
|
||||
"ConfirmRequest": {
|
||||
"type": "object",
|
||||
"properties": { "token": { "type": "string" } },
|
||||
@ -4236,12 +4297,20 @@
|
||||
"type": "string",
|
||||
"description": "更新バージョン(EULA)"
|
||||
},
|
||||
"acceptedPrivacyNoticeVersion": {
|
||||
"type": "string",
|
||||
"description": "更新バージョン(PrivacyNotice)"
|
||||
},
|
||||
"acceptedDPAVersion": {
|
||||
"type": "string",
|
||||
"description": "更新バージョン(DPA)"
|
||||
}
|
||||
},
|
||||
"required": ["idToken", "acceptedEULAVersion"]
|
||||
"required": [
|
||||
"idToken",
|
||||
"acceptedEULAVersion",
|
||||
"acceptedPrivacyNoticeVersion"
|
||||
]
|
||||
},
|
||||
"UpdateAcceptedVersionResponse": { "type": "object", "properties": {} },
|
||||
"GetMyUserResponse": {
|
||||
|
||||
@ -182,6 +182,8 @@ export const makeTestAccount = async (
|
||||
role: d?.role ?? 'admin none',
|
||||
author_id: d?.author_id ?? undefined,
|
||||
accepted_eula_version: d?.accepted_eula_version ?? '1.0',
|
||||
accepted_privacy_notice_version:
|
||||
d?.accepted_privacy_notice_version ?? '1.0',
|
||||
accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
|
||||
email_verified: d?.email_verified ?? true,
|
||||
auto_renew: d?.auto_renew ?? true,
|
||||
|
||||
@ -287,6 +287,7 @@ export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
|
||||
export const TERM_TYPE = {
|
||||
EULA: 'EULA',
|
||||
DPA: 'DPA',
|
||||
PRIVACY_NOTICE: 'PrivacyNotice',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
@ -68,6 +68,8 @@ import {
|
||||
GetAccountInfoMinimalAccessResponse,
|
||||
DeleteWorktypeRequestParam,
|
||||
DeleteWorktypeResponse,
|
||||
GetCompanyNameRequest,
|
||||
GetCompanyNameResponse,
|
||||
} from './types/types';
|
||||
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
@ -116,6 +118,7 @@ export class AccountsController {
|
||||
adminPassword,
|
||||
adminName,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
} = body;
|
||||
const role = USER_ROLES.NONE;
|
||||
@ -132,6 +135,7 @@ export class AccountsController {
|
||||
adminName,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
|
||||
@ -1550,4 +1554,56 @@ export class AccountsController {
|
||||
);
|
||||
return { tier };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetCompanyNameResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'getCompanyName',
|
||||
description: '指定したアカウントの会社名を取得します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Post('company-name')
|
||||
async getCompanyName(
|
||||
@Req() req: Request,
|
||||
@Body() body: GetCompanyNameRequest,
|
||||
): Promise<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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,6 +112,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -144,6 +145,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
// 作成したアカウントのIDが返ってくるか確認
|
||||
@ -161,6 +163,9 @@ describe('createAccount', () => {
|
||||
expect(account?.primary_admin_user_id).toBe(user?.id);
|
||||
expect(account?.secondary_admin_user_id).toBe(null);
|
||||
expect(user?.accepted_eula_version).toBe(acceptedEulaVersion);
|
||||
expect(user?.accepted_privacy_notice_version).toBe(
|
||||
acceptedPrivacyNoticeVersion,
|
||||
);
|
||||
expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion);
|
||||
expect(user?.account_id).toBe(accountId);
|
||||
expect(user?.role).toBe(role);
|
||||
@ -195,6 +200,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'admin none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -216,6 +222,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -264,6 +271,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'admin none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -286,6 +294,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -318,6 +327,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -345,6 +355,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -384,6 +395,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -411,6 +423,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -452,6 +465,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -480,6 +494,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -520,6 +535,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -551,6 +567,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -593,6 +610,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -641,6 +659,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -689,6 +708,7 @@ describe('createAccount', () => {
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedPrivacyNoticeVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
@ -734,6 +754,7 @@ describe('createAccount', () => {
|
||||
username,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@ -6694,3 +6715,60 @@ describe('getAccountInfoMinimalAccess', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('getCompanyName', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('アカウントIDから会社名が取得できること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AccountsService>(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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
PostWorktypeOptionItem,
|
||||
Author,
|
||||
Partner,
|
||||
GetCompanyNameResponse,
|
||||
} from './types/types';
|
||||
import {
|
||||
DateWithZeroTime,
|
||||
@ -175,6 +176,7 @@ export class AccountsService {
|
||||
username: string,
|
||||
role: string,
|
||||
acceptedEulaVersion: string,
|
||||
acceptedPrivacyNoticeVersion: string,
|
||||
acceptedDpaVersion: string,
|
||||
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
|
||||
this.logger.log(
|
||||
@ -184,6 +186,7 @@ export class AccountsService {
|
||||
`dealerAccountId: ${dealerAccountId}, ` +
|
||||
`role: ${role}, ` +
|
||||
`acceptedEulaVersion: ${acceptedEulaVersion}, ` +
|
||||
`acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` +
|
||||
`acceptedDpaVersion: ${acceptedDpaVersion} };`,
|
||||
);
|
||||
try {
|
||||
@ -232,6 +235,7 @@ export class AccountsService {
|
||||
externalUser.sub,
|
||||
role,
|
||||
acceptedEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
account = newAccount;
|
||||
@ -2151,4 +2155,51 @@ export class AccountsService {
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 自アカウントの会社名を取得する
|
||||
* @param accountId
|
||||
* @returns CompanyName
|
||||
*/
|
||||
async getCompanyName(
|
||||
context: Context,
|
||||
accountId: number,
|
||||
): Promise<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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,8 @@ export class CreateAccountRequest {
|
||||
adminPassword: string;
|
||||
@ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' })
|
||||
acceptedEulaVersion: string;
|
||||
@ApiProperty({ description: '同意済みプライバシーポリシーのバージョン' })
|
||||
acceptedPrivacyNoticeVersion: string;
|
||||
@ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' })
|
||||
acceptedDpaVersion: string;
|
||||
@ApiProperty({ description: 'reCAPTCHA Token' })
|
||||
@ -599,3 +601,13 @@ export class GetAccountInfoMinimalAccessResponse {
|
||||
@ApiProperty({ description: '階層' })
|
||||
tier: number;
|
||||
}
|
||||
export class GetCompanyNameRequest {
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
accountId: number;
|
||||
}
|
||||
export class GetCompanyNameResponse {
|
||||
@ApiProperty()
|
||||
companyName: string;
|
||||
}
|
||||
|
||||
@ -196,6 +196,7 @@ describe('checkIsAcceptedLatestVersion', () => {
|
||||
};
|
||||
|
||||
await createTermInfo(source, 'EULA', '1.0');
|
||||
await createTermInfo(source, 'PrivacyNotice', '1.0');
|
||||
await createTermInfo(source, 'DPA', '1.0');
|
||||
const result = await service.isAcceptedLatestVersion(context, idToken);
|
||||
expect(result).toBe(true);
|
||||
@ -219,6 +220,7 @@ describe('checkIsAcceptedLatestVersion', () => {
|
||||
};
|
||||
|
||||
await createTermInfo(source, 'EULA', '1.0');
|
||||
await createTermInfo(source, 'PrivacyNotice', '1.0');
|
||||
await createTermInfo(source, 'DPA', '1.0');
|
||||
const result = await service.isAcceptedLatestVersion(context, idToken);
|
||||
expect(result).toBe(true);
|
||||
@ -242,6 +244,7 @@ describe('checkIsAcceptedLatestVersion', () => {
|
||||
};
|
||||
|
||||
await createTermInfo(source, 'EULA', '1.1');
|
||||
await createTermInfo(source, 'PrivacyNotice', '1.0');
|
||||
await createTermInfo(source, 'DPA', '1.0');
|
||||
const result = await service.isAcceptedLatestVersion(context, idToken);
|
||||
expect(result).toBe(false);
|
||||
@ -265,6 +268,7 @@ describe('checkIsAcceptedLatestVersion', () => {
|
||||
};
|
||||
|
||||
await createTermInfo(source, 'EULA', '1.1');
|
||||
await createTermInfo(source, 'PrivacyNotice', '1.0');
|
||||
await createTermInfo(source, 'DPA', '1.0');
|
||||
const result = await service.isAcceptedLatestVersion(context, idToken);
|
||||
expect(result).toBe(false);
|
||||
@ -288,10 +292,35 @@ describe('checkIsAcceptedLatestVersion', () => {
|
||||
};
|
||||
|
||||
await createTermInfo(source, 'EULA', '1.0');
|
||||
await createTermInfo(source, 'PrivacyNotice', '1.0');
|
||||
await createTermInfo(source, 'DPA', '1.1');
|
||||
const result = await service.isAcceptedLatestVersion(context, idToken);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('同意済みプライバシーポリシーが最新でないときにチェックが通らないこと(第一~第四)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(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', () => {
|
||||
|
||||
@ -689,28 +689,38 @@ export class AuthService {
|
||||
const {
|
||||
acceptedEulaVersion,
|
||||
latestEulaVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
latestPrivacyNoticeVersion,
|
||||
acceptedDpaVersion,
|
||||
latestDpaVersion,
|
||||
tier,
|
||||
} = await this.usersRepository.getAcceptedAndLatestVersion(idToken.sub);
|
||||
|
||||
// 第五階層はEULAのみ判定
|
||||
// 第五階層はEULAとPrivacyNoticeのみ判定
|
||||
if (tier === TIERS.TIER5) {
|
||||
if (!acceptedEulaVersion) {
|
||||
if (!acceptedEulaVersion || !acceptedPrivacyNoticeVersion) {
|
||||
return false;
|
||||
}
|
||||
// 最新バージョンに同意済みか判定
|
||||
const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
|
||||
return eulaAccepted;
|
||||
const privacyNoticeAccepted =
|
||||
acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion;
|
||||
return eulaAccepted && privacyNoticeAccepted;
|
||||
} else {
|
||||
// 第一~第四階層はEULA、DPAを判定
|
||||
if (!acceptedEulaVersion || !acceptedDpaVersion) {
|
||||
// 第一~第四階層はEULA、PrivacyNotice、DPAを判定
|
||||
if (
|
||||
!acceptedEulaVersion ||
|
||||
!acceptedPrivacyNoticeVersion ||
|
||||
!acceptedDpaVersion
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// 最新バージョンに同意済みか判定
|
||||
const eulaAccepted = acceptedEulaVersion === latestEulaVersion;
|
||||
const privacyNoticeAccepted =
|
||||
acceptedPrivacyNoticeVersion === latestPrivacyNoticeVersion;
|
||||
const dpaAccepted = acceptedDpaVersion === latestDpaVersion;
|
||||
return eulaAccepted && dpaAccepted;
|
||||
return eulaAccepted && privacyNoticeAccepted && dpaAccepted;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
|
||||
@ -22,8 +22,10 @@ export class AccessTokenRequest {}
|
||||
export type TermsCheckInfo = {
|
||||
tier: number;
|
||||
acceptedEulaVersion?: string;
|
||||
acceptedPrivacyNoticeVersion?: string;
|
||||
acceptedDpaVersion?: string;
|
||||
latestEulaVersion: string;
|
||||
latestPrivacyNoticeVersion: string;
|
||||
latestDpaVersion: string;
|
||||
};
|
||||
|
||||
|
||||
@ -139,6 +139,7 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
role: 'none',
|
||||
author_id: '',
|
||||
accepted_eula_version: '1.0',
|
||||
accepted_privacy_notice_version: '1.0',
|
||||
accepted_dpa_version: '1.0',
|
||||
email_verified: true,
|
||||
deleted_at: null,
|
||||
|
||||
@ -470,6 +470,7 @@ const defaultTasksRepositoryMockValue: {
|
||||
external_id: 'userId',
|
||||
role: 'typist',
|
||||
accepted_eula_version: '',
|
||||
accepted_privacy_notice_version: '',
|
||||
accepted_dpa_version: '',
|
||||
email_verified: true,
|
||||
auto_renew: true,
|
||||
|
||||
@ -34,6 +34,8 @@ describe('利用規約取得', () => {
|
||||
|
||||
await createTermInfo(source, 'EULA', 'v1.0');
|
||||
await createTermInfo(source, 'EULA', 'v1.1');
|
||||
await createTermInfo(source, 'PrivacyNotice', 'v1.0');
|
||||
await createTermInfo(source, 'PrivacyNotice', 'v1.1');
|
||||
await createTermInfo(source, 'DPA', 'v1.0');
|
||||
await createTermInfo(source, 'DPA', 'v1.2');
|
||||
|
||||
@ -42,8 +44,10 @@ describe('利用規約取得', () => {
|
||||
|
||||
expect(result[0].documentType).toBe('EULA');
|
||||
expect(result[0].version).toBe('v1.1');
|
||||
expect(result[1].documentType).toBe('DPA');
|
||||
expect(result[1].version).toBe('v1.2');
|
||||
expect(result[1].documentType).toBe('PrivacyNotice');
|
||||
expect(result[1].version).toBe('v1.1');
|
||||
expect(result[2].documentType).toBe('DPA');
|
||||
expect(result[2].version).toBe('v1.2');
|
||||
});
|
||||
|
||||
it('利用規約情報(EULA、DPA両方)が存在しない場合エラーとなる', async () => {
|
||||
@ -75,6 +79,21 @@ describe('利用規約取得', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('利用規約情報(PrivacyNoticeのみ)が存在しない場合エラーとなる', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<TermsService>(TermsService);
|
||||
await createTermInfo(source, 'PrivacyNotice', 'v1.0');
|
||||
const context = makeContext(uuidv4());
|
||||
await expect(service.getTermsInfo(context)).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('利用規約情報(DPAのみ)が存在しない場合エラーとなる', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
|
||||
@ -19,13 +19,17 @@ export class TermsService {
|
||||
`[IN] [${context.getTrackingId()}] ${this.getTermsInfo.name}`,
|
||||
);
|
||||
try {
|
||||
const { eulaVersion, dpaVersion } =
|
||||
const { eulaVersion, privacyNoticeVersion, dpaVersion } =
|
||||
await this.termsRepository.getLatestTermsInfo();
|
||||
return [
|
||||
{
|
||||
documentType: TERM_TYPE.EULA,
|
||||
version: eulaVersion,
|
||||
},
|
||||
{
|
||||
documentType: TERM_TYPE.PRIVACY_NOTICE,
|
||||
version: privacyNoticeVersion,
|
||||
},
|
||||
{
|
||||
documentType: TERM_TYPE.DPA,
|
||||
version: dpaVersion,
|
||||
|
||||
@ -13,5 +13,6 @@ export class GetTermsInfoResponse {
|
||||
|
||||
export type TermsVersion = {
|
||||
eulaVersion: string;
|
||||
privacyNoticeVersion: string;
|
||||
dpaVersion: string;
|
||||
};
|
||||
|
||||
@ -263,6 +263,8 @@ export class UpdateAcceptedVersionRequest {
|
||||
idToken: string;
|
||||
@ApiProperty({ description: '更新バージョン(EULA)' })
|
||||
acceptedEULAVersion: string;
|
||||
@ApiProperty({ description: '更新バージョン(PrivacyNotice)' })
|
||||
acceptedPrivacyNoticeVersion: string;
|
||||
@ApiProperty({ description: '更新バージョン(DPA)', required: false })
|
||||
acceptedDPAVersion?: string;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Ip,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
@ -136,6 +137,7 @@ export class UsersController {
|
||||
@Get()
|
||||
async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
@ -627,7 +629,12 @@ export class UsersController {
|
||||
async updateAcceptedVersion(
|
||||
@Body() body: UpdateAcceptedVersionRequest,
|
||||
): Promise<UpdateAcceptedVersionResponse> {
|
||||
const { idToken, acceptedEULAVersion, acceptedDPAVersion } = body;
|
||||
const {
|
||||
idToken,
|
||||
acceptedEULAVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDPAVersion,
|
||||
} = body;
|
||||
|
||||
const context = makeContext(uuidv4());
|
||||
|
||||
@ -650,6 +657,7 @@ export class UsersController {
|
||||
context,
|
||||
verifiedIdToken.sub,
|
||||
acceptedEULAVersion,
|
||||
acceptedPrivacyNoticeVersion,
|
||||
acceptedDPAVersion,
|
||||
);
|
||||
return {};
|
||||
|
||||
@ -208,6 +208,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_privacy_notice_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
@ -259,6 +260,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_privacy_notice_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
@ -306,6 +308,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_privacy_notice_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: true,
|
||||
created_by: 'string;',
|
||||
@ -358,6 +361,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_privacy_notice_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
@ -2617,7 +2621,12 @@ describe('UsersService.updateAcceptedVersion', () => {
|
||||
const context = makeContext(uuidv4());
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
await service.updateAcceptedVersion(context, admin.external_id, 'v2.0');
|
||||
await service.updateAcceptedVersion(
|
||||
context,
|
||||
admin.external_id,
|
||||
'v2.0',
|
||||
'v2.0',
|
||||
);
|
||||
const user = await getUser(source, admin.id);
|
||||
|
||||
expect(user?.accepted_eula_version).toBe('v2.0');
|
||||
@ -2637,6 +2646,7 @@ describe('UsersService.updateAcceptedVersion', () => {
|
||||
context,
|
||||
admin.external_id,
|
||||
'v2.0',
|
||||
'v2.0',
|
||||
'v3.0',
|
||||
);
|
||||
const user = await getUser(source, admin.id);
|
||||
@ -2660,6 +2670,7 @@ describe('UsersService.updateAcceptedVersion', () => {
|
||||
context,
|
||||
admin.external_id,
|
||||
'v2.0',
|
||||
'v2.0',
|
||||
undefined,
|
||||
),
|
||||
).rejects.toEqual(
|
||||
|
||||
@ -403,6 +403,7 @@ export class UsersService {
|
||||
role,
|
||||
accepted_dpa_version: null,
|
||||
accepted_eula_version: null,
|
||||
accepted_privacy_notice_version: null,
|
||||
encryption: false,
|
||||
encryption_password: null,
|
||||
prompt: false,
|
||||
@ -422,6 +423,7 @@ export class UsersService {
|
||||
prompt: prompt ?? false,
|
||||
accepted_dpa_version: null,
|
||||
accepted_eula_version: null,
|
||||
accepted_privacy_notice_version: null,
|
||||
};
|
||||
default:
|
||||
//不正なroleが指定された場合はログを出力してエラーを返す
|
||||
@ -538,6 +540,7 @@ export class UsersService {
|
||||
// DBから同一アカウントのユーザ一覧を取得する
|
||||
const dbUsers = await this.usersRepository.findSameAccountUsers(
|
||||
externalId,
|
||||
context,
|
||||
);
|
||||
|
||||
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
|
||||
@ -1044,12 +1047,14 @@ export class UsersService {
|
||||
* @param context
|
||||
* @param idToken
|
||||
* @param eulaVersion
|
||||
* @param privacyNoticeVersion
|
||||
* @param dpaVersion
|
||||
*/
|
||||
async updateAcceptedVersion(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
eulaVersion: string,
|
||||
privacyNoticeVersion: string,
|
||||
dpaVersion?: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
@ -1058,6 +1063,7 @@ export class UsersService {
|
||||
} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`eulaVersion: ${eulaVersion}, ` +
|
||||
`privacyNoticeVersion: ${privacyNoticeVersion}, ` +
|
||||
`dpaVersion: ${dpaVersion}, };`,
|
||||
);
|
||||
|
||||
@ -1065,6 +1071,7 @@ export class UsersService {
|
||||
await this.usersRepository.updateAcceptedTermsVersion(
|
||||
externalId,
|
||||
eulaVersion,
|
||||
privacyNoticeVersion,
|
||||
dpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@ -127,6 +127,7 @@ export class AccountsRepositoryService {
|
||||
adminExternalUserId: string,
|
||||
adminUserRole: string,
|
||||
adminUserAcceptedEulaVersion?: string,
|
||||
adminUserAcceptedPrivacyNoticeVersion?: string,
|
||||
adminUserAcceptedDpaVersion?: string,
|
||||
): Promise<{ newAccount: Account; adminUser: User }> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
@ -148,6 +149,8 @@ export class AccountsRepositoryService {
|
||||
user.external_id = adminExternalUserId;
|
||||
user.role = adminUserRole;
|
||||
user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
|
||||
user.accepted_privacy_notice_version =
|
||||
adminUserAcceptedPrivacyNoticeVersion ?? null;
|
||||
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
|
||||
}
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
|
||||
@ -24,6 +24,14 @@ export class TermsRepositoryService {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
const latestPrivacyNoticeInfo = await termRepo.findOne({
|
||||
where: {
|
||||
document_type: TERM_TYPE.PRIVACY_NOTICE,
|
||||
},
|
||||
order: {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
const latestDpaInfo = await termRepo.findOne({
|
||||
where: {
|
||||
document_type: TERM_TYPE.DPA,
|
||||
@ -33,13 +41,16 @@ export class TermsRepositoryService {
|
||||
},
|
||||
});
|
||||
|
||||
if (!latestEulaInfo || !latestDpaInfo) {
|
||||
if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) {
|
||||
throw new TermInfoNotFoundError(
|
||||
`Terms info is not found. latestEulaInfo: ${latestEulaInfo}, latestDpaInfo: ${latestDpaInfo}`,
|
||||
`Terms info is not found. latestEulaInfo: ${latestEulaInfo},
|
||||
latestPrivacyNoticeInfo: ${latestPrivacyNoticeInfo},
|
||||
latestDpaInfo: ${latestDpaInfo}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
eulaVersion: latestEulaInfo.version,
|
||||
privacyNoticeVersion: latestEulaInfo.version,
|
||||
dpaVersion: latestDpaInfo.version,
|
||||
};
|
||||
});
|
||||
|
||||
@ -34,6 +34,9 @@ export class User {
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_eula_version: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_privacy_notice_version: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_dpa_version: string | null;
|
||||
|
||||
@ -112,6 +115,9 @@ export class UserArchive {
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_eula_version: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_privacy_notice_version: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
accepted_dpa_version: string | null;
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
import { Account } from '../accounts/entity/account.entity';
|
||||
import { Workflow } from '../workflows/entity/workflow.entity';
|
||||
import { Worktype } from '../worktypes/entity/worktype.entity';
|
||||
import { Context } from '../../common/log';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepositoryService {
|
||||
@ -340,7 +341,10 @@ export class UsersRepositoryService {
|
||||
* @param externalId
|
||||
* @returns User[]
|
||||
*/
|
||||
async findSameAccountUsers(external_id: string): Promise<User[]> {
|
||||
async findSameAccountUsers(
|
||||
external_id: string,
|
||||
context: Context,
|
||||
): Promise<User[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const repo = entityManager.getRepository(User);
|
||||
|
||||
@ -359,8 +363,9 @@ export class UsersRepositoryService {
|
||||
license: true,
|
||||
},
|
||||
where: { account_id: accountId },
|
||||
comment: `${context.getTrackingId()}`,
|
||||
});
|
||||
|
||||
|
||||
return dbUsers;
|
||||
});
|
||||
}
|
||||
@ -471,6 +476,14 @@ export class UsersRepositoryService {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
const latestPrivacyNoticeInfo = await termRepo.findOne({
|
||||
where: {
|
||||
document_type: TERM_TYPE.PRIVACY_NOTICE,
|
||||
},
|
||||
order: {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
const latestDpaInfo = await termRepo.findOne({
|
||||
where: {
|
||||
document_type: TERM_TYPE.DPA,
|
||||
@ -479,16 +492,18 @@ export class UsersRepositoryService {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
if (!latestEulaInfo || !latestDpaInfo) {
|
||||
if (!latestEulaInfo || !latestPrivacyNoticeInfo || !latestDpaInfo) {
|
||||
throw new TermInfoNotFoundError(`Terms info is not found.`);
|
||||
}
|
||||
|
||||
return {
|
||||
tier: user.account.tier,
|
||||
acceptedEulaVersion: user.accepted_eula_version ?? undefined,
|
||||
acceptedPrivacyNoticeVersion:
|
||||
user.accepted_privacy_notice_version ?? undefined,
|
||||
acceptedDpaVersion: user.accepted_dpa_version ?? undefined,
|
||||
latestEulaVersion: latestEulaInfo.version,
|
||||
latestPrivacyNoticeVersion: latestPrivacyNoticeInfo.version,
|
||||
latestDpaVersion: latestDpaInfo.version,
|
||||
};
|
||||
});
|
||||
@ -498,12 +513,14 @@ export class UsersRepositoryService {
|
||||
* 同意済み利用規約のバージョンを更新する
|
||||
* @param externalId
|
||||
* @param eulaVersion
|
||||
* @param privacyNoticeVersion
|
||||
* @param dpaVersion
|
||||
* @returns update
|
||||
*/
|
||||
async updateAcceptedTermsVersion(
|
||||
externalId: string,
|
||||
eulaVersion: string,
|
||||
privacyNoticeVersion: string,
|
||||
dpaVersion: string | undefined,
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
@ -531,6 +548,11 @@ export class UsersRepositoryService {
|
||||
if (!eulaVersion) {
|
||||
throw new UpdateTermsVersionNotSetError(`EULA version param not set.`);
|
||||
}
|
||||
if (!privacyNoticeVersion) {
|
||||
throw new UpdateTermsVersionNotSetError(
|
||||
`PrivacyNotice version param not set.`,
|
||||
);
|
||||
}
|
||||
if (user.account.tier !== TIERS.TIER5 && !dpaVersion) {
|
||||
throw new UpdateTermsVersionNotSetError(
|
||||
`DPA version param not set. User's tier: ${user.account.tier}`,
|
||||
@ -538,6 +560,8 @@ export class UsersRepositoryService {
|
||||
}
|
||||
|
||||
user.accepted_eula_version = eulaVersion;
|
||||
user.accepted_privacy_notice_version =
|
||||
privacyNoticeVersion ?? user.accepted_privacy_notice_version;
|
||||
user.accepted_dpa_version = dpaVersion ?? user.accepted_dpa_version;
|
||||
await userRepo.update({ id: user.id }, user);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user