1019 lines
No EOL
37 KiB
TypeScript
1019 lines
No EOL
37 KiB
TypeScript
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<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 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<string, string> = {
|
||
'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<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();
|
||
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<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
|
||
};
|
||
|
||
// Prepare headers with CSRF token if available
|
||
const headers: Record<string, string> = {
|
||
'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<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();
|
||
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<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 {
|
||
// 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<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 = 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
|
||
};
|
||
}
|