frontend_nyla/src/hooks/useAuthentication.ts
2025-09-10 08:04:15 +02:00

529 lines
No EOL
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (username: string, password: string): Promise<LoginResponse> => {
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<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 = 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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const register = async (userData: RegisterData): Promise<RegisterResponse> => {
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<MsalRegisterData, any>();
const registerWithMsal = async (): Promise<RegisterResponse> => {
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<string | null>(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<string | null>(null);
const logout = async (): Promise<void> => {
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<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const getCurrentUser = async (): Promise<any> => {
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
};
}