interface User { id: string email: string username: string name: string authMethod: string } interface AuthState { user: User | null authenticated: boolean } export const useCustomAuth = () => { const user = ref(null) const authenticated = ref(false) const loading = ref(true) const refreshing = ref(false) const retryCount = ref(0) const maxRetries = 3 // Check authentication status with retry logic const checkAuth = async (skipRetry = false) => { try { loading.value = true const data = await $fetch('/api/auth/session', { retry: skipRetry ? 0 : 2, retryDelay: 1000 }) user.value = data.user authenticated.value = data.authenticated retryCount.value = 0 // Reset retry count on success console.log('[CUSTOM_AUTH] Session check result:', { authenticated: data.authenticated, userId: data.user?.id }) } catch (error) { console.error('[CUSTOM_AUTH] Session check failed:', error) // If it's a network error and we haven't exceeded retry limit, try refresh if (!skipRetry && retryCount.value < maxRetries && (error as any)?.status >= 500) { retryCount.value++ console.log(`[CUSTOM_AUTH] Retrying session check (${retryCount.value}/${maxRetries})...`) // Wait a bit before retrying await new Promise(resolve => setTimeout(resolve, 1000 * retryCount.value)) return checkAuth(false) } user.value = null authenticated.value = false } finally { loading.value = false } } // Refresh token with better error handling const refreshToken = async () => { if (refreshing.value) return false try { refreshing.value = true console.log('[CUSTOM_AUTH] Attempting token refresh...') const response = await $fetch<{ success: boolean; expiresAt?: number }>('/api/auth/refresh', { method: 'POST', retry: 2, retryDelay: 1000 }) if (response.success) { console.log('[CUSTOM_AUTH] Token refresh successful') await checkAuth(true) // Re-check auth state after refresh, skip retry to avoid loops return true } return false } catch (error) { console.error('[CUSTOM_AUTH] Token refresh failed:', error) // Check if it's a 401 (invalid refresh token) vs other errors if ((error as any)?.status === 401) { console.log('[CUSTOM_AUTH] Refresh token invalid, clearing auth state') user.value = null authenticated.value = false return false } // For other errors (network issues, 502, etc.), don't clear auth state immediately // The auto-refresh plugin will handle retries console.log('[CUSTOM_AUTH] Network error during refresh, keeping auth state') return false } finally { refreshing.value = false } } // Login with Keycloak const login = () => { const authUrl = 'https://auth.portnimara.dev/realms/client-portal/protocol/openid-connect/auth?' + new URLSearchParams({ client_id: 'client-portal', redirect_uri: 'https://client.portnimara.dev/api/auth/keycloak/callback', response_type: 'code', scope: 'openid profile email', state: Math.random().toString(36).substring(2) }).toString() console.log('[CUSTOM_AUTH] Redirecting to Keycloak login:', authUrl) navigateTo(authUrl, { external: true }) } // Logout const logout = async () => { try { console.log('[CUSTOM_AUTH] Initiating logout...') // Clear local state immediately user.value = null authenticated.value = false // Redirect to logout endpoint await navigateTo('/api/auth/logout', { external: true }) } catch (error) { console.error('[CUSTOM_AUTH] Logout failed:', error) // Fallback: redirect to login await navigateTo('/login') } } // Initialize auth state on composable creation onMounted(() => { checkAuth() }) return { user: readonly(user), authenticated: readonly(authenticated), loading: readonly(loading), refreshing: readonly(refreshing), login, logout, checkAuth, refreshToken } }