feat(sync): keycloak as source-of-truth with auto catalog sync and token refresh
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user