From 45350d0ab84dd100df54b5f74d25d5b469013416 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 16 Oct 2023 02:14:22 +0000 Subject: [PATCH 01/16] =?UTF-8?q?Merged=20PR=20485:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2610: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2610) - WorkTypeID削除API IFを実装し、OpenAPI定義を更新しました。 ## レビューポイント - パスは適切か - レスポンスは想定通りか ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/api/odms/openapi.json | 54 +++++++++++++++++++ .../features/accounts/accounts.controller.ts | 43 +++++++++++++++ .../src/features/accounts/types/types.ts | 10 ++++ 3 files changed, 107 insertions(+) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 0fd0b8c..3887c30 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -990,6 +990,59 @@ "security": [{ "bearer": [] }] } }, + "/accounts/worktypes/{id}/delete": { + "post": { + "operationId": "deleteWorktype", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteWorktypeResponse" + } + } + } + }, + "400": { + "description": "指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/accounts/worktypes/{id}/option-items": { "get": { "operationId": "getOptionItems", @@ -3706,6 +3759,7 @@ "required": ["worktypeId"] }, "UpdateWorktypeResponse": { "type": "object", "properties": {} }, + "DeleteWorktypeResponse": { "type": "object", "properties": {} }, "GetWorktypeOptionItem": { "type": "object", "properties": { diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index bb98487..dc33c56 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -66,6 +66,8 @@ import { GetAuthorsResponse, GetAccountInfoMinimalAccessRequest, GetAccountInfoMinimalAccessResponse, + DeleteWorktypeRequestParam, + DeleteWorktypeResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -828,6 +830,47 @@ export class AccountsController { return {}; } + @Post('/worktypes/:id/delete') + @ApiResponse({ + status: HttpStatus.OK, + type: DeleteWorktypeResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'deleteWorktype' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + async deleteWorktype( + @Req() req: Request, + @Param() param: DeleteWorktypeRequestParam, + ): Promise { + const { id } = param; + const token = retrieveAuthorizationToken(req); + const { userId } = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(userId); + + console.log(context.trackingId); + console.log(`worktypeId: ${id}`); + + return {}; + } + @Get('/worktypes/:id/option-items') @ApiResponse({ status: HttpStatus.OK, diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 47f6536..b3ebbb5 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -497,6 +497,16 @@ export class UpdateWorktypeRequestParam { id: number; } +export class DeleteWorktypeRequestParam { + @ApiProperty({ description: 'Worktypeの内部ID' }) + @Type(() => Number) + @IsInt() + @Min(0) + id: number; +} + +export class DeleteWorktypeResponse {} + export class PostActiveWorktypeRequest { @ApiProperty({ required: false, From 897bad289be4a211cfa68a2cb176fb995df12a01 Mon Sep 17 00:00:00 2001 From: masaaki Date: Mon, 16 Oct 2023 06:52:08 +0000 Subject: [PATCH 02/16] =?UTF-8?q?Merged=20PR=20480:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=EF=BC=88=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2801: 画面修正(ログイン画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2801) - 以下の修正を実施しました - ログイン画面について、未同意バージョンがある場合、利用規約同意画面に遷移する処理を実装 - 利用規約同意画面(ADB2C以外の画面)からログイン画面に遷移した際も処理継続できるよう対応を実施 - このPull Requestでの対象/対象外 - AcceptToUsePageについては、遷移確認用のダミーページなので対象外でお願いします。 - 影響範囲(他の機能にも影響があるか) - ありません。 ## レビューポイント - 特にレビューしてほしい箇所 1. 既存のLoginPageを以下のように分割しています。 実装内容のイメージあっているか確認お願いします。 - LoginPage→AADB2Cからのリダイレクトを元にLocalStorageアクセス用のキーを生成 - TokenSettingPage→LocalStorageアクセス用のキーを使用してidTokenを取得し各種token生成を実施 1. TokenSettingPage/index.tsxにて、型ガード(isErrorObject)を作成し使用しています。 使い方やガードの実装が妥当か確認お願いします。 ## UIの変更 - 無し ## 動作確認状況 - ローカルで確認を実施 ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/AppRouter.tsx | 4 + dictation_client/src/common/errors/code.ts | 1 + dictation_client/src/common/errors/utils.ts | 18 +++++ dictation_client/src/common/msalConfig.ts | 2 +- .../src/features/login/loginSlice.ts | 14 +++- .../src/features/login/operations.ts | 7 +- .../src/features/login/selectors.ts | 4 + dictation_client/src/features/login/state.ts | 1 + .../src/pages/AcceptToUsePage/index.tsx | 25 ++++++ dictation_client/src/pages/AuthPage/index.tsx | 78 +++++++++++++++++++ .../src/pages/LoginPage/index.tsx | 74 +++++++----------- 11 files changed, 177 insertions(+), 51 deletions(-) create mode 100644 dictation_client/src/pages/AcceptToUsePage/index.tsx create mode 100644 dictation_client/src/pages/AuthPage/index.tsx diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index cf6e1d3..8ac2ccc 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/AcceptToUsePage"; import { TemplateFilePage } from "pages/TemplateFilePage"; import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess"; const AppRouter: React.FC = () => ( } /> + } /> } /> } /> } /> + } /> } /> } /> } /> diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 8c3236f..1867e14 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重複エラー 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/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/pages/AcceptToUsePage/index.tsx b/dictation_client/src/pages/AcceptToUsePage/index.tsx new file mode 100644 index 0000000..a6e5d0e --- /dev/null +++ b/dictation_client/src/pages/AcceptToUsePage/index.tsx @@ -0,0 +1,25 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +import styles from "styles/app.module.scss"; +import { useNavigate } from "react-router-dom"; + +const AcceptToUsePage: React.FC = (): JSX.Element => { + // 遷移確認用のダミーページ + + const navigate = useNavigate(); + + const navigateToLoginPage = () => { + navigate("/login"); + }; + + return ( +
+ 利用規約同意画面のダミー画面 +
+ {/* eslint-disable-next-line */} + +
+
+ ); +}; + +export default AcceptToUsePage; 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 ...

+