Merged PR 550: 画面実装(トークンを定期的に更新する仕組み)

## 概要
[Task2910: 画面実装(トークンを定期的に更新する仕組み)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2910)

- タイマーで定期的に代行操作用のアクセストークンを更新する処理を実装しました。

## レビューポイント
- 通常のアクセストークンのタイマー内で同じタイミングでチェックするように実装していますが分けたほうがいいなどありますでしょうか?

## UIの変更
- [Task2910](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task2910?csf=1&web=1&e=g0RdIf)

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-11-07 07:15:25 +00:00
parent ef4f22029b
commit 0212c61bbc
6 changed files with 109 additions and 12 deletions

View File

@ -1,5 +1,6 @@
// トークンの型やtypeGuardの関数を配置するファイル
export interface Token {
delegateUserId?: string;
userId: string;
role: string;
tier: number;

View File

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

View File

@ -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);
// 期限が分以内であれば更新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);

View File

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

View File

@ -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<
// 正常時の戻り値の型

View File

@ -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,