529 lines
No EOL
15 KiB
TypeScript
529 lines
No EOL
15 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) => {
|
||
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
|
||
};
|
||
}
|