feat(FE) : implementasi access token & refresh token
This commit is contained in:
No files matched your search
@@ -102,6 +102,9 @@ export default defineNuxtConfig({
|
|||||||
wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.150.100:8084/api/v1/ws',
|
wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.150.100:8084/api/v1/ws',
|
||||||
baseUrl: process.env.BASE_URL || 'http://10.10.150.144:8080/api',
|
baseUrl: process.env.BASE_URL || 'http://10.10.150.144:8080/api',
|
||||||
baseUrlGomed: process.env.BASE_URL_GOMED || 'https://gomed.rssa.my.id/api',
|
baseUrlGomed: process.env.BASE_URL_GOMED || 'https://gomed.rssa.my.id/api',
|
||||||
|
keycloakUrl: process.env.KEYCLOAK_ISSUER ? `${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token` : 'https://auth.rssa.top/realms/sandbox/protocol/openid-connect/token',
|
||||||
|
keycloakClientId: process.env.KEYCLOAK_CLIENT_ID || 'akbar-test',
|
||||||
|
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET || 'FDyv3UYMgJOYPnvzXVVv6diRtcgEevKg',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// auth: {
|
// auth: {
|
||||||
|
|||||||
+127
-1
@@ -3,6 +3,11 @@ import axios from 'axios'
|
|||||||
// Get runtime config for Nuxt 3
|
// Get runtime config for Nuxt 3
|
||||||
const config = useRuntimeConfig()
|
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
|
// Create axios instance for local API
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: config.public.baseUrl,
|
baseURL: config.public.baseUrl,
|
||||||
@@ -15,6 +20,68 @@ const apiGomed = axios.create({
|
|||||||
timeout: 10000,
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Shared request interceptor
|
||||||
const requestInterceptor = (config: any) => {
|
const requestInterceptor = (config: any) => {
|
||||||
// Only access localStorage on client-side (avoid SSR errors)
|
// Only access localStorage on client-side (avoid SSR errors)
|
||||||
@@ -31,9 +98,68 @@ const requestInterceptor = (config: any) => {
|
|||||||
|
|
||||||
// Shared response interceptor
|
// Shared response interceptor
|
||||||
const responseInterceptor = (response: any) => response
|
const responseInterceptor = (response: any) => response
|
||||||
const responseErrorInterceptor = (error: any) => {
|
const responseErrorInterceptor = async (error: any) => {
|
||||||
|
const originalRequest = error.config
|
||||||
|
|
||||||
// centralized error
|
// centralized error
|
||||||
console.error('API Error:', error.response)
|
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)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user