feat : middleware and refresh token
This commit is contained in:
+153
-2
@@ -3,9 +3,160 @@ import axios from 'axios'
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: config.public.baseUrl,
|
||||
timeout: 10000,
|
||||
baseURL: config.public.baseUrl
|
||||
})
|
||||
|
||||
// Flag to prevent multiple simultaneous refresh attempts
|
||||
let isRefreshing = false
|
||||
let failedQueue: any[] = []
|
||||
|
||||
const processQueue = (error: any, token: string | null = null) => {
|
||||
failedQueue.forEach(prom => {
|
||||
if (error) {
|
||||
prom.reject(error)
|
||||
} else {
|
||||
prom.resolve(token)
|
||||
}
|
||||
})
|
||||
|
||||
failedQueue = []
|
||||
}
|
||||
|
||||
const refreshAccessToken = async (): Promise<string | null> => {
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refreshToken')
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available')
|
||||
}
|
||||
|
||||
const response = await fetch(config.public.baseUrl+"/api/v1/auth/refresh", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
refresh_token: refreshToken,
|
||||
provider: 'keycloak',
|
||||
}),
|
||||
})
|
||||
|
||||
const responseBody = await response.json()
|
||||
|
||||
const data = responseBody.data
|
||||
|
||||
// Update tokens in localStorage
|
||||
if (data.access_token) {
|
||||
localStorage.setItem('accessToken', data.access_token)
|
||||
}
|
||||
if (data.refresh_token) {
|
||||
localStorage.setItem('refreshToken', data.refresh_token)
|
||||
}
|
||||
|
||||
// Update server-side session with new tokens
|
||||
try {
|
||||
await fetch('/api/auth/sessionUserStore', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
// expiresAt: expiresAt
|
||||
}),
|
||||
})
|
||||
console.log('✅ Server session updated with new tokens')
|
||||
} catch (sessionError) {
|
||||
console.error('⚠️ Failed to update server session:', sessionError)
|
||||
// Don't throw error here - token refresh was successful, session update is secondary
|
||||
}
|
||||
|
||||
console.log('✅ Token refreshed successfully')
|
||||
return data.access_token
|
||||
} catch (error) {
|
||||
console.error('❌ Token refresh failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Shared request interceptor
|
||||
const requestInterceptor = (config: any) => {
|
||||
// Only access localStorage on client-side (avoid SSR errors)
|
||||
if (process.client) {
|
||||
const accessToken = localStorage.getItem('accessToken')
|
||||
|
||||
if (accessToken) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Shared response interceptor
|
||||
const responseInterceptor = (response: any) => response
|
||||
const responseErrorInterceptor = async (error: any) => {
|
||||
const originalRequest = error.config
|
||||
|
||||
// centralized error
|
||||
console.error('API Error:', error.response)
|
||||
|
||||
// Handle token expiration on client-side
|
||||
if (process.client && error.response) {
|
||||
const { status } = error.response
|
||||
|
||||
// Check if response is 401 (token expired or unauthorized)
|
||||
if (status === 401 && !originalRequest._retry) {
|
||||
if (isRefreshing) {
|
||||
// If already refreshing, queue this request
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject })
|
||||
})
|
||||
.then(token => {
|
||||
originalRequest.headers['Authorization'] = 'Bearer ' + token
|
||||
return axios(originalRequest)
|
||||
})
|
||||
.catch(err => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
isRefreshing = true
|
||||
|
||||
// Try to refresh the token
|
||||
const newToken = await refreshAccessToken()
|
||||
|
||||
if (newToken) {
|
||||
// Token refresh successful, retry original request
|
||||
isRefreshing = false
|
||||
processQueue(null, newToken)
|
||||
|
||||
originalRequest.headers['Authorization'] = 'Bearer ' + newToken
|
||||
return axios(originalRequest)
|
||||
} else {
|
||||
// Token refresh failed (refresh token expired or error), logout user
|
||||
isRefreshing = false
|
||||
processQueue(new Error('Token refresh failed'), null)
|
||||
|
||||
// Clear expired tokens
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
|
||||
// Call logout endpoint to clear session
|
||||
fetch('/api/auth/logout', { method: 'POST' })
|
||||
.catch(err => console.error('Logout failed:', err))
|
||||
|
||||
// Redirect to login with error message
|
||||
const errorMessage = encodeURIComponent('Token is expired')
|
||||
window.location.href = `/auth/login?error=${errorMessage}`
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
api.interceptors.request.use(requestInterceptor)
|
||||
api.interceptors.response.use(responseInterceptor, responseErrorInterceptor)
|
||||
|
||||
export default api
|
||||
Reference in New Issue
Block a user