import { useState } from 'react'; import { useMsal } from '@azure/msal-react'; import api from '../api'; import { useApiRequest } from './useApi'; // Regular authentication interface LoginResponse { accessToken: string; tokenType: string; label?: any; fieldLabels?: any; } 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 { // Create the form data in the exact format FastAPI expects const params = new URLSearchParams(); params.append('username', username); params.append('password', password); params.append('grant_type', 'password'); params.append('scope', ''); params.append('client_id', ''); params.append('client_secret', ''); // Generate a simple CSRF token (in production, this should come from the server) const csrfToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); // Use the existing api instance with custom headers for this request const response = await api.post('/api/local/login', params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrfToken } }); // Normalize the response structure to match what the frontend expects let normalizedAuthData; if (response.data.token_data) { // Backend returns token_data with tokenAccess field, normalize to accessToken normalizedAuthData = { accessToken: response.data.token_data.tokenAccess || response.data.access_token, tokenType: response.data.token_data.tokenType || 'bearer', userId: response.data.token_data.userId, expiresAt: response.data.token_data.expiresAt, createdAt: response.data.token_data.createdAt }; } else { // Fallback to old structure if needed normalizedAuthData = { accessToken: response.data.access_token, tokenType: response.data.token_type || 'bearer' }; } // Store the normalized auth response localStorage.setItem('auth_data', JSON.stringify(normalizedAuthData)); return { accessToken: normalizedAuthData.accessToken, tokenType: normalizedAuthData.tokenType }; } catch (error: any) { let errorMessage = 'An error occurred during login'; if (error.response) { errorMessage = error.response.data?.detail || '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 = import.meta.env.VITE_API_BASE_URL; const loginUrl = `${backendUrl}/api/msft/login?state=login`; console.log('🔐 Starting MSAL authentication...'); console.log('🌐 Backend URL:', backendUrl); console.log('🔗 Login URL:', loginUrl); // Open popup to backend Microsoft login route const popup = window.open( loginUrl, 'msft-login', 'width=500,height=600,scrollbars=yes,resizable=yes,top=100,left=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; } console.log('✅ Popup opened successfully'); // Listen for messages from the popup const messageListener = (event: MessageEvent) => { console.log('📨 Received message from popup:', event.origin, event.data); // Verify origin for security 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('✅ MSAL authentication successful'); // Store the auth data with normalized field names if (event.data.token_data) { const normalizedTokenData = { accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken tokenType: event.data.token_data.tokenType, userId: event.data.token_data.userId, expiresAt: event.data.token_data.expiresAt, createdAt: event.data.token_data.createdAt }; localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData)); console.log('💾 Auth data stored in localStorage'); } // Clean up window.removeEventListener('message', messageListener); popup.close(); setIsMsalLoading(false); // Resolve with the token data resolve({ accessToken: event.data.token_data.tokenAccess, tokenType: event.data.token_data.tokenType || 'bearer', user: { username: '', // Will be populated by the backend email: '', fullName: '', mandateId: 0 } }); } else if (event.data.type === 'msft_connection_error') { console.error('❌ MSAL connection error:', 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); // Handle popup closing without completing auth let popupClosedManually = false; const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsMsalLoading(false); if (!popupClosedManually) { console.warn('âš ī¸ Popup was closed before authentication completed'); setMsalError('Authentication was cancelled - popup was closed before completing login'); } else { console.log('â„šī¸ Popup closed after successful authentication'); } if (!popupClosedManually) { reject(new Error('Authentication was cancelled')); } } }, 1000); // Set a timeout to detect if popup doesn't load const loadTimeout = setTimeout(() => { if (!popup.closed) { console.warn('âš ī¸ Popup did not load within 10 seconds'); popup.close(); clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsMsalLoading(false); setMsalError('Authentication timeout - please check your internet connection and try again'); reject(new Error('Authentication timeout')); } }, 10000); // Override popup.close to mark as manually closed const originalClose = popup.close; popup.close = function() { popupClosedManually = true; clearTimeout(loadTimeout); return originalClose.call(this); }; }); } 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 interface RegisterData { username: string; password: string; email: string; fullName: string; language?: string; enabled?: boolean; privilege?: string; } interface RegisterResponse { success: boolean; message?: string; user?: { id: string; username: string; email: string; fullName: string; language: string; enabled: boolean; privilege: string; }; } export function useRegister() { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const register = async (userData: RegisterData): Promise => { setIsLoading(true); setError(null); try { // Prepare data to match backend expectations // Backend expects userData as object and password as embedded field const dataToSend = { userData: { username: userData.username, email: userData.email, fullName: userData.fullName, language: userData.language || 'de', enabled: userData.enabled !== undefined ? userData.enabled : true, privilege: userData.privilege || 'user' }, password: userData.password }; const response = await api.post('/api/local/register', dataToSend, { headers: { 'Content-Type': 'application/json' } }); return { success: true, message: 'Registration successful', user: response.data }; } 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 }; } // Microsoft Registration interface MsalRegisterData { username: string; email: string; fullName: string; language?: string; } 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 const response = await request({ url: '/api/msft/register', method: 'post', data: userData, additionalConfig: { headers: { 'Content-Type': 'application/json' } } }); return { success: true, message: 'Registration successful', user: response }; } 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<{ username: string; authenticationAuthority: string; available: boolean; message: string; }> => { setIsChecking(true); setError(null); try { const response = await api.get('/api/local/available', { params: { username, authenticationAuthority } }); return response.data; } 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 }; } // Logout function export function useLogout() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const logout = async (): Promise => { setIsLoading(true); setError(null); try { await api.post('/api/local/logout'); // Clear local storage localStorage.removeItem('auth_data'); // Redirect to login page window.location.href = '/login'; } catch (error: any) { let errorMessage = 'Logout failed'; if (error.response) { errorMessage = error.response.data?.detail || errorMessage; } setError(errorMessage); throw error; } 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 { const response = await api.get('/api/local/me'); setUser(response.data); return response.data; } 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 }; }