260 lines
No EOL
6 KiB
JavaScript
260 lines
No EOL
6 KiB
JavaScript
import { ref, computed } from 'vue'
|
|
|
|
const userToken = ref(null)
|
|
const refreshToken = ref(null)
|
|
const userProfile = ref(null)
|
|
const userRoles = ref([])
|
|
const isAuthenticated = ref(false)
|
|
const isInitialized = ref(false)
|
|
|
|
let sessionPollTimer = null
|
|
|
|
const extractRolesFromToken = (token) => {
|
|
if (!token) return []
|
|
try {
|
|
const payloadBase64 = token.split('.')[1]
|
|
const base64 = payloadBase64.replace(/-/g, '+').replace(/_/g, '/')
|
|
const jsonPayload = decodeURIComponent(
|
|
atob(base64)
|
|
.split('')
|
|
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
.join('')
|
|
)
|
|
const payload = JSON.parse(jsonPayload)
|
|
|
|
const realmRoles = payload?.realm_access?.roles || []
|
|
const clientRoles = Object.values(payload?.resource_access || {})
|
|
.flatMap(client => client.roles || [])
|
|
const directRoles = payload?.roles || []
|
|
|
|
return [...realmRoles, ...clientRoles, ...directRoles].map(r => String(r).toLowerCase())
|
|
} catch (e) {
|
|
console.warn('Failed to parse roles from token:', e)
|
|
return []
|
|
}
|
|
}
|
|
|
|
const getStoredRefreshToken = () => {
|
|
if (refreshToken.value) {
|
|
return refreshToken.value
|
|
}
|
|
|
|
if (import.meta.client) {
|
|
return localStorage.getItem('keycloak_refresh_token')
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export const useAuth = () => {
|
|
const fetchUserProfile = async () => {
|
|
if (!userToken.value) return
|
|
try {
|
|
const profile = await $fetch('/api/user/profile', {
|
|
headers: {
|
|
Authorization: `Bearer ${userToken.value}`
|
|
}
|
|
})
|
|
userProfile.value = profile
|
|
} catch (err) {
|
|
console.error('Failed to fetch user profile:', err)
|
|
}
|
|
}
|
|
|
|
const setSession = (data) => {
|
|
userToken.value = data.access_token
|
|
userRoles.value = extractRolesFromToken(data.access_token)
|
|
|
|
if (data.refresh_token) {
|
|
refreshToken.value = data.refresh_token
|
|
if (import.meta.client) {
|
|
localStorage.setItem('keycloak_refresh_token', data.refresh_token)
|
|
}
|
|
}
|
|
isAuthenticated.value = true
|
|
}
|
|
|
|
const clearSession = () => {
|
|
stopLiveSessionPolling()
|
|
userToken.value = null
|
|
refreshToken.value = null
|
|
userProfile.value = null
|
|
userRoles.value = []
|
|
isAuthenticated.value = false
|
|
if (import.meta.client) {
|
|
localStorage.removeItem('keycloak_refresh_token')
|
|
}
|
|
}
|
|
|
|
const exchangeCodeForToken = async (code) => {
|
|
try {
|
|
const data = await $fetch('/api/auth/token', {
|
|
method: 'POST',
|
|
body: {
|
|
code,
|
|
redirectUri: `${window.location.origin}/oauth`
|
|
}
|
|
})
|
|
|
|
setSession(data)
|
|
window.history.replaceState({}, document.title, window.location.pathname)
|
|
|
|
await fetchUserProfile()
|
|
startLiveSessionPolling()
|
|
|
|
return { success: true, isFirstLogin: data.isFirstLogin }
|
|
} catch (error) {
|
|
console.error('Code exchange failed:', error)
|
|
clearSession()
|
|
return { success: false, isFirstLogin: false }
|
|
} finally {
|
|
isInitialized.value = true
|
|
}
|
|
}
|
|
|
|
const silentRefresh = async () => {
|
|
const storedRefreshToken = getStoredRefreshToken()
|
|
|
|
if (!storedRefreshToken) {
|
|
clearSession()
|
|
isInitialized.value = true
|
|
return false
|
|
}
|
|
|
|
try {
|
|
const data = await $fetch('/api/auth/refresh', {
|
|
method: 'POST',
|
|
body: { refreshToken: storedRefreshToken }
|
|
})
|
|
|
|
setSession(data)
|
|
await fetchUserProfile()
|
|
startLiveSessionPolling()
|
|
return true
|
|
} catch (error) {
|
|
console.warn('Silent refresh failed:', error)
|
|
clearSession()
|
|
return false
|
|
} finally {
|
|
isInitialized.value = true
|
|
}
|
|
}
|
|
|
|
const verifySession = async () => {
|
|
if (!userToken.value) {
|
|
return await silentRefresh()
|
|
}
|
|
|
|
try {
|
|
const res = await $fetch('/api/auth/check-session', {
|
|
method: 'POST',
|
|
body: { token: userToken.value }
|
|
})
|
|
|
|
if (res.active) {
|
|
isAuthenticated.value = true
|
|
|
|
if (!userProfile.value) {
|
|
await fetchUserProfile()
|
|
}
|
|
|
|
return true
|
|
} else {
|
|
return await silentRefresh()
|
|
}
|
|
} catch (e) {
|
|
return await silentRefresh()
|
|
} finally {
|
|
isInitialized.value = true
|
|
}
|
|
}
|
|
|
|
const initializeAuth = async ({ code, redirectTo = '/oauth', replace = false } = {}) => {
|
|
if (code) {
|
|
const result = await exchangeCodeForToken(code)
|
|
if (!result.success) {
|
|
await navigateTo(redirectTo, { replace })
|
|
return false
|
|
}
|
|
|
|
if (result.isFirstLogin) {
|
|
await navigateTo('/welcome', { replace: true })
|
|
return true
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
if (isAuthenticated.value) {
|
|
const valid = await verifySession()
|
|
if (!valid) {
|
|
await navigateTo(redirectTo, { replace })
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
const success = await silentRefresh()
|
|
if (!success) {
|
|
await navigateTo(redirectTo, { replace })
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
const startLiveSessionPolling = () => {
|
|
if (import.meta.server) return
|
|
stopLiveSessionPolling()
|
|
|
|
sessionPollTimer = setInterval(async () => {
|
|
if (isAuthenticated.value) {
|
|
const valid = await verifySession()
|
|
if (!valid) {
|
|
handleLocalLogout()
|
|
}
|
|
}
|
|
}, 5000)
|
|
}
|
|
|
|
const stopLiveSessionPolling = () => {
|
|
if (sessionPollTimer) {
|
|
clearInterval(sessionPollTimer)
|
|
sessionPollTimer = null
|
|
}
|
|
}
|
|
|
|
const handleLocalLogout = () => {
|
|
clearSession()
|
|
isInitialized.value = true
|
|
navigateTo('/oauth')
|
|
}
|
|
|
|
const logout = async () => {
|
|
try {
|
|
await $fetch('/api/auth/logout', {
|
|
method: 'POST',
|
|
body: { refreshToken: refreshToken.value }
|
|
})
|
|
} catch (e) {
|
|
console.warn('Server logout failed:', e)
|
|
} finally {
|
|
handleLocalLogout()
|
|
}
|
|
}
|
|
|
|
return {
|
|
userToken,
|
|
token: userToken,
|
|
userProfile,
|
|
userRoles,
|
|
isAuthenticated,
|
|
isInitialized,
|
|
exchangeCodeForToken,
|
|
initializeAuth,
|
|
silentRefresh,
|
|
verifySession,
|
|
fetchUserProfile,
|
|
logout
|
|
}
|
|
} |