frontend_nyla/src/hooks/useUsers.ts

878 lines
No EOL
31 KiB
TypeScript

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,
sendPasswordLink as sendPasswordLinkApi,
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<User | null>(null);
const { request, isLoading, error } = useApiRequest<null, User>();
const fetchCurrentUser = async (retryCount = 0) => {
try {
// Check if we already have user data in sessionStorage cache
const cachedUser = getUserDataCache();
if (cachedUser && cachedUser.username) {
// Use cached user data - permissions are checked via RBAC API, not client-side
// Note: roleLabels is deprecated in Multi-Tenant architecture - use isSysAdmin/isPlatformAdmin flags instead
setUser(cachedUser);
console.log('✅ Using cached user data from sessionStorage (persists during session):', {
username: cachedUser.username,
isSysAdmin: cachedUser.isSysAdmin,
isPlatformAdmin: cachedUser.isPlatformAdmin
});
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);
// Log response for debugging
console.log('📦 User data received from API:', {
username: data?.username,
isSysAdmin: data?.isSysAdmin,
isPlatformAdmin: data?.isPlatformAdmin,
allKeys: data ? Object.keys(data) : []
});
// Validate user data
if (!data || !data.username) {
console.error('❌ User data from API is invalid:', {
username: data?.username,
dataKeys: data ? Object.keys(data) : []
});
throw new Error('Invalid user data received from API');
}
// Cache user data (permissions are checked via RBAC API)
// Note: roleLabels is deprecated - use isSysAdmin/isPlatformAdmin flags for admin checks
setUserDataCache(data);
console.log('✅ User data fetched from API and cached:', {
username: data.username,
isSysAdmin: data.isSysAdmin,
isPlatformAdmin: data.isPlatformAdmin
});
setUser(data);
} 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 () => {
if (!user) {
throw new Error('No user to logout');
}
try {
// Determine the correct logout endpoint based on authentication authority
// Note: logoutEndpoint is determined by logoutUserApi based on authenticationAuthority
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);
// Clear client-side auth hints; gateway session ended via API (cookies cleared by backend).
console.log('🧹 Starting comprehensive cleanup...');
// Clear user data cache from sessionStorage
clearUserDataCache();
// Clear auth authority from sessionStorage
sessionStorage.removeItem('auth_authority');
// Optional: clear MSAL browser cache only (PowerOn JWT lives in httpOnly cookies + backend).
// Do not call msal.logoutRedirect — that signs the user out of Microsoft globally.
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key && key.startsWith('msal.')) {
localStorage.removeItem(key);
}
}
// 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');
// 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 && cachedUser.username) {
// Use cached user data - permissions are checked via RBAC API
// Note: roleLabels is deprecated in Multi-Tenant architecture - use isSysAdmin/isPlatformAdmin flags instead
setUser(cachedUser);
console.log('✅ Using cached user data from sessionStorage on mount:', {
username: cachedUser.username,
isSysAdmin: cachedUser.isSysAdmin,
isPlatformAdmin: cachedUser.isPlatformAdmin
});
}
// 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;
}
// Organization users hook (list, update, delete) - following prompts/workflows pattern
export function useOrgUsers() {
const [users, setUsers] = useState<User[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const [pagination, setPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, User[]>();
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) {
// Don't log 429 errors as errors (they're rate limit warnings)
if (error.response?.status === 429) {
console.warn('Rate limit exceeded while fetching user attributes. Please wait.');
} else if (error.response?.status !== 401) {
// Only log non-auth errors (401 is expected when not logged in)
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<User>) => {
setUsers(prevUsers =>
prevUsers.map(user =>
user.id === userId
? { ...user, ...updateData }
: user
)
);
};
// Fetch a single user by ID
const fetchUserById = useCallback(async (userId: string): Promise<User | null> => {
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', 'sysCreatedBy', '_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 => ({
value: opt.value,
label: opt.label || String(opt.value)
}));
} 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 => ({
value: opt.value,
label: opt.label || String(opt.value)
}));
} 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';
}
}
// Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union
// If needed, they should be handled via type casting: (attr as any).type === 'boolean'
// 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: any) => {
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
return 'Email cannot be empty';
}
if (value && typeof value === 'string' && !/^[^\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 as any).editable !== false && (attr as any).readonly !== true,
required,
validator,
minRows,
maxRows,
options,
optionsReference
};
});
return editableFields;
}, [attributes]);
// Generate create fields from attributes dynamically
// For users, we add a password field that's not in the backend attributes (since passwords are hashed)
const generateCreateFieldsFromAttributes = useCallback((): Array<{
key: string;
label: string;
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
required?: boolean;
validator?: (value: any) => string | null;
minRows?: number;
maxRows?: number;
options?: Array<{ value: string | number; label: string }>;
optionsReference?: string;
placeholder?: string;
}> => {
if (!attributes || attributes.length === 0) {
return [];
}
const createFields = attributes
.filter(attr => {
// Filter out non-editable fields and auto-generated fields for create forms
if (attr.readonly === true || attr.editable === false) {
return false;
}
// Filter out ID fields and other auto-generated fields
const nonEditableFields = ['id', 'mandateId', 'sysCreatedBy', '_hideDelete', 'authenticationAuthority'];
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;
// Map backend types to form field types
// Cast to string to handle all possible backend type values
const attrType = attr.type as string;
if (attrType === 'checkbox' || attrType === 'boolean') {
fieldType = 'boolean';
} else if (attrType === 'email') {
fieldType = 'email';
} else if (attrType === 'timestamp' || attrType === 'date' || attrType === 'time') {
fieldType = 'date';
} else if (attrType === 'select' || attrType === 'enum') {
fieldType = 'enum';
if (Array.isArray(attr.options)) {
options = attr.options.map(opt => {
return {
value: opt.value,
label: opt.label || String(opt.value)
};
});
} else if (typeof attr.options === 'string') {
optionsReference = attr.options;
}
} else if (attrType === 'multiselect') {
fieldType = 'multiselect';
if (Array.isArray(attr.options)) {
options = attr.options.map(opt => ({
value: opt.value,
label: opt.label || String(opt.value)
}));
} else if (typeof attr.options === 'string') {
optionsReference = attr.options;
}
} else if (attrType === 'textarea') {
fieldType = 'textarea';
}
// Determine if required and build validator
const required = attr.required === true;
let validator: ((value: any) => string | null) | undefined = undefined;
// Email validation
if (attr.type === 'email') {
validator = (value: any) => {
if (required && (!value || (typeof value === 'string' && value.trim() === ''))) {
return 'Email cannot be empty';
}
if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Invalid email format';
}
return null;
};
}
// Required string validation
else if (required && fieldType === 'string') {
validator = (value: any) => {
if (!value || (typeof value === 'string' && value.trim() === '')) {
return `${attr.label} is required`;
}
return null;
};
}
// 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,
required,
validator,
options,
optionsReference
};
});
// Note: Password field removed - users are created without password
// Admin can send password setup link after user creation using handleSendPasswordLink
return createFields;
}, [attributes]);
// Ensure attributes are loaded - can be called by EditActionButton
const ensureAttributesLoaded = useCallback(async () => {
// Don't fetch attributes if user is not authenticated (prevents 401 errors)
const currentUser = getUserDataCache();
if (!currentUser) {
return [];
}
if (attributes && attributes.length > 0) {
return attributes;
}
const fetchedAttributes = await fetchAttributes();
return fetchedAttributes;
}, [attributes, fetchAttributes]);
// Fetch attributes and permissions on mount (only if user is authenticated)
useEffect(() => {
const currentUser = getUserDataCache();
if (currentUser) {
fetchAttributes();
fetchPermissions();
}
}, [fetchAttributes, fetchPermissions]);
// Initial fetch
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
return {
data: users,
loading,
error,
refetch: fetchUsers,
removeOptimistically,
updateOptimistically,
attributes,
permissions,
pagination,
fetchUserById,
generateEditFieldsFromAttributes,
generateCreateFieldsFromAttributes,
ensureAttributesLoaded
};
}
// User operations hook
export function useUserOperations() {
const [deletingUsers, setDeletingUsers] = useState<Set<string>>(new Set());
const [editingUsers, setEditingUsers] = useState<Set<string>>(new Set());
const [sendingPasswordLink, setSendingPasswordLink] = useState<Set<string>>(new Set());
const [creatingUser, setCreatingUser] = useState(false);
const { request, isLoading } = useApiRequest();
const [deleteError, setDeleteError] = useState<string | null>(null);
const [createError, setCreateError] = useState<string | null>(null);
const [updateError, setUpdateError] = useState<string | null>(null);
const [passwordLinkError, setPasswordLinkError] = useState<string | null>(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<User, 'id'>) => {
setCreateError(null);
setCreatingUser(true);
try {
// mandateId wird nicht mehr vom Client gesendet
// Das Backend bestimmt den Kontext über die instanceId im Request
const newUser = await createUserApi(request, userData);
return { success: true, userData: newUser };
} catch (error: any) {
setCreateError(error.message);
return { success: false, error: error.message };
} finally {
setCreatingUser(false);
}
};
// Send password setup link to a user (admin function)
const handleSendPasswordLink = async (userId: string) => {
setPasswordLinkError(null);
setSendingPasswordLink(prev => new Set(prev).add(userId));
try {
// Get frontend URL from current window location
const frontendUrl = `${window.location.protocol}//${window.location.host}`;
const result = await sendPasswordLinkApi(request, userId, frontendUrl);
return { success: true, message: result.message, email: result.email };
} catch (error: any) {
const errorMessage = error.response?.data?.detail || error.message || 'Failed to send password link';
setPasswordLinkError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setSendingPasswordLink(prev => {
const newSet = new Set(prev);
newSet.delete(userId);
return newSet;
});
}
};
const handleUserUpdate = async (userId: string, updateData: UserUpdateData, _originalData?: any) => {
setUpdateError(null);
setEditingUsers(prev => new Set(prev).add(userId));
try {
// mandateId wird nicht mehr vom Client gesendet
const updatedUser = await updateUserApi(request, userId, updateData);
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;
});
}
};
// Generic inline update handler for FormGeneratorTable.
//
// The User PUT endpoint accepts PARTIAL payloads — only fields explicitly
// present are applied; missing fields keep their stored value. We therefore
// forward ONLY the changed cells. This avoids two classes of bugs:
// 1. Stale snapshot: spreading ``existingRow`` onto the payload would
// overwrite fields with whatever the client last loaded, even if the
// backend has been updated since (e.g. by a parallel admin action).
// 2. Missing-field default-flip: previously, any non-listed field (e.g.
// ``isSysAdmin`` while toggling ``isPlatformAdmin``) was absent from
// the merged payload and the Pydantic ``User`` body on the backend
// filled it with ``False``, silently dropping the other privileged flag.
//
// ``existingRow`` is kept in the signature for forward-compat with table
// hooks but is no longer consulted to build the payload.
const handleInlineUpdate = async (userId: string, changes: Partial<UserUpdateData>, _existingRow?: any) => {
if (!changes || Object.keys(changes).length === 0) {
throw new Error('No fields to update');
}
const result = await handleUserUpdate(userId, changes);
if (!result.success) {
throw new Error(result.error || 'Failed to update');
}
return result;
};
return {
deletingUsers,
editingUsers,
sendingPasswordLink,
creatingUser,
deleteError,
createError,
updateError,
passwordLinkError,
handleUserDelete,
handleUserCreate,
handleUserUpdate,
handleInlineUpdate,
handleSendPasswordLink,
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<User> => {
const user = await fetchUserByIdApi(request, userId);
if (!user) {
throw new Error('User not found');
}
return user;
};
const updateUser = async (userId: string, userData: UserUpdateData): Promise<User> => {
return await updateUserApi(request, userId, userData);
};
const deleteUser = async (userId: string): Promise<void> => {
await deleteUserApi(request, userId);
};
return {
getUser,
updateUser,
deleteUser,
isLoading,
error
};
}