diff --git a/src/api/auth.js b/src/api/auth.js index 520ce88..ab5cb29 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -18,3 +18,8 @@ export const exchangeOidcCode = (code, redirectUri, codeVerifier) => redirect_uri: redirectUri, code_verifier: codeVerifier || undefined }) + +export const refreshOidcToken = (refreshToken) => + userHttp.post('/auth/refresh', { + refresh_token: refreshToken + }) diff --git a/src/api/http.js b/src/api/http.js index 12f5212..d5df82d 100644 --- a/src/api/http.js +++ b/src/api/http.js @@ -2,6 +2,37 @@ import axios from 'axios' import router from '@/router' const BASE_URL = import.meta.env.VITE_API_BASE_URL +let refreshPromise = null + +async function refreshAccessToken() { + if (refreshPromise) return refreshPromise + const refreshToken = localStorage.getItem('refresh_token') + if (!refreshToken) throw new Error('missing_refresh_token') + + refreshPromise = axios + .post(`${BASE_URL}/auth/refresh`, { refresh_token: refreshToken }) + .then((res) => { + const nextAccessToken = res.data?.access_token + const nextRefreshToken = res.data?.refresh_token || refreshToken + if (!nextAccessToken) { + throw new Error('missing_access_token') + } + localStorage.setItem('access_token', nextAccessToken) + localStorage.setItem('refresh_token', nextRefreshToken) + return nextAccessToken + }) + .finally(() => { + refreshPromise = null + }) + + return refreshPromise +} + +function hardLogoutToLogin() { + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + router.push('/login') +} // 使用者 API:帶 Bearer token export const userHttp = axios.create({ baseURL: BASE_URL }) @@ -16,10 +47,18 @@ userHttp.interceptors.request.use(config => { userHttp.interceptors.response.use( res => res, - err => { - if (err.response?.status === 401) { - localStorage.removeItem('access_token') - router.push('/login') + async err => { + const original = err.config || {} + if (err.response?.status === 401 && !original._retriedByRefresh) { + original._retriedByRefresh = true + try { + const nextToken = await refreshAccessToken() + original.headers = original.headers || {} + original.headers['Authorization'] = `Bearer ${nextToken}` + return userHttp.request(original) + } catch (_refreshErr) { + hardLogoutToLogin() + } } return Promise.reject(err) } @@ -38,10 +77,18 @@ adminHttp.interceptors.request.use(config => { adminHttp.interceptors.response.use( res => res, - err => { - if (err.response?.status === 401) { - localStorage.removeItem('access_token') - router.push('/login') + async err => { + const original = err.config || {} + if (err.response?.status === 401 && !original._retriedByRefresh) { + original._retriedByRefresh = true + try { + const nextToken = await refreshAccessToken() + original.headers = original.headers || {} + original.headers['Authorization'] = `Bearer ${nextToken}` + return adminHttp.request(original) + } catch (_refreshErr) { + hardLogoutToLogin() + } } return Promise.reject(err) } diff --git a/src/pages/AuthCallbackPage.vue b/src/pages/AuthCallbackPage.vue index 7a10c8b..4e17dbe 100644 --- a/src/pages/AuthCallbackPage.vue +++ b/src/pages/AuthCallbackPage.vue @@ -63,7 +63,7 @@ onMounted(async () => { const redirectUri = `${window.location.origin}/auth/callback` const res = await exchangeOidcCode(code, redirectUri, codeVerifier) - const { access_token } = res.data + const { access_token, refresh_token } = res.data if (!access_token) { error.value = '無法取得 access token' @@ -72,7 +72,7 @@ onMounted(async () => { } // 存 token 並取得使用者資料 - authStore.setToken(access_token) + authStore.setTokens(access_token, refresh_token || null) await authStore.fetchMe() // 導向原頁面或預設的 /me diff --git a/src/pages/admin/SystemsPage.vue b/src/pages/admin/SystemsPage.vue index ad7c8eb..fce0003 100644 --- a/src/pages/admin/SystemsPage.vue +++ b/src/pages/admin/SystemsPage.vue @@ -1,10 +1,19 @@