Files
antrean-operasi/composables/useAuth.ts
2026-03-16 10:54:57 +07:00

167 lines
5.6 KiB
TypeScript

// composables/useAuth.ts - Enhanced version with better error handling
import type { User, SessionResponse, LoginResponse, LogoutResponse } from '~/types/auth'
export const useAuth = () => {
// Use useState inside the composable function (not at module level)
// This ensures all components and plugins share the same reactive state
const _user = useState<User | null>('auth:user', () => null)
const _accessToken = useState<string | null>('auth:accessToken', () => null)
const _isLoading = useState<boolean>('auth:isLoading', () => false)
const _error = useState<string | null>('auth:error', () => null)
const isAuthenticated = computed(() => !!_user.value)
const clearError = () => {
_error.value = null
}
const checkAuth = async (): Promise<User | null> => {
try {
_isLoading.value = true
clearError()
// The session API returns SessionResponse, or throws 401 if not authenticated
// $fetch automatically sends cookies for same-origin requests
const response = await $fetch<SessionResponse>('/api/auth/session', {
credentials: 'include' // Explicitly include cookies (though $fetch does this by default)
})
// Handle response structure - session API returns { success, user, ... }
if (response && response.user) {
_user.value = response.user
_accessToken.value = response.accessToken || null
// Store tokens to localStorage for API interceptor (only on client-side)
// Only update if localStorage is empty to avoid overwriting refreshed tokens
if (process.client) {
// Only set tokens if they don't exist yet (avoid overwriting refreshed tokens)
if (!localStorage.getItem('idToken') && response.idToken) {
localStorage.setItem('idToken', response.idToken)
}
if (!localStorage.getItem('accessToken') && response.accessToken) {
localStorage.setItem('accessToken', response.accessToken)
}
if (!localStorage.getItem('refreshToken') && response.refreshToken) {
localStorage.setItem('refreshToken', response.refreshToken)
}
}
return response.user
}
// If response exists but no user, clear and return null
_user.value = null
_accessToken.value = null
return null
} catch (fetchError: any) {
console.error('❌ checkAuth: Session check failed:', fetchError)
// 401 errors are expected when not authenticated
if (fetchError.statusCode === 401 || fetchError.status === 401) {
_error.value = null // Don't show error for expected unauthenticated state
} else {
_error.value = 'Failed to check authentication status'
}
_user.value = null
_accessToken.value = null
// Clear tokens from localStorage on auth failure
if (process.client) {
localStorage.removeItem('idToken')
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
}
return null
} finally {
_isLoading.value = false
}
}
const login = async (): Promise<void> => {
try {
clearError()
const response = await $fetch<LoginResponse>('/api/auth/keycloak-login', {
method: 'POST'
})
if (response?.success && response?.data?.authUrl) {
window.location.href = response.data.authUrl
} else {
const errorMsg = response?.error || 'Failed to get authorization URL'
_error.value = errorMsg
throw new Error(errorMsg)
}
} catch (loginError: any) {
console.error('❌ Login error:', loginError)
_error.value = loginError.message || 'Login failed'
throw loginError
}
}
const logout = async (): Promise<void> => {
try {
_isLoading.value = true
clearError()
const response = await $fetch<LogoutResponse>('/api/auth/logout', {
method: 'POST'
})
// const response = await $fetch<LogoutResponse>('/api/auth/clear-session', {
// method: 'POST'
// });
// Clear user immediately regardless of response
_user.value = null
_accessToken.value = null
if (response?.success && response?.logoutUrl) {
//remove all tokens from localstorage
localStorage.removeItem('idToken');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// Keycloak will redirect back to the base URL after logout
// We can add a query param to detect the logout and show a message
// window.location.href = response.logoutUrl
window.location.href = "/auth/login?logout=success"
} else {
const warningMsg = response?.error || response?.message || 'No logout URL received'
console.warn('⚠️', warningMsg)
_error.value = warningMsg
await navigateTo('/auth/login')
}
} catch (logoutError: any) {
console.error('❌ Logout error:', logoutError)
_error.value = logoutError.message || 'Logout failed'
_user.value = null
_accessToken.value = null
await navigateTo('/auth/login')
} finally {
_isLoading.value = false
}
}
// Helper function to refresh user data
const refreshUser = async (): Promise<boolean> => {
const userData = await checkAuth()
return !!userData
}
return {
// State - expose the global state
user: _user,
accessToken: _accessToken,
isAuthenticated,
isLoading: _isLoading,
error: _error,
// Actions
checkAuth,
login,
logout,
refreshUser,
clearError,
}
}