diff --git a/azure-pipelines-staging.yml b/azure-pipelines-staging.yml index 4ecbc47..22dd085 100644 --- a/azure-pipelines-staging.yml +++ b/azure-pipelines-staging.yml @@ -58,6 +58,7 @@ jobs: npm run test env: JWT_PUBLIC_KEY: $(token-public-key) + JWT_PRIVATE_KEY: $(token-private-key) SENDGRID_API_KEY: $(sendgrid-api-key) NOTIFICATION_HUB_NAME: $(notification-hub-name) NOTIFICATION_HUB_CONNECT_STRING: $(notification-hub-connect-string) @@ -73,6 +74,12 @@ jobs: ADB2C_TENANT_ID: $(adb2c-tenant-id) ADB2C_CLIENT_ID: $(adb2c-client-id) ADB2C_CLIENT_SECRET: $(adb2c-client-secret) + MAIL_FROM: xxxxxx + APP_DOMAIN: xxxxxxxxx + EMAIL_CONFIRM_LIFETIME : 0 + TENANT_NAME : xxxxxxxxxxxx + SIGNIN_FLOW_NAME : xxxxxxxxxxxx + STORAGE_TOKEN_EXPIRE_TIME : 0 - task: Docker@0 displayName: build inputs: diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index cf6e1d3..7abc3ca 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -1,5 +1,6 @@ import { Route, Routes } from "react-router-dom"; import TopPage from "pages/TopPage"; +import AuthPage from "pages/AuthPage"; import LoginPage from "pages/LoginPage"; import SamplePage from "pages/SamplePage"; import { AuthErrorPage } from "pages/ErrorPage"; @@ -20,18 +21,21 @@ import WorkflowPage from "pages/WorkflowPage"; import TypistGroupSettingPage from "pages/TypistGroupSettingPage"; import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage"; import AccountPage from "pages/AccountPage"; +import AcceptToUsePage from "pages/TermsPage"; import { TemplateFilePage } from "pages/TemplateFilePage"; import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess"; const AppRouter: React.FC = () => ( } /> + } /> } /> } /> } /> + } /> } /> } /> } /> diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 265a030..680a7e6 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -127,7 +127,7 @@ export interface AllocatableLicenseInfo { * @type {string} * @memberof AllocatableLicenseInfo */ - 'expiryDate': string; + 'expiryDate'?: string; } /** * @@ -2561,6 +2561,44 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorktype: async (id: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('deleteWorktype', 'id', id) + const localVarPath = `/accounts/worktypes/{id}/delete` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3340,6 +3378,17 @@ export const AccountsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccountAndData(deleteAccountRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteWorktype(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorktype(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary @@ -3616,6 +3665,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise { return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorktype(id: number, options?: any): AxiosPromise { + return localVarFp.deleteWorktype(id, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -3888,6 +3947,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteWorktype(id: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteWorktype(id, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/app/store.ts b/dictation_client/src/app/store.ts index e48a4ba..fe711ab 100644 --- a/dictation_client/src/app/store.ts +++ b/dictation_client/src/app/store.ts @@ -18,6 +18,7 @@ import worktype from "features/workflow/worktype/worktypeSlice"; import account from "features/account/accountSlice"; import template from "features/workflow/template/templateSlice"; import workflow from "features/workflow/workflowSlice"; +import terms from "features/terms/termsSlice"; export const store = configureStore({ reducer: { @@ -40,6 +41,7 @@ export const store = configureStore({ account, template, workflow, + terms, }, }); diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 8c3236f..559a39e 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -32,6 +32,7 @@ export const errorCodes = [ "E010206", // DBのTierが想定外の値エラー "E010207", // ユーザーのRole変更不可エラー "E010208", // ユーザーの暗号化パスワード不足エラー + "E010209", // ユーザーの同意済み利用規約バージョンが最新でないエラー "E010301", // メールアドレス登録済みエラー "E010302", // authorId重複エラー "E010401", // PONumber重複エラー @@ -55,6 +56,7 @@ export const errorCodes = [ "E011001", // ワークタイプ重複エラー "E011002", // ワークタイプ登録上限超過エラー "E011003", // ワークタイプ不在エラー + "E011004", // ワークタイプ使用中エラー "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー "E013002", // ワークフロー不在エラー ] as const; diff --git a/dictation_client/src/common/errors/utils.ts b/dictation_client/src/common/errors/utils.ts index 8f756ca..3dd2410 100644 --- a/dictation_client/src/common/errors/utils.ts +++ b/dictation_client/src/common/errors/utils.ts @@ -81,3 +81,21 @@ const isErrorResponse = (error: unknown): error is ErrorResponse => { const isErrorCode = (errorCode: string): errorCode is ErrorCodeType => errorCodes.includes(errorCode as ErrorCodeType); + +export const isErrorObject = ( + data: unknown +): data is { error: ErrorObject } => { + if ( + data && + typeof data === "object" && + "error" in data && + typeof (data as { error: ErrorObject }).error === "object" && + typeof (data as { error: ErrorObject }).error.message === "string" && + typeof (data as { error: ErrorObject }).error.code === "string" && + (typeof (data as { error: ErrorObject }).error.statusCode === "number" || + (data as { error: ErrorObject }).error.statusCode === undefined) + ) { + return true; + } + return false; +}; diff --git a/dictation_client/src/common/msalConfig.ts b/dictation_client/src/common/msalConfig.ts index e90e744..6a28910 100644 --- a/dictation_client/src/common/msalConfig.ts +++ b/dictation_client/src/common/msalConfig.ts @@ -5,7 +5,7 @@ export const msalConfig: Configuration = { clientId: import.meta.env.VITE_B2C_CLIENTID, authority: import.meta.env.VITE_B2C_AUTHORITY, knownAuthorities: [import.meta.env.VITE_B2C_KNOWNAUTHORITIES], - redirectUri: `${globalThis.location.origin}/login`, + redirectUri: `${globalThis.location.origin}/auth`, navigateToLoginRequestUrl: false, }, cache: { diff --git a/dictation_client/src/common/token.ts b/dictation_client/src/common/token.ts index 587a0ea..c41442f 100644 --- a/dictation_client/src/common/token.ts +++ b/dictation_client/src/common/token.ts @@ -62,3 +62,16 @@ export const isIdToken = (arg: any): arg is IdToken => { return true; }; + +export const getIdTokenFromLocalStorage = ( + localStorageKeyforIdToken: string +): string | null => { + const idTokenString = localStorage.getItem(localStorageKeyforIdToken); + if (idTokenString) { + const idTokenObject = JSON.parse(idTokenString); + if (isIdToken(idTokenObject)) { + return idTokenObject.secret; + } + } + return null; +}; diff --git a/dictation_client/src/features/login/loginSlice.ts b/dictation_client/src/features/login/loginSlice.ts index 322a3ce..312bae1 100644 --- a/dictation_client/src/features/login/loginSlice.ts +++ b/dictation_client/src/features/login/loginSlice.ts @@ -1,17 +1,26 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { LoginState } from "./state"; import { loginAsync } from "./operations"; const initialState: LoginState = { apps: { LoginApiCallStatus: "none", + localStorageKeyforIdToken: null, }, }; export const loginSlice = createSlice({ name: "login", initialState, - reducers: {}, + reducers: { + changeLocalStorageKeyforIdToken: ( + state, + action: PayloadAction<{ localStorageKeyforIdToken: string }> + ) => { + const { localStorageKeyforIdToken } = action.payload; + state.apps.localStorageKeyforIdToken = localStorageKeyforIdToken; + }, + }, extraReducers: (builder) => { builder.addCase(loginAsync.pending, (state) => { state.apps.LoginApiCallStatus = "pending"; @@ -25,4 +34,5 @@ export const loginSlice = createSlice({ }, }); +export const { changeLocalStorageKeyforIdToken } = loginSlice.actions; export default loginSlice.reducer; diff --git a/dictation_client/src/features/login/operations.ts b/dictation_client/src/features/login/operations.ts index 61ddece..0ac9edd 100644 --- a/dictation_client/src/features/login/operations.ts +++ b/dictation_client/src/features/login/operations.ts @@ -3,6 +3,7 @@ import type { RootState } from "app/store"; import { setToken } from "features/auth/authSlice"; import { AuthApi } from "../../api/api"; import { Configuration } from "../../api/configuration"; +import { ErrorObject, createErrorObject } from "../../common/errors"; export const loginAsync = createAsyncThunk< { @@ -14,7 +15,7 @@ export const loginAsync = createAsyncThunk< { // rejectした時の返却値の型 rejectValue: { - /* Empty Object */ + error: ErrorObject; }; } >("login/loginAsync", async (args, thunkApi) => { @@ -41,6 +42,8 @@ export const loginAsync = createAsyncThunk< return {}; } catch (e) { - return thunkApi.rejectWithValue({}); + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + return thunkApi.rejectWithValue({ error }); } }); diff --git a/dictation_client/src/features/login/selectors.ts b/dictation_client/src/features/login/selectors.ts index 9615b35..d0ded8e 100644 --- a/dictation_client/src/features/login/selectors.ts +++ b/dictation_client/src/features/login/selectors.ts @@ -4,3 +4,7 @@ export const selectLoginApiCallStatus = ( state: RootState ): "fulfilled" | "rejected" | "none" | "pending" => state.login.apps.LoginApiCallStatus; + +export const selectLocalStorageKeyforIdToken = ( + state: RootState +): string | null => state.login.apps.localStorageKeyforIdToken; diff --git a/dictation_client/src/features/login/state.ts b/dictation_client/src/features/login/state.ts index 98a61ad..98fa599 100644 --- a/dictation_client/src/features/login/state.ts +++ b/dictation_client/src/features/login/state.ts @@ -4,4 +4,5 @@ export interface LoginState { export interface Apps { LoginApiCallStatus: "fulfilled" | "rejected" | "none" | "pending"; + localStorageKeyforIdToken: string | null; } diff --git a/dictation_client/src/features/signup/operations.ts b/dictation_client/src/features/signup/operations.ts index 67e505e..2557b0d 100644 --- a/dictation_client/src/features/signup/operations.ts +++ b/dictation_client/src/features/signup/operations.ts @@ -3,10 +3,12 @@ import type { RootState } from "app/store"; import { ErrorObject, createErrorObject } from "common/errors"; import { getTranslationID } from "translation"; import { closeSnackbar, openSnackbar } from "features/ui/uiSlice"; +import { TERMS_DOCUMENT_TYPE } from "features/terms/constants"; import { AccountsApi, CreateAccountRequest, GetDealersResponse, + TermsApi, } from "../../api/api"; import { Configuration } from "../../api/configuration"; @@ -93,3 +95,42 @@ export const getDealersAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const getLatestEulaVersionAsync = createAsyncThunk< + string, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("login/getLatestEulaVersionAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const config = new Configuration(configuration); + const termsApi = new TermsApi(config); + + try { + const termsInfo = await termsApi.getTermsInfo(); + const latestEulaVersion = termsInfo.data.termsInfo.find( + (val) => val.documentType === TERMS_DOCUMENT_TYPE.EULA + ); + if (!latestEulaVersion) { + throw new Error("EULA info is not found"); + } + return latestEulaVersion.version; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/signup/selectors.ts b/dictation_client/src/features/signup/selectors.ts index 87c3118..823347f 100644 --- a/dictation_client/src/features/signup/selectors.ts +++ b/dictation_client/src/features/signup/selectors.ts @@ -72,3 +72,6 @@ export const selectSelectedDealer = (state: RootState) => { const { dealer } = state.signup.apps; return dealers.find((x: Dealer) => x.id === dealer); }; + +export const selectEulaVersion = (state: RootState) => + state.signup.domain.eulaVersion; diff --git a/dictation_client/src/features/signup/signupSlice.ts b/dictation_client/src/features/signup/signupSlice.ts index 5b03e63..0f6bd83 100644 --- a/dictation_client/src/features/signup/signupSlice.ts +++ b/dictation_client/src/features/signup/signupSlice.ts @@ -1,6 +1,10 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { SignupState } from "./state"; -import { getDealersAsync, signupAsync } from "./operations"; +import { + getDealersAsync, + getLatestEulaVersionAsync, + signupAsync, +} from "./operations"; const initialState: SignupState = { apps: { @@ -15,6 +19,7 @@ const initialState: SignupState = { }, domain: { dealers: [], + eulaVersion: "", }, }; @@ -74,6 +79,15 @@ export const signupSlice = createSlice({ builder.addCase(getDealersAsync.rejected, () => { // }); + builder.addCase(getLatestEulaVersionAsync.pending, () => { + // + }); + builder.addCase(getLatestEulaVersionAsync.fulfilled, (state, action) => { + state.domain.eulaVersion = action.payload; + }); + builder.addCase(getLatestEulaVersionAsync.rejected, () => { + // + }); }, }); export const { diff --git a/dictation_client/src/features/signup/state.ts b/dictation_client/src/features/signup/state.ts index 164ab04..36850ad 100644 --- a/dictation_client/src/features/signup/state.ts +++ b/dictation_client/src/features/signup/state.ts @@ -18,4 +18,5 @@ export interface Apps { export interface Domain { dealers: Dealer[]; + eulaVersion: string; } diff --git a/dictation_client/src/features/terms/constants.ts b/dictation_client/src/features/terms/constants.ts new file mode 100644 index 0000000..dd78e43 --- /dev/null +++ b/dictation_client/src/features/terms/constants.ts @@ -0,0 +1,8 @@ +/** + * 利用規約の種類 + * @const {string[]} + */ +export const TERMS_DOCUMENT_TYPE = { + DPA: "DPA", + EULA: "EULA", +} as const; diff --git a/dictation_client/src/features/terms/index.ts b/dictation_client/src/features/terms/index.ts new file mode 100644 index 0000000..8692ec6 --- /dev/null +++ b/dictation_client/src/features/terms/index.ts @@ -0,0 +1,4 @@ +export * from "./termsSlice"; +export * from "./state"; +export * from "./operations"; +export * from "./selectors"; diff --git a/dictation_client/src/features/terms/operations.ts b/dictation_client/src/features/terms/operations.ts new file mode 100644 index 0000000..511dfe9 --- /dev/null +++ b/dictation_client/src/features/terms/operations.ts @@ -0,0 +1,158 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import type { RootState } from "app/store"; +import { ErrorObject, createErrorObject } from "common/errors"; +import { getTranslationID } from "translation"; +import { openSnackbar } from "features/ui/uiSlice"; +import { getIdTokenFromLocalStorage } from "common/token"; +import { TIERS } from "components/auth/constants"; +import { + UsersApi, + GetAccountInfoMinimalAccessResponse, + AccountsApi, + TermsApi, + GetTermsInfoResponse, +} from "../../api/api"; +import { Configuration } from "../../api/configuration"; + +export const getAccountInfoMinimalAccessAsync = createAsyncThunk< + GetAccountInfoMinimalAccessResponse, + { + localStorageKeyforIdToken: string; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accept/getAccountInfoMinimalAccessAsync", async (args, thunkApi) => { + const { localStorageKeyforIdToken } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + // IDトークンの取得 + const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken); + + // IDトークンが取得できない場合エラーとする + if (!idToken) { + throw new Error("Unable to retrieve the ID token."); + } + const res = await accountApi.getAccountInfoMinimalAccess( + { idToken }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + return res.data; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); + +export const getTermsInfoAsync = createAsyncThunk< + GetTermsInfoResponse, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accept/getTermsInfoAsync", async (_args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const termsApi = new TermsApi(config); + + try { + const termsInfo = await termsApi.getTermsInfo({ + headers: { authorization: `Bearer ${accessToken}` }, + }); + + return termsInfo.data; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); + +export const updateAcceptedVersionAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + tier: number; + localStorageKeyforIdToken: string; + updateAccceptVersions: { + acceptedVerDPA: string; + acceptedVerEULA: string; + }; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accept/UpdateAcceptedVersionAsync", async (args, thunkApi) => { + const { tier, localStorageKeyforIdToken, updateAccceptVersions } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const userApi = new UsersApi(config); + + try { + // IDトークンの取得 + const idToken = getIdTokenFromLocalStorage(localStorageKeyforIdToken); + + // IDトークンが取得できない場合エラーとする + if (!idToken) { + throw new Error("Unable to retrieve the ID token."); + } + await userApi.updateAcceptedVersion( + { + idToken, + acceptedEULAVersion: updateAccceptVersions.acceptedVerEULA, + acceptedDPAVersion: !(TIERS.TIER5 === tier.toString()) + ? updateAccceptVersions.acceptedVerDPA + : undefined, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/terms/selectors.ts b/dictation_client/src/features/terms/selectors.ts new file mode 100644 index 0000000..5cd00f7 --- /dev/null +++ b/dictation_client/src/features/terms/selectors.ts @@ -0,0 +1,20 @@ +import { RootState } from "app/store"; +import { TERMS_DOCUMENT_TYPE } from "features/terms/constants"; + +export const selectTermVersions = (state: RootState) => { + const { termsInfo } = state.terms.domain; + + const acceptedVerDPA = + termsInfo.find( + (termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.DPA + )?.version || ""; + + const acceptedVerEULA = + termsInfo.find( + (termInfo) => termInfo.documentType === TERMS_DOCUMENT_TYPE.EULA + )?.version || ""; + + return { acceptedVerDPA, acceptedVerEULA }; +}; + +export const selectTier = (state: RootState) => state.terms.domain.tier; diff --git a/dictation_client/src/features/terms/state.ts b/dictation_client/src/features/terms/state.ts new file mode 100644 index 0000000..0c724dc --- /dev/null +++ b/dictation_client/src/features/terms/state.ts @@ -0,0 +1,15 @@ +import { TermInfo } from "../../api/api"; + +export interface AcceptState { + domain: Domain; + apps: Apps; +} + +export interface Domain { + tier: number; + termsInfo: TermInfo[]; +} + +export interface Apps { + isLoading: boolean; +} diff --git a/dictation_client/src/features/terms/termsSlice.ts b/dictation_client/src/features/terms/termsSlice.ts new file mode 100644 index 0000000..7f88271 --- /dev/null +++ b/dictation_client/src/features/terms/termsSlice.ts @@ -0,0 +1,64 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { AcceptState } from "./state"; +import { + getAccountInfoMinimalAccessAsync, + getTermsInfoAsync, + updateAcceptedVersionAsync, +} from "./operations"; + +const initialState: AcceptState = { + domain: { + tier: 0, + termsInfo: [ + { + documentType: "", + version: "", + }, + ], + }, + apps: { + isLoading: false, + }, +}; + +export const termsSlice = createSlice({ + name: "terms", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(getAccountInfoMinimalAccessAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase( + getAccountInfoMinimalAccessAsync.fulfilled, + (state, actions) => { + state.apps.isLoading = false; + state.domain.tier = actions.payload.tier; + } + ); + builder.addCase(getAccountInfoMinimalAccessAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(getTermsInfoAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(getTermsInfoAsync.fulfilled, (state, actions) => { + state.apps.isLoading = false; + state.domain.termsInfo = actions.payload.termsInfo; + }); + builder.addCase(getTermsInfoAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateAcceptedVersionAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateAcceptedVersionAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateAcceptedVersionAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + }, +}); + +export default termsSlice.reducer; diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 6d64137..52ffbc4 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -87,7 +87,7 @@ export const userSlice = createSlice({ action: PayloadAction<{ authorId: string | undefined }> ) => { const { authorId } = action.payload; - state.apps.addUser.authorId = authorId; + state.apps.addUser.authorId = authorId?.toUpperCase(); }, changeAutoRenew: (state, action: PayloadAction<{ autoRenew: boolean }>) => { const { autoRenew } = action.payload; @@ -144,7 +144,7 @@ export const userSlice = createSlice({ state.apps.updateUser.name = user.name; state.apps.updateUser.email = user.email; state.apps.updateUser.role = user.role as RoleType; - state.apps.updateUser.authorId = user.authorId; + state.apps.updateUser.authorId = user.authorId?.toUpperCase(); state.apps.updateUser.encryption = user.encryption; state.apps.updateUser.encryptionPassword = undefined; state.apps.updateUser.prompt = user.prompt; @@ -156,7 +156,7 @@ export const userSlice = createSlice({ state.apps.selectedUser.name = user.name; state.apps.selectedUser.email = user.email; state.apps.selectedUser.role = user.role as RoleType; - state.apps.selectedUser.authorId = user.authorId; + state.apps.selectedUser.authorId = user.authorId?.toUpperCase(); state.apps.selectedUser.encryption = user.encryption; state.apps.selectedUser.encryptionPassword = undefined; state.apps.selectedUser.prompt = user.prompt; @@ -175,7 +175,7 @@ export const userSlice = createSlice({ action: PayloadAction<{ authorId: string }> ) => { const { authorId } = action.payload; - state.apps.updateUser.authorId = authorId; + state.apps.updateUser.authorId = authorId.toUpperCase(); }, changeUpdateEncryption: ( state, @@ -243,7 +243,8 @@ export const userSlice = createSlice({ state.apps.licenseAllocateUser.id = selectedUser.id; state.apps.licenseAllocateUser.name = selectedUser.name; state.apps.licenseAllocateUser.email = selectedUser.email; - state.apps.licenseAllocateUser.authorId = selectedUser.authorId; + state.apps.licenseAllocateUser.authorId = + selectedUser.authorId.toUpperCase(); state.apps.licenseAllocateUser.licenseStatus = selectedUser.licenseStatus; state.apps.licenseAllocateUser.expiration = selectedUser.expiration; state.apps.licenseAllocateUser.remaining = selectedUser.remaining; diff --git a/dictation_client/src/features/workflow/worktype/operations.ts b/dictation_client/src/features/workflow/worktype/operations.ts index 0e45d33..fa173c1 100644 --- a/dictation_client/src/features/workflow/worktype/operations.ts +++ b/dictation_client/src/features/workflow/worktype/operations.ts @@ -342,3 +342,75 @@ export const updateActiveWorktypeAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteWorktypeAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { worktypeId: number }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/deleteWorktypeAsync", async (args, thunkApi) => { + const { worktypeId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + + try { + await accountsApi.deleteWorktype(worktypeId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + if (error.statusCode === 400) { + if (error.code === "E011003") { + // ワークタイプが削除済みの場合は成功扱いとする + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + + if (error.code === "E011004") { + // ワークタイプがワークフローで使用中の場合は削除できない + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID( + "worktypeIdSetting.message.worktypeInUseError" + ), + }) + ); + return {}; + } + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/pages/AuthPage/index.tsx b/dictation_client/src/pages/AuthPage/index.tsx new file mode 100644 index 0000000..a1559bd --- /dev/null +++ b/dictation_client/src/pages/AuthPage/index.tsx @@ -0,0 +1,78 @@ +import { useMsal } from "@azure/msal-react"; +import { AuthError } from "@azure/msal-browser"; +import { AppDispatch } from "app/store"; +import Footer from "components/footer"; +import Header from "components/header"; +import { + selectLoginApiCallStatus, + changeLocalStorageKeyforIdToken, +} from "features/login"; +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; + +const AuthPage: React.FC = (): JSX.Element => { + const { instance } = useMsal(); + const dispatch: AppDispatch = useDispatch(); + const navigate = useNavigate(); + const status = useSelector(selectLoginApiCallStatus); + + // TODO 将来的にトークンの取得処理をoperations.ts側に移動させたい。useEffect内で非同期処理を行いたくない。 + useEffect(() => { + if (status !== "none") { + // ログイン処理で、何回か本画面が描画される契機があるが、認証処理は一度だけ実施すればよいため認証処理実行済みであれば何もしない + return; + } + + (async () => { + try { + const loginResult = await instance.handleRedirectPromise(); + + // eslint-disable-next-line + console.log({ loginResult }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除(eslint-disable含めて)する。 + + if (loginResult && loginResult.account) { + const { homeAccountId, idTokenClaims } = loginResult.account; + if (idTokenClaims && idTokenClaims.aud) { + const localStorageKeyforIdToken = `${homeAccountId}-${ + import.meta.env.VITE_B2C_KNOWNAUTHORITIES + }-idtoken-${idTokenClaims.aud}----`; + + // AADB2Cログイン画面以外から本画面に遷移した場合用にIDトークン取得用キーをstateに保存 + dispatch( + changeLocalStorageKeyforIdToken({ + localStorageKeyforIdToken, + }) + ); + + // トークン取得と設定を行う + navigate("/login"); + } + } + } catch (e) { + // eslint-disable-next-line + console.log({ e }); // TODO:loading画面から遷移できない事象の調査用ログ。事象解消後削除(eslint-disable含めて)する。 + + // AAD B2Cの多要素認証画面やパスワードリセット画面で「cancel」をクリックすると、handleRedirectPromise()にてエラーが発生するため、 + // それをハンドリングして適切な画面遷移処理を行う。 + if (e instanceof AuthError) { + // エラーコードはerrorMessageの中の一部として埋め込まれており完全一致で取得するのは筋が悪いため、部分一致で取得する。 + // TODO 他にもAADB2Cのエラーコードを使用する箇所が出てきた場合、定数化すること + if (e.errorMessage.startsWith("AADB2C90091")) { + navigate("/"); + } + } + } + })(); + }, [instance, navigate, status, dispatch]); + + return ( + <> +
+

loading ...

+