frontend_nyla/src/hooks/useAuthentication.ts
2025-08-22 13:09:24 +02:00

480 lines
No EOL
13 KiB
TypeScript

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) => {
// Open popup to backend Microsoft login route
const popup = window.open(
`${import.meta.env.VITE_API_BASE_URL}/api/msft/login?state=login`,
'msft-login',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
setMsalError('Popup was blocked. Please allow popups and try again.');
setIsMsalLoading(false);
reject(new Error('Popup was blocked'));
return;
}
// Listen for messages from the popup
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(import.meta.env.VITE_API_BASE_URL);
if (event.origin !== apiUrl.origin) {
return;
}
if (event.data.type === 'msft_auth_success') {
// 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));
}
// 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') {
// 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
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsMsalLoading(false);
setMsalError('Authentication was cancelled');
reject(new Error('Authentication was cancelled'));
}
}, 1000);
});
} catch (error: any) {
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
};
}