diff --git a/dictation_client/src/common/token.ts b/dictation_client/src/common/token.ts index c41442f..ac7c3e9 100644 --- a/dictation_client/src/common/token.ts +++ b/dictation_client/src/common/token.ts @@ -1,5 +1,6 @@ // トークンの型やtypeGuardの関数を配置するファイル export interface Token { + delegateUserId?: string; userId: string; role: string; tier: number; diff --git a/dictation_client/src/components/auth/constants.ts b/dictation_client/src/components/auth/constants.ts index ee22851..34f51b0 100644 --- a/dictation_client/src/components/auth/constants.ts +++ b/dictation_client/src/components/auth/constants.ts @@ -33,4 +33,20 @@ export const TIERS = { * 401エラー時にログアウトさせずに処理を継続するエラーコード * @const {string[]} */ -export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = ["E010209"]; +export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [ + "E010209", + "E010503", + "E10501", +]; + +/** + * アクセストークンを更新する基準の秒数 + * @const {number} + */ +export const TOKEN_UPDATE_TIME = 5 * 60; + +/** + * アクセストークンの更新チェックを行う間隔(ミリ秒) + * @const {number} + */ +export const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000; diff --git a/dictation_client/src/components/auth/updateTokenTimer.tsx b/dictation_client/src/components/auth/updateTokenTimer.tsx index 27fa972..b615ac6 100644 --- a/dictation_client/src/components/auth/updateTokenTimer.tsx +++ b/dictation_client/src/components/auth/updateTokenTimer.tsx @@ -2,33 +2,56 @@ import React, { useCallback } from "react"; import { AppDispatch } from "app/store"; import { decodeToken } from "common/decodeToken"; import { useInterval } from "common/useInterval"; -import { updateTokenAsync, loadAccessToken } from "features/auth"; +import { + updateTokenAsync, + loadAccessToken, + updateDelegationTokenAsync, +} from "features/auth"; import { DateTime } from "luxon"; -import { useDispatch } from "react-redux"; -// アクセストークンを更新する基準の秒数 -const TOKEN_UPDATE_TIME = 5 * 60; -// アクセストークンの更新チェックを行う間隔(ミリ秒) -const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000; +import { useDispatch, useSelector } from "react-redux"; +import { selectDelegationAccessToken } from "features/auth/selectors"; +import { useNavigate } from "react-router-dom"; +import { cleanupDelegateAccount } from "features/partner"; +import { TOKEN_UPDATE_INTERVAL_MS, TOKEN_UPDATE_TIME } from "./constants"; export const UpdateTokenTimer = () => { const dispatch: AppDispatch = useDispatch(); + const navigate = useNavigate(); + + const delegattionToken = useSelector(selectDelegationAccessToken); // 期限が5分以内であれば更新APIを呼ぶ const updateToken = useCallback(async () => { // localStorageからトークンを取得 const jwt = loadAccessToken(); + // 現在時刻を取得 + const now = DateTime.local().toSeconds(); // 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]); + + // 代行操作トークン更新処理 + if (delegattionToken) { + const token = decodeToken(delegattionToken); + if (token) { + const { exp } = token; + if (exp - now <= TOKEN_UPDATE_TIME) { + const { meta } = await dispatch(updateDelegationTokenAsync()); + if (meta.requestStatus === "rejected") { + dispatch(cleanupDelegateAccount()); + navigate("/partners"); + } + } + } + } + }, [dispatch, delegattionToken, navigate]); useInterval(updateToken, TOKEN_UPDATE_INTERVAL_MS); diff --git a/dictation_client/src/features/auth/authSlice.ts b/dictation_client/src/features/auth/authSlice.ts index bf98cdf..66d5e13 100644 --- a/dictation_client/src/features/auth/authSlice.ts +++ b/dictation_client/src/features/auth/authSlice.ts @@ -9,7 +9,11 @@ import { removeRefreshToken, } from "./utils"; import type { AuthState } from "./state"; -import { getDelegationTokenAsync, updateTokenAsync } from "./operations"; +import { + getDelegationTokenAsync, + updateDelegationTokenAsync, + updateTokenAsync, +} from "./operations"; const initialState: AuthState = { configuration: initialConfig(), @@ -61,6 +65,14 @@ export const authSlice = createSlice({ state.delegationAccessToken = accessToken; state.delegationRefreshToken = refreshToken; }); + builder.addCase(updateDelegationTokenAsync.fulfilled, (state, action) => { + const { accessToken } = action.payload; + state.delegationAccessToken = accessToken; + }); + builder.addCase(updateDelegationTokenAsync.rejected, (state) => { + state.delegationAccessToken = null; + state.delegationRefreshToken = null; + }); }, }); diff --git a/dictation_client/src/features/auth/operations.ts b/dictation_client/src/features/auth/operations.ts index f565742..f1a87c5 100644 --- a/dictation_client/src/features/auth/operations.ts +++ b/dictation_client/src/features/auth/operations.ts @@ -6,10 +6,11 @@ import { ErrorObject, createErrorObject } from "common/errors"; import { AccessTokenResponse, AuthApi, + DelegationAccessTokenResponse, DelegationTokenResponse, } from "../../api/api"; import { Configuration } from "../../api/configuration"; -import { getAccessToken, loadRefreshToken } from "./utils"; +import { getAccessToken, getRefreshToken, loadRefreshToken } from "./utils"; export const updateTokenAsync = createAsyncThunk< AccessTokenResponse, @@ -40,6 +41,50 @@ export const updateTokenAsync = createAsyncThunk< } }); +export const updateDelegationTokenAsync = createAsyncThunk< + DelegationAccessTokenResponse, + void, + { + // rejectした時の返却値の型 + rejectValue: { + /* Empty Object */ + }; + } +>("auth/updateDelegationTokenAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const refreshToken = getRefreshToken(state.auth); + const config = new Configuration(configuration); + const authApi = new AuthApi(config); + + try { + const { data } = await authApi.delegationAccessToken({ + headers: { authorization: `Bearer ${refreshToken}` }, + }); + + return data; + } catch (e) { + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + if (error.code === "E010503") { + errorMessage = getTranslationID( + "partnerPage.message.delegateCancelError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + return thunkApi.rejectWithValue({}); + } +}); + // パートナーのアカウントを代理操作するトークンを取得する export const getDelegationTokenAsync = createAsyncThunk< // 正常時の戻り値の型 diff --git a/dictation_client/src/features/login/operations.ts b/dictation_client/src/features/login/operations.ts index 0ee702c..07185ba 100644 --- a/dictation_client/src/features/login/operations.ts +++ b/dictation_client/src/features/login/operations.ts @@ -1,6 +1,6 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import type { RootState } from "app/store"; -import { setToken, getAccessToken } from "features/auth"; +import { getAccessToken, setToken } from "features/auth"; import { AuthApi, UsersApi,