>(callback);
+ useEffect(() => {
+ callbackRef.current = callback;
+ }, [callback]);
+
+ useEffect(() => {
+ const updateToken = async () => {
+ await callbackRef.current();
+ };
+ const id = setInterval(updateToken, interval);
+ return () => {
+ clearInterval(id);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+};
diff --git a/dictation_client/src/components/auth/routeAuthGuard.tsx b/dictation_client/src/components/auth/routeAuthGuard.tsx
new file mode 100644
index 0000000..78813d5
--- /dev/null
+++ b/dictation_client/src/components/auth/routeAuthGuard.tsx
@@ -0,0 +1,41 @@
+import React, { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import {
+ isAuthenticatedSelector,
+ isTokenExpired,
+} from "features/auth/selectors";
+import { useDispatch, useSelector } from "react-redux";
+import { AppDispatch } from "app/store";
+import { clearToken } from "features/auth";
+import { useMsal } from "@azure/msal-react";
+
+type RouteAuthGuardProps = {
+ component: JSX.Element;
+};
+
+export const RouteAuthGuard = (props: RouteAuthGuardProps) => {
+ const isAuth = useSelector(isAuthenticatedSelector);
+ const isExpired = useSelector(isTokenExpired);
+ const dispatch: AppDispatch = useDispatch();
+ const navigate = useNavigate();
+ const { instance } = useMsal();
+ const { component } = props;
+ // トークンの有効期限が切れていたらTopへリダイレクト
+ useEffect(() => {
+ if (!isAuth) {
+ navigate("/");
+ } else if (isExpired) {
+ dispatch(clearToken());
+ // B2Cからもログアウトする
+ instance.logout({
+ postLogoutRedirectUri: "/",
+ });
+ }
+ }, [isAuth, isExpired, dispatch, navigate, instance]);
+
+ if (!isAuth || isExpired) {
+ return ;
+ }
+
+ return component;
+};
diff --git a/dictation_client/src/components/auth/updateTokenTimer.tsx b/dictation_client/src/components/auth/updateTokenTimer.tsx
new file mode 100644
index 0000000..fff1e1f
--- /dev/null
+++ b/dictation_client/src/components/auth/updateTokenTimer.tsx
@@ -0,0 +1,38 @@
+import React, { useCallback } from "react";
+import { AppDispatch } from "app/store";
+import { decodeToken } from "common/decodeToken";
+import { useInterval } from "common/useInterval";
+import { updateTokenAsync } from "features/auth/operations";
+import { loadAccessToken } from "features/auth/utils";
+import { DateTime } from "luxon";
+import { useDispatch } from "react-redux";
+//アクセストークンを更新する基準の秒数
+const TOKEN_UPDATE_TIME = 5 * 60;
+//アクセストークンの更新チェックを行う間隔(ミリ秒)
+const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000;
+
+export const UpdateTokenTimer = () => {
+ const dispatch: AppDispatch = useDispatch();
+
+ // 期限が5分以内であれば更新APIを呼ぶ
+ const updateToken = useCallback(async () => {
+ // localStorageからトークンを取得
+ const jwt = loadAccessToken();
+ // selectorに以下の判定処理を移したかったが、初期表示時の値でしか判定できないのでComponent内に置く
+ if (jwt) {
+ const token = decodeToken(jwt);
+ if (token) {
+ const { exp } = token;
+ const now = DateTime.local().toSeconds();
+ if (exp - now <= TOKEN_UPDATE_TIME) {
+ await dispatch(updateTokenAsync());
+ }
+ }
+ }
+ }, [dispatch]);
+
+ useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS);
+
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>>;
+};
diff --git a/dictation_client/src/features/auth/authSlice.ts b/dictation_client/src/features/auth/authSlice.ts
index b88c705..a35be42 100644
--- a/dictation_client/src/features/auth/authSlice.ts
+++ b/dictation_client/src/features/auth/authSlice.ts
@@ -26,7 +26,6 @@ export const authSlice = createSlice({
) => {
const { accessToken, refreshToken } = action.payload;
if (accessToken && refreshToken) {
- state.configuration.accessToken = accessToken;
state.accessToken = accessToken;
saveAccessToken(accessToken);
@@ -35,7 +34,6 @@ export const authSlice = createSlice({
}
},
clearToken: (state) => {
- state.configuration.accessToken = undefined;
state.accessToken = null;
state.refreshToken = null;
removeAccessToken();
diff --git a/dictation_client/src/features/auth/operations.ts b/dictation_client/src/features/auth/operations.ts
new file mode 100644
index 0000000..04c1288
--- /dev/null
+++ b/dictation_client/src/features/auth/operations.ts
@@ -0,0 +1,42 @@
+import { createAsyncThunk } from "@reduxjs/toolkit";
+import { RootState } from "app/store";
+import { AuthApi } from "../../api/api";
+import { Configuration } from "../../api/configuration";
+import { setToken } from "./authSlice";
+
+export const updateTokenAsync = createAsyncThunk<
+ {
+ /* Empty Object */
+ },
+ void,
+ {
+ // rejectした時の返却値の型
+ rejectValue: {
+ /* Empty Object */
+ };
+ }
+>("auth/updateTokenAsync", async (args, thunkApi) => {
+ // apiのConfigurationを取得する
+ const { getState } = thunkApi;
+ const state = getState() as RootState;
+ const { configuration, refreshToken } = state.auth;
+ const config = new Configuration(configuration);
+ const authApi = new AuthApi(config);
+
+ try {
+ const { data } = await authApi.accessToken({
+ headers: { authorization: `Bearer ${refreshToken}` },
+ });
+ // アクセストークン・リフレッシュトークンをlocalStorageに保存
+ thunkApi.dispatch(
+ setToken({
+ accessToken: data.accessToken,
+ refreshToken: state.auth.refreshToken,
+ })
+ );
+
+ return {};
+ } catch (e) {
+ return thunkApi.rejectWithValue({});
+ }
+});
diff --git a/dictation_client/src/features/auth/selectors.ts b/dictation_client/src/features/auth/selectors.ts
new file mode 100644
index 0000000..db5f6a9
--- /dev/null
+++ b/dictation_client/src/features/auth/selectors.ts
@@ -0,0 +1,24 @@
+import { RootState } from "app/store";
+import { decodeToken } from "common/decodeToken";
+import { DateTime } from "luxon";
+/**
+ * ログイン済みかどうかを判定する
+ * @param state RootState
+ */
+export const isAuthenticatedSelector = (state: RootState): boolean =>
+ state.auth.accessToken !== null;
+
+// トークンが有効期限内かどうかを判定する
+export const isTokenExpired = (state: RootState): boolean => {
+ if (state.auth.accessToken) {
+ const token = decodeToken(state.auth.accessToken);
+ if (token) {
+ const { exp } = token;
+ const now = DateTime.local().toSeconds();
+ if (now <= exp) {
+ return false;
+ }
+ }
+ }
+ return true;
+};
diff --git a/dictation_client/src/features/auth/utils.ts b/dictation_client/src/features/auth/utils.ts
index 8923183..8e0e16d 100644
--- a/dictation_client/src/features/auth/utils.ts
+++ b/dictation_client/src/features/auth/utils.ts
@@ -49,11 +49,5 @@ export const initialConfig = (): ConfigurationParameters => {
config.basePath = window.location.origin;
}
- // localStorageからAccessTokenを復元する
- const accessToken = loadAccessToken();
- if (accessToken) {
- config.accessToken = accessToken;
- }
-
return config;
};
diff --git a/dictation_client/src/features/login/index.ts b/dictation_client/src/features/login/index.ts
index fe55245..d3e1e79 100644
--- a/dictation_client/src/features/login/index.ts
+++ b/dictation_client/src/features/login/index.ts
@@ -1,5 +1,3 @@
export * from "./loginSlice";
export * from "./state";
-export * from "./selectors";
export * from "./operations";
-export * from "./types";
diff --git a/dictation_client/src/features/login/loginSlice.ts b/dictation_client/src/features/login/loginSlice.ts
index 2fea776..f665f2f 100644
--- a/dictation_client/src/features/login/loginSlice.ts
+++ b/dictation_client/src/features/login/loginSlice.ts
@@ -1,5 +1,4 @@
import { createSlice } from "@reduxjs/toolkit";
-import { loginAsync } from "./operations";
import { LoginState } from "./state";
const initialState: LoginState = {
@@ -12,16 +11,8 @@ export const loginSlice = createSlice({
name: "login",
initialState,
reducers: {},
- extraReducers: (builder) => {
- builder.addCase(loginAsync.pending, (state) => {
- state.apps.loadState = "Loading";
- });
- builder.addCase(loginAsync.fulfilled, (state) => {
- state.apps.loadState = "Loaded";
- });
- builder.addCase(loginAsync.rejected, (state) => {
- state.apps.loadState = "LoadError";
- });
+ extraReducers: () => {
+ //
},
});
diff --git a/dictation_client/src/features/login/operations.ts b/dictation_client/src/features/login/operations.ts
index a711f01..f5c2060 100644
--- a/dictation_client/src/features/login/operations.ts
+++ b/dictation_client/src/features/login/operations.ts
@@ -43,7 +43,7 @@ export const loginAsync = createAsyncThunk<
})
);
- return data;
+ return {};
} catch (e) {
return thunkApi.rejectWithValue({});
}
diff --git a/dictation_client/src/features/login/state.ts b/dictation_client/src/features/login/state.ts
index caed902..00862e6 100644
--- a/dictation_client/src/features/login/state.ts
+++ b/dictation_client/src/features/login/state.ts
@@ -1,9 +1,6 @@
-import { LoadState } from "./types";
-
export interface LoginState {
apps: Apps;
}
-export interface Apps {
- loadState: LoadState;
-}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface Apps {}
diff --git a/dictation_client/src/pages/ErrorPage/index.tsx b/dictation_client/src/pages/ErrorPage/index.tsx
index 4592ea0..e8fb978 100644
--- a/dictation_client/src/pages/ErrorPage/index.tsx
+++ b/dictation_client/src/pages/ErrorPage/index.tsx
@@ -1,7 +1,9 @@
+import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import React from "react";
export const AuthErrorPage = (): JSX.Element => (
diff --git a/dictation_client/src/pages/LoginPage/index.tsx b/dictation_client/src/pages/LoginPage/index.tsx
index 4eb5ed4..32ac481 100644
--- a/dictation_client/src/pages/LoginPage/index.tsx
+++ b/dictation_client/src/pages/LoginPage/index.tsx
@@ -1,16 +1,16 @@
import { InteractionStatus, SilentRequest } from "@azure/msal-browser";
-import { useMsal } from "@azure/msal-react";
+import { useIsAuthenticated, useMsal } from "@azure/msal-react";
import { AppDispatch } from "app/store";
import { loginAsync } from "features/login";
import React, { useCallback, useEffect } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
-const LoginPage: React.FC = () => {
+const LoginPage: React.FC = (): JSX.Element => {
const { accounts, instance, inProgress } = useMsal();
const dispatch: AppDispatch = useDispatch();
const navigate = useNavigate();
-
+ const isAuthenticated = useIsAuthenticated();
const login = useCallback(async () => {
const request: SilentRequest = {
scopes: ["openid"],
@@ -32,10 +32,18 @@ const LoginPage: React.FC = () => {
useEffect(() => {
// B2CからリダイレクトされてB2Cへのログインが完了してからAPIを呼ぶ
- if (inProgress === InteractionStatus.None) {
+ if (isAuthenticated && inProgress === InteractionStatus.None) {
login();
}
- }, [accounts, dispatch, inProgress, instance, login]);
+ }, [
+ accounts,
+ dispatch,
+ inProgress,
+ instance,
+ isAuthenticated,
+ login,
+ navigate,
+ ]);
return loading ...
;
};
diff --git a/dictation_client/src/pages/SamplePage/index.tsx b/dictation_client/src/pages/SamplePage/index.tsx
index f93f218..01407c5 100644
--- a/dictation_client/src/pages/SamplePage/index.tsx
+++ b/dictation_client/src/pages/SamplePage/index.tsx
@@ -1,14 +1,23 @@
import { useMsal } from "@azure/msal-react";
+import { AppDispatch } from "app/store";
+import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
+import { clearToken } from "features/auth";
import React from "react";
+import { useDispatch } from "react-redux";
-const SamplePage: React.FC = () => {
+const SamplePage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
+ const dispatch: AppDispatch = useDispatch();
return (
+
hello world!!
diff --git a/dictation_client/src/pages/TopPage/index.tsx b/dictation_client/src/pages/TopPage/index.tsx
index 40e0b80..a088937 100644
--- a/dictation_client/src/pages/TopPage/index.tsx
+++ b/dictation_client/src/pages/TopPage/index.tsx
@@ -1,8 +1,8 @@
import { useMsal } from "@azure/msal-react";
-import { loginRequest } from "common/auth/msalConfig";
+import { loginRequest } from "common/msalConfig";
import React from "react";
-const TopPage: React.FC = () => {
+const TopPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
return (