feat(sync): keycloak as source-of-truth with auto catalog sync and token refresh

This commit is contained in:
Chris
2026-04-03 00:46:46 +08:00
parent 7986160d9e
commit 7660c662a5
18 changed files with 773 additions and 174 deletions

View File

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