import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import { setUserDataCache, getUserDataCache, clearUserDataCache } from '../utils/userCache'; import api from '../api'; import { usePermissions, type UserPermissions } from './usePermissions'; import { fetchCurrentUser as fetchCurrentUserApi, logoutUser as logoutUserApi, fetchUsers as fetchUsersApi, fetchUserById as fetchUserByIdApi, createUser as createUserApi, updateUser as updateUserApi, deleteUser as deleteUserApi, type User, type UserUpdateData, type AttributeDefinition, type PaginationParams } from '../api/userApi'; // Re-export types for backward compatibility export type { User, UserUpdateData, AttributeDefinition, PaginationParams }; // Current user hook export function useCurrentUser() { const [user, setUser] = useState(null); const { request, isLoading, error } = useApiRequest(); const fetchCurrentUser = async (retryCount = 0) => { try { // Check if we already have user data in sessionStorage cache const cachedUser = getUserDataCache(); if (cachedUser) { setUser(cachedUser); console.log('โœ… Using cached user data from sessionStorage (persists during session):', { username: cachedUser.username, privilege: cachedUser.privilege }); return; } // JWT tokens are now stored in httpOnly cookies, so we fetch user data from API console.log('๐Ÿช JWT tokens are in httpOnly cookies, fetching user data from API'); // Determine the correct endpoint based on authentication authority const authAuthority = sessionStorage.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 fetchCurrentUserApi(request, authAuthority || undefined); setUser(data); // Cache user data in sessionStorage (cleared on tab close - more secure than localStorage) setUserDataCache(data); console.log('โœ… User data fetched from API and cached in sessionStorage (secure):', { username: data.username, privilege: data.privilege }); } catch (error: any) { console.error('โŒ Failed to fetch user data:', error); // Display debug information if this is a Microsoft/Google auth failure const authAuthority = sessionStorage.getItem('auth_authority'); if (authAuthority === 'msft' || authAuthority === 'google') { 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); clearUserDataCache(); // 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'); // Clear auth authority sessionStorage.removeItem('auth_authority'); clearUserDataCache(); // 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 logoutUserApi(request, user.authenticationAuthority); console.log('โœ… Logout API call completed, waiting for browser to process cookies...'); // CRITICAL: Wait for browser to process Set-Cookie headers from logout response // This gives the browser time to clear httpOnly cookies before redirect await new Promise(resolve => setTimeout(resolve, 1000)); // Clear user state after successful logout setUser(null); // CRITICAL: Clear all authentication data BEFORE any redirects // This ensures cleanup happens even if MSAL redirect interrupts the process console.log('๐Ÿงน Starting comprehensive cleanup...'); // Clear user data cache from sessionStorage clearUserDataCache(); // Clear auth authority from sessionStorage sessionStorage.removeItem('auth_authority'); // Clear MSAL cache tokens from localStorage // MSAL stores tokens with keys starting with 'msal.' const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && ( key.startsWith('msal.') || key === 'auth_token' || key === 'refresh_token' || key.includes('token') || key.includes('auth') || key.includes('msal') )) { keysToRemove.push(key); } } keysToRemove.forEach(key => { console.log('๐Ÿ—‘๏ธ Removing token:', key); localStorage.removeItem(key); }); // Clear ALL MSAL cache data (including account keys, token keys, version) const msalKeysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('msal.')) { msalKeysToRemove.push(key); } } msalKeysToRemove.forEach(key => { console.log('๐Ÿ—‘๏ธ Removing MSAL cache:', key); localStorage.removeItem(key); }); // Clear sessionStorage as well (CSRF tokens, etc.) sessionStorage.clear(); // Clear cookies as backup (in case backend doesn't clear them properly) // Note: This only works for cookies that are accessible to JavaScript console.log('๐Ÿช Checking cookies for cleanup...'); console.log('๐Ÿช All cookies:', document.cookie); const cookies = document.cookie.split(";"); console.log('๐Ÿช Cookie count:', cookies.length); cookies.forEach(function(c) { const cookieName = c.split("=")[0].trim(); console.log('๐Ÿช Checking cookie:', cookieName); if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) { console.log('๐Ÿ—‘๏ธ Clearing cookie:', cookieName); document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; } }); console.log('๐Ÿช Cookies after cleanup attempt:', document.cookie); console.log('โœ… Cleanup completed'); // Handle MSAL logout for Microsoft authentication if (user.authenticationAuthority === 'msft' && msalInstance) { try { console.log('๐Ÿ”„ Starting MSAL logout redirect...'); await msalInstance.logoutRedirect({ onRedirectNavigate: () => { console.log('๐Ÿ”„ MSAL redirect initiated - cleanup already completed'); return 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 console.log('๐Ÿ”„ Redirecting to login page...'); window.location.href = '/login'; } catch (error) { console.error('Logout failed:', error); throw error; } }; useEffect(() => { // Try to load user from sessionStorage cache first for faster initial load const cachedUser = getUserDataCache(); if (cachedUser) { setUser(cachedUser); console.log('โœ… Using cached user data from sessionStorage on mount (persists during session)'); } // For OAuth authentication, wait a bit longer before fetching user data const authAuthority = sessionStorage.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 }; } // Re-export AttributeOption for backward compatibility export interface AttributeOption { value: string | number; label: string | { [key: string]: string }; } // Organization users hook (list, update, delete) - following prompts/workflows pattern export function useOrgUsers() { const [users, setUsers] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { try { const response = await api.get('/api/attributes/User'); // Extract attributes from response let attrs: AttributeDefinition[] = []; if (response.data?.attributes && Array.isArray(response.data.attributes)) { attrs = response.data.attributes; } else if (Array.isArray(response.data)) { attrs = response.data; } else if (response.data && typeof response.data === 'object') { const keys = Object.keys(response.data); for (const key of keys) { if (Array.isArray(response.data[key])) { attrs = response.data[key]; break; } } } setAttributes(attrs); return attrs; } catch (error: any) { console.error('Error fetching attributes:', error); setAttributes([]); return []; } }, []); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'User'); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchUsers = useCallback(async (params?: PaginationParams) => { try { const requestParams: any = {}; // Build pagination object if provided if (params) { const paginationObj: any = {}; if (params.page !== undefined) paginationObj.page = params.page; if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); } } const data = await fetchUsersApi(request, params); // Handle paginated response if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; setUsers(items); if (data.pagination) { setPagination(data.pagination); } } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; setUsers(items); setPagination(null); } } catch (error: any) { // Error is already handled by useApiRequest setUsers([]); setPagination(null); } }, [request]); // Optimistically remove a user from the local state const removeOptimistically = (userId: string) => { setUsers(prevUsers => prevUsers.filter(user => user.id !== userId)); }; // Optimistically update a user in the local state const updateOptimistically = (userId: string, updateData: Partial) => { setUsers(prevUsers => prevUsers.map(user => user.id === userId ? { ...user, ...updateData } : user ) ); }; // Fetch a single user by ID const fetchUserById = useCallback(async (userId: string): Promise => { return await fetchUserByIdApi(request, userId); }, [request]); // Generate edit fields from attributes dynamically const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; editable?: boolean; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; // For options that need to be fetched (e.g., "user.role") }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { // Filter out non-editable fields based on readonly/editable flags if (attr.readonly === true || attr.editable === false) { return false; // Don't show readonly fields in edit form } // Also filter out common non-editable fields const nonEditableFields = ['id', 'mandateId', '_createdBy', '_hideDelete']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map backend attribute type to form field type let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; // Handle readonly fields if (attr.readonly === true || attr.editable === false) { fieldType = 'readonly'; } // Map backend types to form field types else if (attr.type === 'checkbox') { fieldType = 'boolean'; } else if (attr.type === 'email') { fieldType = 'email'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'select') { fieldType = 'enum'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attr.options; } } else if (attr.type === 'multiselect') { fieldType = 'multiselect'; // Handle options - can be array or string reference if (Array.isArray(attr.options)) { options = attr.options.map(opt => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attr.options === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attr.options; } } else if (attr.type === 'textarea') { fieldType = 'textarea'; } else if (attr.type === 'text') { // Check if it should be textarea based on name if (attr.name.toLowerCase().includes('description') || attr.name.toLowerCase().includes('content') || attr.name.toLowerCase().includes('note')) { fieldType = 'textarea'; } else { fieldType = 'string'; } } // Legacy support for old format else if (attr.type === 'boolean') { fieldType = 'boolean'; } else if (attr.type === 'enum' && attr.filterOptions) { fieldType = 'enum'; options = attr.filterOptions.map(opt => ({ value: opt, label: opt })); } // Define validators and required fields let required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; let minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; // Email validation if (fieldType === 'email') { validator = (value: string) => { if (required && (!value || value.trim() === '')) { return 'Email cannot be empty'; } if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { return 'Invalid email format'; } return null; }; } // Textarea settings else if (fieldType === 'textarea') { minRows = 4; maxRows = 8; if (attr.name.toLowerCase().includes('content')) { minRows = 6; maxRows = 12; } } // Multiselect validation else if (fieldType === 'multiselect' && required) { validator = (value: any[]) => { if (!value || !Array.isArray(value) || value.length === 0) { return `${attr.label} is required`; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: attr.editable !== false && attr.readonly !== true, required, validator, minRows, maxRows, options, optionsReference }; }); return editableFields; }, [attributes]); // Ensure attributes are loaded - can be called by EditActionButton const ensureAttributesLoaded = useCallback(async () => { if (attributes && attributes.length > 0) { return attributes; } const fetchedAttributes = await fetchAttributes(); return fetchedAttributes; }, [attributes, fetchAttributes]); // Fetch attributes and permissions on mount useEffect(() => { fetchAttributes(); fetchPermissions(); }, [fetchAttributes, fetchPermissions]); // Initial fetch useEffect(() => { fetchUsers(); }, [fetchUsers]); return { data: users, loading, error, refetch: fetchUsers, removeOptimistically, updateOptimistically, attributes, permissions, pagination, fetchUserById, generateEditFieldsFromAttributes, ensureAttributesLoaded }; } // User operations hook export function useUserOperations() { const [deletingUsers, setDeletingUsers] = useState>(new Set()); const [editingUsers, setEditingUsers] = useState>(new Set()); const [creatingUser, setCreatingUser] = useState(false); const { request, isLoading } = useApiRequest(); const [deleteError, setDeleteError] = useState(null); const [createError, setCreateError] = useState(null); const [updateError, setUpdateError] = useState(null); const handleUserDelete = async (userId: string) => { setDeleteError(null); setDeletingUsers(prev => new Set(prev).add(userId)); try { await deleteUserApi(request, userId); await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { setDeleteError(error.message); return false; } finally { setDeletingUsers(prev => { const newSet = new Set(prev); newSet.delete(userId); return newSet; }); } }; const handleUserCreate = async (userData: Omit & { password: string }) => { setCreateError(null); setCreatingUser(true); try { const currentUserData = getUserDataCache(); const mandateId = currentUserData?.mandateId || ''; const requestBody = { mandateId: mandateId, ...userData }; const newUser = await createUserApi(request, requestBody); return { success: true, userData: newUser }; } catch (error: any) { setCreateError(error.message); return { success: false, error: error.message }; } finally { setCreatingUser(false); } }; const handleUserUpdate = async (userId: string, updateData: UserUpdateData, originalData?: any) => { setUpdateError(null); setEditingUsers(prev => new Set(prev).add(userId)); try { const currentUserData = getUserDataCache(); const mandateId = currentUserData?.mandateId || ''; const requestBody = { mandateId: mandateId, ...updateData }; const updatedUser = await updateUserApi(request, userId, requestBody); return { success: true, userData: updatedUser }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update user'; const statusCode = error.response?.status; setUpdateError(errorMessage); return { success: false, error: errorMessage, statusCode, isPermissionError: statusCode === 403, isValidationError: statusCode === 400 }; } finally { setEditingUsers(prev => { const newSet = new Set(prev); newSet.delete(userId); return newSet; }); } }; return { deletingUsers, editingUsers, creatingUser, deleteError, createError, updateError, handleUserDelete, handleUserCreate, handleUserUpdate, isLoading }; } // 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 => { const user = await fetchUserByIdApi(request, userId); if (!user) { throw new Error('User not found'); } return user; }; const updateUser = async (userId: string, userData: UserUpdateData): Promise => { return await updateUserApi(request, userId, userData); }; const deleteUser = async (userId: string): Promise => { await deleteUserApi(request, userId); }; return { getUser, updateUser, deleteUser, isLoading, error }; }