Merged PR 80: 画面実装
## 概要 [Task1618: 画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1618) - login処理が成功した時にデスクトップアプリを起動するように実装 - デスクトップアプリを起動するURLは確認済み ## レビューポイント - デスクトップアプリを起動するタイミングは問題ないか - 実装を追加した場所は問題ないか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ## 補足 - useEffectの依存関係からloginを削除 - これでAPI呼び出しが複数回行われることは無くなったが再度調査が必要そう
This commit is contained in:
parent
728bd6dfeb
commit
16b7416de0
@ -25,3 +25,37 @@ export const isToken = (arg: any): arg is Token => {
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
interface IdToken {
|
||||
credentialType: string;
|
||||
homeAccountId: string;
|
||||
environment: string;
|
||||
clientId: string;
|
||||
secret: string;
|
||||
realm: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIdToken = (arg: any): arg is IdToken => {
|
||||
const idToken = arg as IdToken;
|
||||
if (idToken.credentialType === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (idToken.homeAccountId === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (idToken.environment === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (idToken.clientId === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (idToken.secret === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (idToken.realm === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./loginSlice";
|
||||
export * from "./state";
|
||||
export * from "./operations";
|
||||
export * from "./selectors";
|
||||
|
||||
@ -1,16 +1,27 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { LoginState } from "./state";
|
||||
import { loginAsync } from "./operations";
|
||||
|
||||
const initialState: LoginState = {
|
||||
apps: {},
|
||||
apps: {
|
||||
LoginApiCallStatus: "none",
|
||||
},
|
||||
};
|
||||
|
||||
export const loginSlice = createSlice({
|
||||
name: "login",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: () => {
|
||||
//
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(loginAsync.pending, (state) => {
|
||||
state.apps.LoginApiCallStatus = "pending";
|
||||
});
|
||||
builder.addCase(loginAsync.fulfilled, (state) => {
|
||||
state.apps.LoginApiCallStatus = "fulfilled";
|
||||
});
|
||||
builder.addCase(loginAsync.rejected, (state) => {
|
||||
state.apps.LoginApiCallStatus = "rejected";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { IPublicClientApplication, SilentRequest } from "@azure/msal-browser";
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import type { RootState } from "app/store";
|
||||
import { setToken } from "features/auth/authSlice";
|
||||
@ -7,11 +6,10 @@ import { Configuration } from "../../api/configuration";
|
||||
|
||||
export const loginAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
//
|
||||
},
|
||||
{
|
||||
instance: IPublicClientApplication;
|
||||
request: SilentRequest;
|
||||
idToken: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -20,7 +18,7 @@ export const loginAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("login/loginAsync", async (args, thunkApi) => {
|
||||
const { instance, request } = args;
|
||||
const { idToken } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
@ -29,10 +27,8 @@ export const loginAsync = createAsyncThunk<
|
||||
const authApi = new AuthApi(config);
|
||||
|
||||
try {
|
||||
// B2CからIDトークンを取得
|
||||
const b2cToken = await instance.acquireTokenSilent(request);
|
||||
const { data } = await authApi.token({
|
||||
idToken: b2cToken.idToken,
|
||||
idToken,
|
||||
type: "web",
|
||||
});
|
||||
// アクセストークン・リフレッシュトークンをlocalStorageに保存
|
||||
|
||||
6
dictation_client/src/features/login/selectors.ts
Normal file
6
dictation_client/src/features/login/selectors.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { RootState } from "app/store";
|
||||
|
||||
export const selectLoginApiCallStatus = (
|
||||
state: RootState
|
||||
): "fulfilled" | "rejected" | "none" | "pending" =>
|
||||
state.login.apps.LoginApiCallStatus;
|
||||
@ -2,5 +2,6 @@ export interface LoginState {
|
||||
apps: Apps;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Apps {}
|
||||
export interface Apps {
|
||||
LoginApiCallStatus: "fulfilled" | "rejected" | "none" | "pending";
|
||||
}
|
||||
|
||||
@ -1,52 +1,78 @@
|
||||
import { InteractionStatus, SilentRequest } from "@azure/msal-browser";
|
||||
import { useIsAuthenticated, useMsal } from "@azure/msal-react";
|
||||
import { useMsal } from "@azure/msal-react";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { isIdToken } from "common/token";
|
||||
import Footer from "components/footer";
|
||||
import Header from "components/header";
|
||||
import { loginAsync } from "features/login";
|
||||
import React, { useCallback, useLayoutEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { loadAccessToken, loadRefreshToken } from "features/auth/utils";
|
||||
import { loginAsync, selectLoginApiCallStatus } from "features/login";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const LoginPage: React.FC = (): JSX.Element => {
|
||||
const { accounts, instance, inProgress } = useMsal();
|
||||
/* XXX B2CのログインからIDトークンの取得までの挙動を整理する必要がある。
|
||||
「プロダクト バックログ項目 1655: ログイン周りの挙動について調査・整理する」で調査・整理する。
|
||||
*/
|
||||
const { accounts, instance } = useMsal();
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const login = useCallback(async () => {
|
||||
const request: SilentRequest = {
|
||||
scopes: ["openid"],
|
||||
account: accounts[0],
|
||||
};
|
||||
// ログイン処理呼び出し
|
||||
const { meta } = await dispatch(loginAsync({ instance, request }));
|
||||
const [, i18n] = useTranslation();
|
||||
const status = useSelector(selectLoginApiCallStatus);
|
||||
|
||||
// ログイン失敗した場合、B2Cをログアウトしてからエラーページに遷移する
|
||||
if (meta.requestStatus === "rejected") {
|
||||
instance.logout({
|
||||
postLogoutRedirectUri: "/AuthError",
|
||||
});
|
||||
}
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
navigate("/xxx");
|
||||
}
|
||||
}, [accounts, dispatch, instance, navigate]);
|
||||
const login = useCallback(
|
||||
async (idToken: string) => {
|
||||
// ログイン処理呼び出し
|
||||
const { meta } = await dispatch(loginAsync({ idToken }));
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// B2CからリダイレクトされてB2Cへのログインが完了してからAPIを呼ぶ
|
||||
if (isAuthenticated && inProgress === InteractionStatus.None) {
|
||||
login();
|
||||
}
|
||||
}, [
|
||||
accounts,
|
||||
dispatch,
|
||||
inProgress,
|
||||
instance,
|
||||
isAuthenticated,
|
||||
login,
|
||||
navigate,
|
||||
]);
|
||||
// ログイン失敗した場合、B2Cをログアウトしてからエラーページに遷移する
|
||||
if (meta.requestStatus === "rejected") {
|
||||
instance.logout({
|
||||
postLogoutRedirectUri: "/AuthError",
|
||||
});
|
||||
}
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
const accessToken = loadAccessToken();
|
||||
const refreshToken = loadRefreshToken();
|
||||
/* TODO デスクトップアプリが無いためメモ帳を開くようにしている
|
||||
デスクトップアプリチームでスキーム名が決まり次第修正する
|
||||
*/
|
||||
const url = `note:login?accessToken=${accessToken}&refreshToken=${refreshToken}&language=${i18n.language}`; // カスタムURLスキーム
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
navigate("/xxx");
|
||||
}
|
||||
},
|
||||
[dispatch, i18n.language, instance, navigate]
|
||||
);
|
||||
|
||||
// TODO 将来的にトークンの取得処理をoperations.ts側に移動させたい。useEffect内で非同期処理を行いたくない。
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (accounts.length >= 1) {
|
||||
const { homeAccountId, idTokenClaims } = accounts[0];
|
||||
if (idTokenClaims) {
|
||||
if (idTokenClaims.aud) {
|
||||
// IDトークンの取得
|
||||
const idTokenString = localStorage.getItem(
|
||||
`${homeAccountId}-${
|
||||
import.meta.env.VITE_B2C_KNOWNAUTHORITIES
|
||||
}-idtoken-${idTokenClaims.aud}----`
|
||||
);
|
||||
if (idTokenString && status === "none") {
|
||||
const idTokenObject = JSON.parse(idTokenString);
|
||||
if (isIdToken(idTokenObject)) {
|
||||
await login(idTokenObject.secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [accounts, login, status]);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user