// 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('auth:user', () => null) const _accessToken = useState('auth:accessToken', () => null) const _isLoading = useState('auth:isLoading', () => false) const _error = useState('auth:error', () => null) const isAuthenticated = computed(() => !!_user.value) const clearError = () => { _error.value = null } const checkAuth = async (): Promise => { 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('/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 => { try { clearError() const response = await $fetch('/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 => { try { _isLoading.value = true clearError() const response = await $fetch('/api/auth/logout', { method: 'POST' }) // const response = await $fetch('/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 => { 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, } }