import { useState } from 'react'; import { useMsal } from '@azure/msal-react'; import api from '../api'; import { useApiRequest } from './useApi'; import { getApiBaseUrl } from '../../config/config'; import { setUserDataCache, clearUserDataCache, type CachedUserData } from '../utils/userCache'; import { loginApi, fetchCurrentUserApi, registerApi, registerWithMsalApi, checkUsernameAvailabilityApi, logoutApi, requestPasswordResetApi, resetPasswordApi, type LoginResponse, type RegisterResponse, type UsernameAvailabilityResponse, type RegisterData, type MsalRegisterData, type PasswordResetRequestResponse, type PasswordResetResponse } from '../api/authApi'; // Regular authentication export function useAuth() { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const login = async (username: string, password: string): Promise => { setIsLoading(true); setError(null); try { const response = await loginApi({ username, password }); // Tokens are automatically set in httpOnly cookies by backend if (response.type === 'local_auth_success') { if (response.authenticationAuthority) { // Use sessionStorage for non-sensitive routing hint (cleared when tab closes) sessionStorage.setItem('auth_authority', response.authenticationAuthority); } // CRITICAL: Immediately fetch user data after successful login try { const userData = await fetchCurrentUserApi(); if (userData) { // Cache user data in sessionStorage (cleared on tab close - more secure than localStorage) setUserDataCache(userData as CachedUserData); } } catch (userError) { console.error('Failed to fetch user data after login:', userError); } return response; } throw new Error('Login failed'); } catch (error: any) { let errorMessage = 'An error occurred during login'; console.error('Login error:', error); if (error.response) { // Handle different error response formats if (error.response.data?.detail) { if (Array.isArray(error.response.data.detail)) { errorMessage = error.response.data.detail.map((err: any) => err.msg || err).join(', '); } else { errorMessage = error.response.data.detail; } } else if (error.response.data?.message) { errorMessage = error.response.data.message; } else if (error.response.status === 500) { errorMessage = 'Server error during login. Please try again.'; } else { errorMessage = 'Invalid username or password'; } } else if (error.request) { errorMessage = 'No response received from server'; } else { errorMessage = error.message; } setError(errorMessage); throw error; } finally { setIsLoading(false); } }; return { login, error, isLoading }; } // Microsoft Authentication interface MsalAuthResponse { accessToken: string; tokenType: string; user: { username: string; email: string; fullName: string; mandateId: number; }; } export function useMsalAuth() { const [msalError, setMsalError] = useState(null); const [isMsalLoading, setIsMsalLoading] = useState(false); const loginWithMsal = async (): Promise => { setIsMsalLoading(true); setMsalError(null); try { return new Promise((resolve, reject) => { const backendUrl = getApiBaseUrl(); // Open popup window const popup = window.open( `${backendUrl}/api/msft/login?state=login`, 'microsoft-login', 'width=600,height=700,left=100,top=100' ); if (!popup) { const errorMsg = 'Popup was blocked by browser. Please allow popups for this site and try again.'; console.error('Popup blocked:', errorMsg); setMsalError(errorMsg); setIsMsalLoading(false); reject(new Error('Popup was blocked')); return; } // Listen for messages from the popup const messageListener = (event: MessageEvent) => { // Filter out React DevTools messages if (event.data?.source?.includes('react-devtools') || event.data?.source?.includes('devtools') || event.data?.hello === true) { return; // Ignore React DevTools messages } // Verify the message origin for security (should match backend origin) const apiUrl = new URL(backendUrl); if (event.origin !== apiUrl.origin) { console.warn('Message from unauthorized origin:', event.origin, 'Expected:', apiUrl.origin); return; } if (event.data.type === 'msft_auth_success') { console.log('Login successful!'); const tokenData = event.data.token_data; // Store the token FIRST localStorage.setItem('authToken', tokenData.tokenAccess); // Set auth authority if (event.data.authenticationAuthority) { sessionStorage.setItem('auth_authority', event.data.authenticationAuthority); } else { sessionStorage.setItem('auth_authority', 'msft'); } // Configure axios to use the token api.defaults.headers.common['Authorization'] = `Bearer ${tokenData.tokenAccess}`; // NOW fetch user data with proper authentication setTimeout(async () => { try { const userData = await fetchCurrentUserApi('msft'); if (userData) { setUserDataCache(userData as CachedUserData); } } catch (userError) { console.error('Failed to fetch user data after Microsoft login:', userError); } }, 100); // Reduced timeout since we're not waiting for cookies // Clean up window.removeEventListener('message', messageListener); popup.close(); setIsMsalLoading(false); // Resolve with the response data resolve(event.data); } else if (event.data.type === 'msft_connection_error') { console.error('Login failed:', event.data.error); // Handle error window.removeEventListener('message', messageListener); popup.close(); setIsMsalLoading(false); setMsalError(event.data.error || 'Microsoft authentication failed'); reject(new Error(event.data.error || 'Microsoft authentication failed')); } }; // Add message listener window.addEventListener('message', messageListener); // Optional: Check if popup was closed without completing auth const checkPopup = setInterval(() => { if (popup.closed) { clearInterval(checkPopup); window.removeEventListener('message', messageListener); setIsMsalLoading(false); setMsalError('Authentication was cancelled - popup was closed before completing login'); reject(new Error('Authentication was cancelled')); } }, 1000); }); } catch (error: any) { console.error('MSAL authentication error:', error); setMsalError(error.message || 'Microsoft authentication failed'); setIsMsalLoading(false); throw error; } }; return { loginWithMsal, error: msalError, isLoading: isMsalLoading }; } // Registration export function useRegister() { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const register = async (userData: RegisterData): Promise => { setIsLoading(true); setError(null); try { return await registerApi(userData); } catch (error: any) { let errorMessage = 'An error occurred during registration'; if (error.response) { // Handle validation errors from FastAPI if (error.response.data?.detail) { if (Array.isArray(error.response.data.detail)) { // Handle FastAPI validation errors array errorMessage = error.response.data.detail.map((err: any) => err.msg).join(', '); } else { errorMessage = error.response.data.detail; } } else { errorMessage = 'Registration failed'; } } else if (error.request) { errorMessage = 'No response received from server'; } else { errorMessage = error.message; } setError(errorMessage); throw error; } finally { setIsLoading(false); } }; return { register, error, isLoading }; } // Google Authentication interface GoogleAuthResponse { accessToken: string; tokenType: string; user: { username: string; email: string; fullName: string; mandateId: number; }; } export function useGoogleAuth() { const [googleError, setGoogleError] = useState(null); const [isGoogleLoading, setIsGoogleLoading] = useState(false); const loginWithGoogle = async (): Promise => { setIsGoogleLoading(true); setGoogleError(null); try { return new Promise((resolve, reject) => { const backendUrl = getApiBaseUrl(); // Open popup window const popup = window.open( `${backendUrl}/api/google/login?state=login`, 'google-login', 'width=600,height=700,left=100,top=100' ); if (!popup) { const errorMsg = 'Popup was blocked by browser. Please allow popups for this site and try again.'; console.error('Popup blocked:', errorMsg); setGoogleError(errorMsg); setIsGoogleLoading(false); reject(new Error('Popup was blocked')); return; } // Listen for messages from the popup const messageListener = (event: MessageEvent) => { // Filter out React DevTools messages if (event.data?.source?.includes('react-devtools') || event.data?.source?.includes('devtools') || event.data?.hello === true) { return; // Ignore React DevTools messages } // Verify the message origin for security (should match backend origin) const apiUrl = new URL(backendUrl); if (event.origin !== apiUrl.origin) { console.warn('Message from unauthorized origin:', event.origin, 'Expected:', apiUrl.origin); return; } if (event.data.type === 'google_auth_success') { console.log('Login successful!'); const tokenData = event.data.token_data; // Store the token FIRST localStorage.setItem('authToken', tokenData.tokenAccess); // Set auth authority if (event.data.authenticationAuthority) { sessionStorage.setItem('auth_authority', event.data.authenticationAuthority); } else { sessionStorage.setItem('auth_authority', 'google'); } // Configure axios to use the token api.defaults.headers.common['Authorization'] = `Bearer ${tokenData.tokenAccess}`; // NOW fetch user data with proper authentication setTimeout(async () => { try { const userData = await fetchCurrentUserApi('google'); if (userData) { setUserDataCache(userData as CachedUserData); } } catch (userError) { console.error('Failed to fetch user data after Google login:', userError); } }, 100); // Reduced timeout since we're not waiting for cookies // Clean up window.removeEventListener('message', messageListener); popup.close(); setIsGoogleLoading(false); // Resolve with the response data resolve(event.data); } else if (event.data.type === 'google_connection_error') { console.error('Login failed:', event.data.error); // Handle error window.removeEventListener('message', messageListener); popup.close(); setIsGoogleLoading(false); setGoogleError(event.data.error || 'Google authentication failed'); reject(new Error(event.data.error || 'Google authentication failed')); } }; // Add message listener window.addEventListener('message', messageListener); // Optional: Check if popup was closed without completing auth const checkPopup = setInterval(() => { if (popup.closed) { clearInterval(checkPopup); window.removeEventListener('message', messageListener); setIsGoogleLoading(false); setGoogleError('Authentication was cancelled - popup was closed before completing login'); reject(new Error('Authentication was cancelled')); } }, 1000); }); } catch (error: any) { console.error('Google authentication error:', error); setGoogleError(error.message || 'Google authentication failed'); setIsGoogleLoading(false); throw error; } }; return { loginWithGoogle, error: googleError, isLoading: isGoogleLoading }; } // Microsoft Registration export function useMsalRegister() { const { instance, accounts } = useMsal(); const { request, isLoading, error } = useApiRequest(); const registerWithMsal = async (): Promise => { try { if (!accounts || accounts.length === 0) { // If not signed in with Microsoft, sign in first await instance.loginPopup({ scopes: ['user.read'] }); } // Get the current account const currentAccount = instance.getAllAccounts()[0]; if (!currentAccount) { throw new Error('No Microsoft account found'); } // Prepare user data from Microsoft account const userData: MsalRegisterData = { username: currentAccount.username, email: currentAccount.username, fullName: currentAccount.name || currentAccount.username, language: 'de' }; // Register the user through our backend return await registerWithMsalApi(request, userData); } catch (error: any) { throw error; } }; return { registerWithMsal, error, isLoading }; } // Username availability check export function useUsernameAvailability() { const [isChecking, setIsChecking] = useState(false); const [error, setError] = useState(null); const checkAvailability = async ( username: string, authenticationAuthority: string = 'local' ): Promise => { setIsChecking(true); setError(null); try { return await checkUsernameAvailabilityApi(username, authenticationAuthority); } catch (error: any) { let errorMessage = 'Failed to check username availability'; if (error.response) { errorMessage = error.response.data?.detail || errorMessage; } setError(errorMessage); throw error; } finally { setIsChecking(false); } }; return { checkAvailability, isChecking, error }; } // Password reset request (by username) export function usePasswordResetRequest() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const requestReset = async (username: string): Promise => { setIsLoading(true); setError(null); setSuccess(false); try { const response = await requestPasswordResetApi(username); setSuccess(true); return response; } catch (error: any) { // For security, we don't reveal if the username exists or not // So we still show success even on error setSuccess(true); return { success: true, message: 'If a user with this username exists, a reset link has been sent to their email.' }; } finally { setIsLoading(false); } }; return { requestReset, isLoading, error, success }; } // Password reset (set new password with token) export function usePasswordReset() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const resetPassword = async (token: string, password: string): Promise => { setIsLoading(true); setError(null); setSuccess(false); try { const response = await resetPasswordApi(token, password); setSuccess(true); return response; } catch (error: any) { let errorMessage = 'Passwort-Zurücksetzung fehlgeschlagen'; if (error.response) { if (error.response.data?.detail) { if (Array.isArray(error.response.data.detail)) { errorMessage = error.response.data.detail.map((err: any) => err.msg || err).join(', '); } else { errorMessage = error.response.data.detail; } } } else if (error.message) { errorMessage = error.message; } setError(errorMessage); throw error; } finally { setIsLoading(false); } }; return { resetPassword, isLoading, error, success }; } // Logout function export function useLogout() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const logout = async (): Promise => { setIsLoading(true); setError(null); try { // Call logout endpoint to clear JWT tokens on server await logoutApi(); // CRITICAL: Wait for browser to process Set-Cookie headers from logout response // This gives the browser time to clear httpOnly cookies before redirect await new Promise(resolve => setTimeout(resolve, 1000)); // Clear user data cache from sessionStorage clearUserDataCache(); // Clear auth authority from sessionStorage sessionStorage.removeItem('auth_authority'); // Clear MSAL cache tokens from localStorage // MSAL stores tokens with keys starting with 'msal.' const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && ( key.startsWith('msal.') || key === 'auth_token' || key === 'refresh_token' || key.includes('token') || key.includes('auth') || key.includes('msal') )) { keysToRemove.push(key); } } keysToRemove.forEach(key => { localStorage.removeItem(key); }); // Clear ALL MSAL cache data (including account keys, token keys, version) const msalKeysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('msal.')) { msalKeysToRemove.push(key); } } msalKeysToRemove.forEach(key => { localStorage.removeItem(key); }); // Clear sessionStorage as well (CSRF tokens, etc.) sessionStorage.clear(); // Clear cookies as backup (in case backend doesn't clear them properly) // Note: This only works for cookies that are accessible to JavaScript const cookies = document.cookie.split(";"); cookies.forEach(function(c) { const cookieName = c.split("=")[0].trim(); if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) { document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; } }); // Redirect to login page window.location.href = '/login?logout=true'; } catch (error: any) { let errorMessage = 'Logout failed'; if (error.response) { errorMessage = error.response.data?.detail || errorMessage; } setError(errorMessage); // Even if logout fails on server, clear local data and redirect clearUserDataCache(); sessionStorage.removeItem('auth_authority'); // Clear MSAL cache tokens from localStorage const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && ( key.startsWith('msal.') || key === 'auth_token' || key === 'refresh_token' || key.includes('token') || key.includes('auth') || key.includes('msal') )) { keysToRemove.push(key); } } keysToRemove.forEach(key => { localStorage.removeItem(key); }); // Clear ALL MSAL cache data (including account keys, token keys, version) const msalKeysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('msal.')) { msalKeysToRemove.push(key); } } msalKeysToRemove.forEach(key => { localStorage.removeItem(key); }); // Clear sessionStorage as well sessionStorage.clear(); // Clear cookies as backup (in case backend doesn't clear them properly) document.cookie.split(";").forEach(function(c) { const cookieName = c.split("=")[0].trim(); if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) { document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; } }); window.location.href = '/login?logout=true'; } finally { setIsLoading(false); } }; return { logout, isLoading, error }; } // Get current user export function useCurrentUser() { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const getCurrentUser = async (): Promise => { setIsLoading(true); setError(null); try { // Determine the correct endpoint based on authentication authority const authAuthority = sessionStorage.getItem('auth_authority') || undefined; const userData = await fetchCurrentUserApi(authAuthority); setUser(userData); // Cache user data in sessionStorage (cleared on tab close - more secure than localStorage) setUserDataCache(userData as CachedUserData); return userData; } catch (error: any) { let errorMessage = 'Failed to get current user'; if (error.response) { errorMessage = error.response.data?.detail || errorMessage; } setError(errorMessage); throw error; } finally { setIsLoading(false); } }; return { user, getCurrentUser, isLoading, error }; }