ui-nyla/src/hooks/useAuthentication.ts
2026-05-08 11:49:24 +02:00

602 lines
No EOL
19 KiB
TypeScript

import { useState } from 'react';
import api from '../api';
import { getApiBaseUrl } from '../../config/config';
import { setUserDataCache, clearUserDataCache, type CachedUserData } from '../utils/userCache';
import {
loginApi,
fetchCurrentUserApi,
registerApi,
checkUsernameAvailabilityApi,
logoutApi,
requestPasswordResetApi,
resetPasswordApi,
type LoginResponse,
type RegisterResponse,
type UsernameAvailabilityResponse,
type RegisterData,
type PasswordResetRequestResponse,
type PasswordResetResponse
} from '../api/authApi';
// Regular authentication
export function useAuth() {
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (username: string, password: string): Promise<LoginResponse> => {
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<string | null>(null);
const [isMsalLoading, setIsMsalLoading] = useState(false);
const loginWithMsal = async (): Promise<MsalAuthResponse> => {
setIsMsalLoading(true);
setMsalError(null);
try {
return new Promise((resolve, reject) => {
const backendUrl = getApiBaseUrl();
// Open popup window
const popup = window.open(
`${backendUrl}/api/msft/auth/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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const register = async (userData: RegisterData): Promise<RegisterResponse> => {
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;
isNewUser?: boolean;
user: {
username: string;
email: string;
fullName: string;
mandateId: number;
};
}
export function useGoogleAuth() {
const [googleError, setGoogleError] = useState<string | null>(null);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const loginWithGoogle = async (): Promise<GoogleAuthResponse> => {
setIsGoogleLoading(true);
setGoogleError(null);
try {
return new Promise((resolve, reject) => {
const backendUrl = getApiBaseUrl();
// Open popup window
const popup = window.open(
`${backendUrl}/api/google/auth/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
};
}
// Username availability check
export function useUsernameAvailability() {
const [isChecking, setIsChecking] = useState(false);
const [error, setError] = useState<string | null>(null);
const checkAvailability = async (
username: string,
authenticationAuthority: string = 'local'
): Promise<UsernameAvailabilityResponse> => {
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<string | null>(null);
const [success, setSuccess] = useState(false);
const requestReset = async (username: string): Promise<PasswordResetRequestResponse> => {
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<string | null>(null);
const [success, setSuccess] = useState(false);
const resetPassword = async (token: string, password: string): Promise<PasswordResetResponse> => {
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<string | null>(null);
const _clearLocalState = () => {
clearUserDataCache();
localStorage.removeItem('authToken');
sessionStorage.clear();
document.cookie.split(";").forEach((c) => {
const name = c.split("=")[0].trim();
if (name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
});
};
const logout = async (): Promise<void> => {
setIsLoading(true);
setError(null);
try {
await logoutApi();
// Give browser time to process Set-Cookie headers from logout response
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (err: any) {
setError(err.response?.data?.detail || 'Logout failed');
} finally {
_clearLocalState();
setIsLoading(false);
window.location.href = '/login?logout=true';
}
};
return {
logout,
isLoading,
error
};
}
// Get current user
export function useCurrentUser() {
const [user, setUser] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const getCurrentUser = async (): Promise<any> => {
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
};
}