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:
saito.k 2023-04-25 10:18:00 +00:00
parent 728bd6dfeb
commit 16b7416de0
7 changed files with 126 additions and 51 deletions

View File

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

View File

@ -1,3 +1,4 @@
export * from "./loginSlice";
export * from "./state";
export * from "./operations";
export * from "./selectors";

View File

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

View File

@ -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に保存

View File

@ -0,0 +1,6 @@
import { RootState } from "app/store";
export const selectLoginApiCallStatus = (
state: RootState
): "fulfilled" | "rejected" | "none" | "pending" =>
state.login.apps.LoginApiCallStatus;

View File

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

View File

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