import { useState } from 'react'; import { useMsal } from '@azure/msal-react'; import api from '../api'; import { useApiRequest } from './useApi'; import { addCSRFTokenToHeaders } from '../utils/csrfUtils'; import { getApiBaseUrl } from '../../config/config'; // 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 OAuth2 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', ''); // Prepare headers with CSRF token if available const headers: Record = { 'Content-Type': 'application/x-www-form-urlencoded' }; // Add CSRF token if available (for new security implementation) addCSRFTokenToHeaders(headers); // Log the request details for debugging console.log('🔍 Login request details:', { url: '/api/local/login', headers: headers, hasParams: !!params, paramsSize: params.toString().length, paramsContent: params.toString() }); // Use the existing api instance with custom headers for this request const response = await api.post('/api/local/login', params, { headers }); // Tokens are automatically set in httpOnly cookies by backend if (response.data.type === 'local_auth_success') { if (response.data.authenticationAuthority) { localStorage.setItem('auth_authority', response.data.authenticationAuthority); } console.log('✅ Local authentication successful - tokens set in httpOnly cookies'); // CRITICAL: Immediately fetch user data after successful login try { console.log('🔄 Fetching user data immediately after login...'); const userResponse = await api.get('/api/local/me'); if (userResponse.data) { // Cache user data in localStorage for privilege checkers and language localStorage.setItem('currentUser', JSON.stringify(userResponse.data)); console.log('✅ User data fetched and cached:', { username: userResponse.data.username, privilege: userResponse.data.privilege, language: userResponse.data.language }); } } catch (userError) { console.error('❌ Failed to fetch user data after login:', userError); // Don't block login flow, but log the error } return response.data; } throw new Error('Login failed'); } catch (error: any) { let errorMessage = 'An error occurred during login'; console.error('❌ Login error details:', { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, message: error.message, headers: error.response?.headers, config: { url: error.config?.url, method: error.config?.method, headers: error.config?.headers } }); // Additional debugging for CSRF-related errors if (error.response?.status === 500) { console.error('🚨 500 Error - Possible causes:'); console.error('1. Backend CSRF validation not implemented'); console.error('2. Backend expecting different CSRF token format'); console.error('3. Backend server error'); console.error('4. Check backend logs for detailed error information'); console.error('💡 To temporarily bypass CSRF, set CSRF_BYPASS_FOR_TESTING = true in csrfUtils.ts'); } 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(); const loginUrl = `${backendUrl}/api/msft/login?state=login`; console.log('🔐 Starting MSAL authentication...'); console.log('🌐 Backend URL:', backendUrl); console.log('🔗 Login URL:', loginUrl); console.log('đŸĒ Current cookies before auth:', document.cookie || 'No cookies'); // Open popup to backend Microsoft login route console.log('🚀 Opening Microsoft auth popup...'); const popup = window.open( loginUrl, 'msft-login', 'width=500,height=600,scrollbars=yes,resizable=yes,top=100,left=100' ); console.log('đŸĒŸ Popup window object:', popup ? 'Created successfully' : 'Failed to create'); 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) => { // 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 } console.log('📨 Received message from Microsoft auth popup:', { origin: event.origin, data: event.data, dataType: typeof event.data, hasType: !!event.data?.type, messageKeys: event.data ? Object.keys(event.data) : 'No data object', timestamp: new Date().toISOString() }); // 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'); console.log('📋 Full event data received:', event.data); // Store debug info in localStorage for persistence across navigation const debugInfo = { timestamp: new Date().toISOString(), eventData: event.data, eventDataKeys: Object.keys(event.data), hasAuthenticationAuthority: !!event.data.authenticationAuthority, cookiesBeforeAuth: document.cookie || 'No cookies', authFlow: 'msft_popup_success' }; localStorage.setItem('msft_auth_debug', JSON.stringify(debugInfo)); // Tokens are automatically set in httpOnly cookies by backend if (event.data.authenticationAuthority) { localStorage.setItem('auth_authority', event.data.authenticationAuthority); console.log('✅ Auth authority set:', event.data.authenticationAuthority); } else { // Fallback: set 'msft' as the auth authority for Microsoft login localStorage.setItem('auth_authority', 'msft'); console.log('âš ī¸ authenticationAuthority not in event data, setting fallback: msft'); console.log('📋 Available event.data properties:', Object.keys(event.data)); } console.log('✅ Microsoft authentication successful - tokens set in httpOnly cookies'); // CRITICAL: Immediately fetch user data after successful login // Wait a bit for cookies to be properly set setTimeout(async () => { try { console.log('🔄 Fetching user data immediately after Microsoft login...'); const userResponse = await api.get('/api/msft/me'); if (userResponse.data) { // Cache user data in localStorage for privilege checkers and language localStorage.setItem('currentUser', JSON.stringify(userResponse.data)); console.log('✅ User data fetched and cached:', { username: userResponse.data.username, privilege: userResponse.data.privilege, language: userResponse.data.language }); } } catch (userError) { console.error('❌ Failed to fetch user data after Microsoft login:', userError); // Store debug info const allCookies = document.cookie; const hasAccessToken = allCookies.includes('access_token'); const hasRefreshToken = allCookies.includes('refresh_token'); const cookieInfo = { allCookies: allCookies || 'No cookies visible', hasAccessToken, hasRefreshToken, authAuthority: localStorage.getItem('auth_authority'), timestamp: new Date().toISOString(), userFetchError: userError }; localStorage.setItem('msft_cookie_debug', JSON.stringify(cookieInfo)); console.log('đŸĒ Cookie check after Microsoft auth:', cookieInfo); } }, 500); // 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('❌ 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')); } }, 60000); // 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 }; // Prepare headers with CSRF token if available const headers: Record = { 'Content-Type': 'application/json' }; // Add CSRF token if available (for new security implementation) addCSRFTokenToHeaders(headers); const response = await api.post('/api/local/register', dataToSend, { headers }); 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 }; } // 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(); const loginUrl = `${backendUrl}/api/google/login`; console.log('🔐 Starting Google authentication...'); console.log('🌐 Backend URL:', backendUrl); console.log('🔗 Login URL:', loginUrl); // First, get the Google login URL from the backend using fetch to avoid CORS issues fetch(`${backendUrl}/api/google/login`, { method: 'GET', mode: 'cors', credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, redirect: 'manual' // Don't follow redirects }) .then(response => { console.log('📡 Backend response:', response); console.log('📊 Response status:', response.status); console.log('📊 Response type:', response.type); console.log('📊 Response headers:', response.headers); // Check if it's a redirect response if (response.status === 0 || response.type === 'opaque') { // This might be a CORS issue, try to get the redirect URL from the response console.log('🔄 CORS/Redirect detected, trying to extract URL from response'); // Try to read the response as text to get the redirect URL return response.text().then(text => { console.log('📄 Response text:', text); // Look for redirect URL in the response const urlMatch = text.match(/https:\/\/accounts\.google\.com\/o\/oauth2\/auth[^"'\s]*/); if (urlMatch) { return { login_url: urlMatch[0] }; } // If no URL found in text, try to construct it from the error throw new Error('Could not extract Google OAuth URL from response'); }); } else if (response.status >= 200 && response.status < 300) { // Normal JSON response return response.json(); } else if (response.status >= 300 && response.status < 400) { // Redirect response const location = response.headers.get('location'); console.log('🔄 Redirect location:', location); if (location) { return { login_url: location }; } throw new Error('Redirect response without location header'); } else { throw new Error(`HTTP error! status: ${response.status}`); } }) .then(data => { console.log('📄 Response data:', data); if (data.login_url) { // Open popup with the Google login URL const popup = window.open( data.login_url, 'google-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); setGoogleError(errorMsg); setIsGoogleLoading(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 === 'google_auth_success') { console.log('✅ Google authentication successful'); console.log('📋 Full event data received:', event.data); // Tokens are automatically set in httpOnly cookies by backend if (event.data.authenticationAuthority) { localStorage.setItem('auth_authority', event.data.authenticationAuthority); console.log('✅ Auth authority set:', event.data.authenticationAuthority); } else { // Fallback: set 'google' as the auth authority for Google login localStorage.setItem('auth_authority', 'google'); console.log('âš ī¸ authenticationAuthority not in event data, setting fallback: google'); console.log('📋 Available event.data properties:', Object.keys(event.data)); } console.log('✅ Google authentication successful - tokens set in httpOnly 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('❌ Google connection error:', 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); // Handle popup closing without completing auth let popupClosedManually = false; const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsGoogleLoading(false); if (!popupClosedManually) { console.warn('âš ī¸ Popup was closed before authentication completed'); setGoogleError('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 60 seconds'); popup.close(); clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsGoogleLoading(false); setGoogleError('Authentication timeout - please check your internet connection and try again'); reject(new Error('Authentication timeout')); } }, 60000); // Override popup.close to mark as manually closed const originalClose = popup.close; popup.close = function() { popupClosedManually = true; clearTimeout(loadTimeout); return originalClose.call(this); }; } else { throw new Error('No login URL received from backend'); } }) .catch(error => { console.error('❌ Failed to get Google login URL:', error); console.log('🔄 Attempting fallback approach...'); // Fallback: Try to construct the Google OAuth URL directly // This is a temporary solution until the backend is fixed const fallbackGoogleUrl = `${backendUrl}/api/google/login`; console.log('🔄 Using fallback URL:', fallbackGoogleUrl); // Open popup with the fallback URL (let the backend handle the redirect) const popup = window.open( fallbackGoogleUrl, 'google-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); setGoogleError(errorMsg); setIsGoogleLoading(false); reject(new Error('Popup was blocked')); return; } console.log('✅ Popup opened successfully with fallback URL'); // 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 === 'google_auth_success') { console.log('✅ Google authentication successful'); console.log('📋 Full event data received:', event.data); // Tokens are automatically set in httpOnly cookies by backend if (event.data.authenticationAuthority) { localStorage.setItem('auth_authority', event.data.authenticationAuthority); console.log('✅ Auth authority set:', event.data.authenticationAuthority); } else { // Fallback: set 'google' as the auth authority for Google login localStorage.setItem('auth_authority', 'google'); console.log('âš ī¸ authenticationAuthority not in event data, setting fallback: google'); console.log('📋 Available event.data properties:', Object.keys(event.data)); } console.log('✅ Google authentication successful - tokens set in httpOnly cookies'); // CRITICAL: Immediately fetch user data after successful login // Wait a bit for cookies to be properly set setTimeout(async () => { try { console.log('🔄 Fetching user data immediately after Google login...'); const userResponse = await api.get('/api/google/me'); if (userResponse.data) { // Cache user data in localStorage for privilege checkers and language localStorage.setItem('currentUser', JSON.stringify(userResponse.data)); console.log('✅ User data fetched and cached:', { username: userResponse.data.username, privilege: userResponse.data.privilege, language: userResponse.data.language }); } } catch (userError) { console.error('❌ Failed to fetch user data after Google login:', userError); // Don't block login flow, but log the error } }, 500); // 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('❌ Google connection error:', 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); // Handle popup closing without completing auth let popupClosedManually = false; const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsGoogleLoading(false); if (!popupClosedManually) { console.warn('âš ī¸ Popup was closed before authentication completed'); setGoogleError('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 60 seconds'); popup.close(); clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsGoogleLoading(false); setGoogleError('Authentication timeout - please check your internet connection and try again'); reject(new Error('Authentication timeout')); } }, 60000); // 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('❌ Google authentication error:', error); setGoogleError(error.message || 'Google authentication failed'); setIsGoogleLoading(false); throw error; } }; return { loginWithGoogle, error: googleError, isLoading: isGoogleLoading }; } // 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 { // Call logout endpoint to clear JWT tokens on server await api.post('/api/local/logout'); // Clear local storage (user data and auth_authority) // Note: JWT tokens are now stored in httpOnly cookies and cleared by backend localStorage.removeItem('currentUser'); localStorage.removeItem('auth_authority'); // 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 // Note: JWT tokens are now stored in httpOnly cookies and cleared by backend localStorage.removeItem('currentUser'); localStorage.removeItem('auth_authority'); 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 = localStorage.getItem('auth_authority'); let endpoint = '/api/local/me'; if (authAuthority === 'msft') { endpoint = '/api/msft/me'; } else if (authAuthority === 'google') { endpoint = '/api/google/me'; } console.log('🔍 Fetching user data from:', endpoint, 'auth authority:', authAuthority); const response = await api.get(endpoint); 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 }; }