326 lines
No EOL
9.8 KiB
TypeScript
326 lines
No EOL
9.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useApiRequest } from './useApi';
|
|
|
|
// User interfaces
|
|
export interface User {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
fullName: string;
|
|
language: string;
|
|
enabled: boolean;
|
|
privilege: string;
|
|
authenticationAuthority: string;
|
|
mandateId: string;
|
|
}
|
|
|
|
export type UserUpdateData = Partial<Omit<User, 'id' | 'mandateId'>>;
|
|
|
|
// Current user hook
|
|
export function useCurrentUser() {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const { request, isLoading, error } = useApiRequest<null, User>();
|
|
|
|
const fetchCurrentUser = async (retryCount = 0) => {
|
|
try {
|
|
// Check if we already have user data from JWT token
|
|
const cachedUser = localStorage.getItem('currentUser');
|
|
if (cachedUser) {
|
|
try {
|
|
const userData = JSON.parse(cachedUser);
|
|
setUser(userData);
|
|
console.log('✅ Using cached user data from JWT:', userData);
|
|
return;
|
|
} catch (error) {
|
|
console.error('Error parsing cached user data:', error);
|
|
localStorage.removeItem('currentUser');
|
|
}
|
|
}
|
|
|
|
// JWT tokens are now stored in httpOnly cookies, so we fetch user data from API
|
|
console.log('🍪 JWT tokens are now in httpOnly cookies, fetching user data from API');
|
|
|
|
// 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 API:', {
|
|
endpoint,
|
|
authAuthority,
|
|
hasAuthCookies: document.cookie.includes('access_token') || document.cookie.includes('refresh_token')
|
|
});
|
|
|
|
// Add a small delay to ensure cookies are properly set after authentication
|
|
if (authAuthority === 'msft' || authAuthority === 'google') {
|
|
console.log('⏳ Adding delay for OAuth authentication cookie propagation...');
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
}
|
|
|
|
const data = await request({
|
|
url: endpoint,
|
|
method: 'get'
|
|
});
|
|
setUser(data);
|
|
// Cache user data in localStorage for privilege checkers
|
|
localStorage.setItem('currentUser', JSON.stringify(data));
|
|
console.log('✅ User data fetched from API and cached:', data);
|
|
} catch (error: any) {
|
|
console.error('❌ Failed to fetch user data:', error);
|
|
|
|
// Display stored debug information if this is a Microsoft auth failure
|
|
const authAuthority = localStorage.getItem('auth_authority');
|
|
if (authAuthority === 'msft') {
|
|
const msftAuthDebug = localStorage.getItem('msft_auth_debug');
|
|
const msftCookieDebug = localStorage.getItem('msft_cookie_debug');
|
|
|
|
console.log('🔍 Microsoft authentication debug information:');
|
|
if (msftAuthDebug) {
|
|
console.log('📋 Auth flow debug:', JSON.parse(msftAuthDebug));
|
|
} else {
|
|
console.log('❌ No Microsoft auth debug info found');
|
|
}
|
|
|
|
if (msftCookieDebug) {
|
|
console.log('🍪 Cookie debug:', JSON.parse(msftCookieDebug));
|
|
} else {
|
|
console.log('❌ No Microsoft cookie debug info found');
|
|
}
|
|
|
|
console.log('🔧 Current state:', {
|
|
authAuthority,
|
|
currentCookies: document.cookie || 'No cookies',
|
|
hasAccessToken: document.cookie.includes('access_token'),
|
|
hasRefreshToken: document.cookie.includes('refresh_token'),
|
|
retryCount,
|
|
error: error.message
|
|
});
|
|
}
|
|
|
|
// If authentication failed and we haven't retried yet, try again after a delay
|
|
const isOAuth = authAuthority === 'msft' || authAuthority === 'google';
|
|
const maxRetries = isOAuth ? 2 : 0; // Only retry for OAuth
|
|
|
|
if (retryCount < maxRetries && (error.message?.includes('Not authenticated') || error.message?.includes('Request aborted'))) {
|
|
console.log(`🔄 Retrying user data fetch (attempt ${retryCount + 1}/${maxRetries + 1}) in 2 seconds...`);
|
|
setTimeout(() => {
|
|
fetchCurrentUser(retryCount + 1);
|
|
}, 2000);
|
|
return;
|
|
}
|
|
|
|
// If all retries failed or this is not a retryable error
|
|
setUser(null);
|
|
localStorage.removeItem('currentUser');
|
|
|
|
// If authentication failed after all retries, clear auth data
|
|
if (error.message?.includes('Not authenticated') || error.message?.includes('401')) {
|
|
console.log('🔐 Authentication failed after retries - clearing auth data');
|
|
|
|
// Clean up debug info
|
|
localStorage.removeItem('msft_auth_debug');
|
|
localStorage.removeItem('msft_cookie_debug');
|
|
localStorage.removeItem('auth_authority');
|
|
localStorage.removeItem('currentUser');
|
|
|
|
// Trigger a redirect to login by forcing a page reload
|
|
setTimeout(() => {
|
|
window.location.href = '/login';
|
|
}, 1000);
|
|
}
|
|
}
|
|
};
|
|
|
|
const logout = async (msalInstance?: any) => {
|
|
if (!user) {
|
|
throw new Error('No user to logout');
|
|
}
|
|
|
|
try {
|
|
let logoutEndpoint = '/api/local/logout';
|
|
|
|
// Determine the correct logout endpoint based on authentication authority
|
|
if (user.authenticationAuthority === 'msft') {
|
|
logoutEndpoint = '/api/msft/logout';
|
|
} else if (user.authenticationAuthority === 'local') {
|
|
logoutEndpoint = '/api/local/logout';
|
|
}
|
|
|
|
await request({
|
|
url: logoutEndpoint,
|
|
method: 'post'
|
|
});
|
|
|
|
// Clear user state after successful logout
|
|
setUser(null);
|
|
|
|
// Clear any local storage data
|
|
localStorage.clear();
|
|
|
|
// Handle MSAL logout for Microsoft authentication
|
|
if (user.authenticationAuthority === 'msft' && msalInstance) {
|
|
try {
|
|
await msalInstance.logoutRedirect({
|
|
onRedirectNavigate: () => true
|
|
});
|
|
return; // MSAL will handle the redirect
|
|
} catch (msalError) {
|
|
console.error('MSAL logout failed:', msalError);
|
|
// Continue with regular redirect if MSAL logout fails
|
|
}
|
|
}
|
|
|
|
// Redirect to login or home page
|
|
window.location.href = '/login';
|
|
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
// Try to load user from localStorage first for faster initial load
|
|
const cachedUser = localStorage.getItem('currentUser');
|
|
if (cachedUser) {
|
|
try {
|
|
const userData = JSON.parse(cachedUser);
|
|
setUser(userData);
|
|
} catch (error) {
|
|
console.error('Error parsing cached user data:', error);
|
|
localStorage.removeItem('currentUser');
|
|
}
|
|
}
|
|
|
|
// For OAuth authentication, wait a bit longer before fetching user data
|
|
const authAuthority = localStorage.getItem('auth_authority');
|
|
const isOAuth = authAuthority === 'msft' || authAuthority === 'google';
|
|
|
|
if (isOAuth) {
|
|
console.log('⏳ OAuth authentication detected, delaying user data fetch...');
|
|
// Wait for authentication cookies to be properly set
|
|
const timer = setTimeout(() => {
|
|
fetchCurrentUser();
|
|
}, 1000);
|
|
|
|
return () => clearTimeout(timer);
|
|
} else {
|
|
// For local authentication, fetch immediately
|
|
fetchCurrentUser();
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
user,
|
|
error,
|
|
isLoading,
|
|
refetch: fetchCurrentUser,
|
|
logout
|
|
};
|
|
}
|
|
|
|
// Organization users hook (list, update, delete)
|
|
export function useOrgUsers() {
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const { request, isLoading: loading, error } = useApiRequest();
|
|
|
|
const fetchUsers = async () => {
|
|
try {
|
|
const data = await request({
|
|
url: '/api/users/',
|
|
method: 'get'
|
|
});
|
|
setUsers(data);
|
|
} catch (error) {
|
|
// Error is already handled by useApiRequest
|
|
}
|
|
};
|
|
|
|
const updateUser = async (userId: string, userData: User) => {
|
|
await request({
|
|
url: `/api/users/${userId}`,
|
|
method: 'put',
|
|
data: userData
|
|
});
|
|
await fetchUsers(); // Refresh the list after update
|
|
};
|
|
|
|
const deleteUser = async (userId: string) => {
|
|
await request({
|
|
url: `/api/users/${userId}`,
|
|
method: 'delete'
|
|
});
|
|
await fetchUsers(); // Refresh the list after deletion
|
|
};
|
|
|
|
const getUser = async (userId: string): Promise<User> => {
|
|
return await request({
|
|
url: `/api/users/${userId}`,
|
|
method: 'get'
|
|
});
|
|
};
|
|
|
|
const createUser = async (userData: Omit<User, 'id' | 'mandateId'> & { password: string }) => {
|
|
await request({
|
|
url: '/api/users',
|
|
method: 'post',
|
|
data: userData
|
|
});
|
|
await fetchUsers(); // Refresh the list after creation
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
}, []);
|
|
|
|
return {
|
|
users,
|
|
loading,
|
|
error,
|
|
refetch: fetchUsers,
|
|
updateUser,
|
|
deleteUser,
|
|
getUser,
|
|
createUser
|
|
};
|
|
}
|
|
|
|
// Individual user operations hook (for use when you don't need the full list)
|
|
export function useUser() {
|
|
const { request, isLoading, error } = useApiRequest();
|
|
|
|
const getUser = async (userId: string): Promise<User> => {
|
|
return await request({
|
|
url: `/api/users/${userId}`,
|
|
method: 'get'
|
|
});
|
|
};
|
|
|
|
const updateUser = async (userId: string, userData: User): Promise<User> => {
|
|
return await request({
|
|
url: `/api/users/${userId}`,
|
|
method: 'put',
|
|
data: userData
|
|
});
|
|
};
|
|
|
|
const deleteUser = async (userId: string): Promise<void> => {
|
|
await request({
|
|
url: `/api/users/${userId}`,
|
|
method: 'delete'
|
|
});
|
|
};
|
|
|
|
return {
|
|
getUser,
|
|
updateUser,
|
|
deleteUser,
|
|
isLoading,
|
|
error
|
|
};
|
|
}
|