Files
antrean-operasi/services/api.ts
2026-02-24 14:33:16 +07:00

207 lines
6.2 KiB
TypeScript

import axios from 'axios'
// Get runtime config for Nuxt 3
const config = useRuntimeConfig()
// Keycloak configuration for token refresh
const KEYCLOAK_TOKEN_URL = (config.public.keycloakUrl as string) || 'https://auth.rssa.top/realms/sandbox/protocol/openid-connect/token'
const KEYCLOAK_CLIENT_ID = (config.public.keycloakClientId as string) || 'akbar-test'
const KEYCLOAK_CLIENT_SECRET = (config.public.keycloakClientSecret as string) || 'FDyv3UYMgJOYPnvzXVVv6diRtcgEevKg'
// Create axios instance for local API
const api = axios.create({
baseURL: config.public.baseUrl,
timeout: 10000,
})
// Create axios instance for GoMed API
const apiGomed = axios.create({
baseURL: config.public.baseUrlGomed,
timeout: 10000,
})
// 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 params = new URLSearchParams()
params.append('grant_type', 'refresh_token')
params.append('client_id', KEYCLOAK_CLIENT_ID)
params.append('client_secret', KEYCLOAK_CLIENT_SECRET)
params.append('refresh_token', refreshToken)
const response = await fetch(KEYCLOAK_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString()
})
if (!response.ok) {
throw new Error('Token refresh failed')
}
const data = await response.json()
// Update tokens in localStorage
if (data.access_token) {
localStorage.setItem('accessToken', data.access_token)
}
if (data.id_token) {
localStorage.setItem('idToken', data.id_token)
}
if (data.refresh_token) {
localStorage.setItem('refreshToken', data.refresh_token)
}
// let refreshTokenPayload;
// try {
// refreshTokenPayload = JSON.parse(
// Buffer.from(data.refresh_token.split('.')[1], 'base64').toString()
// );
// } catch (decodeError) {
// console.error('❌ Failed to decode refresh token:', decodeError);
// throw new Error('Invalid refresh token format');
// }
// const expiresAt = refreshTokenPayload.exp * 1000
// Update server-side session with new tokens
try {
await fetch('/api/auth/session', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
accessToken: data.access_token,
idToken: data.id_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.id_token || 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 idToken = localStorage.getItem('idToken')
if (idToken) {
config.headers.Authorization = `Bearer ${idToken}`
}
}
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('idToken')
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)
}
// Apply interceptors to both instances
api.interceptors.request.use(requestInterceptor)
api.interceptors.response.use(responseInterceptor, responseErrorInterceptor)
apiGomed.interceptors.request.use(requestInterceptor)
apiGomed.interceptors.response.use(responseInterceptor, responseErrorInterceptor)
export default api
export { apiGomed }