import { useState, useEffect, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { ChevronRight, Search, Filter, UserPlus, History, ChevronLeft, MoreVertical, Lock, Verified, Wrench, Eye, HardDrive, Shield, ArrowRight, Network, ChevronRight as ChevronRightIcon, X, Edit, Trash2, User as UserIcon, Plus } from 'lucide-react' import { Button } from '@/components/ui/button' import { iamApi, type User, type Group } from '@/api/iam' export default function IAM() { const [activeTab, setActiveTab] = useState('users') const [searchQuery, setSearchQuery] = useState('') const [showCreateUserForm, setShowCreateUserForm] = useState(false) const [showEditUserForm, setShowEditUserForm] = useState(false) const [selectedUser, setSelectedUser] = useState(null) const [openActionMenu, setOpenActionMenu] = useState(null) const queryClient = useQueryClient() const { data: users, isLoading, error } = useQuery({ queryKey: ['iam-users'], queryFn: iamApi.listUsers, refetchOnWindowFocus: true, }) if (error) { console.error('Failed to load users:', error) } const filteredUsers = (users || []).filter((user: User) => user.username.toLowerCase().includes(searchQuery.toLowerCase()) || (user.full_name && user.full_name.toLowerCase().includes(searchQuery.toLowerCase())) || (user.email && user.email.toLowerCase().includes(searchQuery.toLowerCase())) || (user.roles && user.roles.some((r: string) => r.toLowerCase().includes(searchQuery.toLowerCase()))) || (user.groups && user.groups.some((g: string) => g.toLowerCase().includes(searchQuery.toLowerCase()))) ) const getRoleBadge = (roles: string[] | undefined) => { if (!roles || roles.length === 0) { return { bg: 'bg-slate-700', text: 'text-slate-300', border: 'border-slate-600', icon: Shield, Icon: Shield, label: 'No Role' } } // Use first role for display const role = roles[0] const roleConfig: Record = { 'admin': { bg: 'bg-purple-500/10', text: 'text-purple-400', border: 'border-purple-500/20', icon: Verified, label: 'Admin' }, 'operator': { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/20', icon: Wrench, label: 'Operator' }, 'auditor': { bg: 'bg-yellow-500/10', text: 'text-yellow-500', border: 'border-yellow-500/20', icon: Eye, label: 'Auditor' }, 'storage_admin': { bg: 'bg-teal-500/10', text: 'text-teal-500', border: 'border-teal-500/20', icon: HardDrive, label: 'Storage Admin' }, 'service': { bg: 'bg-slate-700', text: 'text-slate-300', border: 'border-slate-600', icon: Shield, label: 'Service' } } const config = roleConfig[role.toLowerCase()] || { bg: 'bg-slate-700', text: 'text-slate-300', border: 'border-slate-600', icon: Shield, label: role } const Icon = config.icon return { ...config, Icon } } const getAvatarBg = (username: string) => { if (username.toLowerCase() === 'admin') { return 'bg-gradient-to-br from-blue-500 to-indigo-600' } return 'bg-slate-700' } const deleteUserMutation = useMutation({ mutationFn: iamApi.deleteUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['iam-users'] }) queryClient.refetchQueries({ queryKey: ['iam-users'] }) }, onError: (error: any) => { console.error('Failed to delete user:', error) const errorMessage = error.response?.data?.error || error.message || 'Failed to delete user' alert(errorMessage) }, }) const handleDeleteUser = (userId: string) => { deleteUserMutation.mutate(userId) } const formatLastLogin = (lastLoginAt: string | null) => { if (!lastLoginAt) return 'Never' const date = new Date(lastLoginAt) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) if (diffMins < 1) return 'Just now' if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago` if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago` if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago` return date.toLocaleDateString() } return (
{/* Page Header */}

User & Access Management

Manage local accounts, define RBAC roles, and configure directory services (LDAP/AD) integration.

{/* Content Container */}
{/* Tabs */}
{/* Toolbar Area */} {activeTab === 'users' && ( <>
{/* Search & Filter */}
setSearchQuery(e.target.value)} className="w-full bg-card-dark border border-border-dark rounded-lg pl-10 pr-4 py-2.5 text-white placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm" />
{/* Primary Action */}
{/* Users Table */}
{isLoading ? ( ) : error ? ( ) : filteredUsers.length > 0 ? ( filteredUsers.map((user: User) => { const roleBadge = getRoleBadge(user.roles) const Icon = roleBadge.Icon const avatarInitials = user.full_name ? user.full_name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase() : user.username.substring(0, 2).toUpperCase() return ( ) }) ) : ( )}
Status Username Full Name Role Groups Last Login Actions
Loading users...
Error loading users: {error instanceof Error ? error.message : 'Unknown error'}
{user.is_active ? (
Active
) : (
Locked
)}
{avatarInitials}
{user.username}
{user.full_name || '-'} {user.roles && user.roles.length > 0 ? ( {roleBadge.label} ) : ( No role )} {user.groups && user.groups.length > 0 ? user.groups.join(', ') : '-'} {formatLastLogin(user.last_login_at)}
{openActionMenu === user.id && ( <>
setOpenActionMenu(null)} />
{!user.is_system && ( )}
)}
No users found
{/* Pagination */}
Showing 1-{filteredUsers.length} of{' '} {filteredUsers.length} users
)} {/* Create User Form Modal */} {showCreateUserForm && ( setShowCreateUserForm(false)} onSuccess={async () => { setShowCreateUserForm(false) // Invalidate and refetch users list immediately queryClient.invalidateQueries({ queryKey: ['iam-users'] }) await queryClient.refetchQueries({ queryKey: ['iam-users'] }) }} /> )} {/* Edit User Form Modal */} {showEditUserForm && selectedUser && ( { setShowEditUserForm(false) setSelectedUser(null) }} onSuccess={async () => { setShowEditUserForm(false) setSelectedUser(null) queryClient.invalidateQueries({ queryKey: ['iam-users'] }) await queryClient.refetchQueries({ queryKey: ['iam-users'] }) }} /> )} {/* Groups Tab */} {activeTab === 'groups' && } {/* Roles Tab */} {activeTab === 'roles' && } {activeTab !== 'users' && activeTab !== 'groups' && (
{activeTab === 'directory' && 'Directory Services tab coming soon'} {activeTab === 'auth' && 'Authentication & SSO tab coming soon'}
)} {/* Info Cards */}
{/* Directory Status */}

Directory Service

Inactive

No LDAP or Active Directory server is currently connected. Local authentication is being used.

{/* MFA Status */}

Security Policy

Good
Multi-Factor Auth Enforced
Password Rotation 90 Days
) } // Create User Form Component interface CreateUserFormProps { onClose: () => void onSuccess: () => void } function CreateUserForm({ onClose, onSuccess }: CreateUserFormProps) { const [username, setUsername] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [fullName, setFullName] = useState('') const createMutation = useMutation({ mutationFn: iamApi.createUser, onSuccess: async () => { // Wait a bit to ensure backend has processed the request await new Promise(resolve => setTimeout(resolve, 300)) onSuccess() }, onError: (error: any) => { console.error('Failed to create user:', error) const errorMessage = error.response?.data?.error || error.message || 'Failed to create user' alert(errorMessage) }, }) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!username.trim() || !email.trim() || !password.trim()) { alert('Username, email, and password are required') return } const userData = { username: username.trim(), email: email.trim(), password: password, full_name: fullName.trim() || undefined, } console.log('Creating user:', { ...userData, password: '***' }) createMutation.mutate(userData) } return (
{/* Modal Header */}

Create User

Create a new user account

{/* Modal Content */}
setUsername(e.target.value)} placeholder="johndoe" className="w-full px-4 py-3 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-colors" required />
setEmail(e.target.value)} placeholder="john.doe@example.com" className="w-full px-4 py-3 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-colors" required />
setPassword(e.target.value)} placeholder="Enter password" className="w-full px-4 py-3 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-colors" required minLength={8} />

Minimum 8 characters

setFullName(e.target.value)} placeholder="John Doe" className="w-full px-4 py-3 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-colors" />
{/* Action Buttons */}
) } // Edit User Form Component interface EditUserFormProps { user: User onClose: () => void onSuccess: () => void } function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) { const [email, setEmail] = useState(user.email || '') const [fullName, setFullName] = useState(user.full_name || '') const [isActive, setIsActive] = useState(user.is_active) const [userRoles, setUserRoles] = useState(user.roles || []) const [userGroups, setUserGroups] = useState(user.groups || []) const [selectedRole, setSelectedRole] = useState('') const [selectedGroup, setSelectedGroup] = useState('') const queryClient = useQueryClient() // Use refs to always get the latest state values const userRolesRef = useRef(userRoles) const userGroupsRef = useRef(userGroups) // Update refs whenever state changes useEffect(() => { userRolesRef.current = userRoles console.log('useEffect userRoles - state updated:', userRoles) }, [userRoles]) useEffect(() => { userGroupsRef.current = userGroups console.log('useEffect userGroups - state updated:', userGroups) }, [userGroups]) // Debug: log state changes useEffect(() => { console.log('EditUserForm - userRoles state changed:', userRoles) }, [userRoles]) useEffect(() => { console.log('EditUserForm - userGroups state changed:', userGroups) }, [userGroups]) // Fetch available roles and groups const { data: availableRoles = [] } = useQuery({ queryKey: ['iam-roles'], queryFn: iamApi.listRoles, }) const { data: availableGroups = [] } = useQuery({ queryKey: ['iam-groups'], queryFn: iamApi.listGroups, }) // Filter out already assigned roles/groups const unassignedRoles = availableRoles.filter(r => !userRoles.includes(r.name)) const unassignedGroups = availableGroups.filter(g => !userGroups.includes(g.name)) const updateMutation = useMutation({ mutationFn: (data: { email?: string; full_name?: string; is_active?: boolean; roles?: string[]; groups?: string[] }) => iamApi.updateUser(user.id, data), onSuccess: async () => { onSuccess() queryClient.invalidateQueries({ queryKey: ['iam-users'] }) await queryClient.refetchQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) await queryClient.refetchQueries({ queryKey: ['iam-user', user.id] }) }, onError: (error: any) => { console.error('Failed to update user:', error) const errorMessage = error.response?.data?.error || error.message || 'Failed to update user' alert(errorMessage) }, }) const assignRoleMutation = useMutation({ mutationFn: (roleName: string) => iamApi.assignRoleToUser(user.id, roleName), onMutate: async (roleName: string) => { // Optimistic update: add role to state immediately console.log('assignRoleMutation onMutate - BEFORE update, current userRoles:', userRolesRef.current) setUserRoles(prev => { const newRoles = prev.includes(roleName) ? prev : [...prev, roleName] console.log('assignRoleMutation onMutate - prev:', prev, 'roleName:', roleName, 'newRoles:', newRoles) // Also update ref immediately userRolesRef.current = newRoles return newRoles }) setSelectedRole('') console.log('assignRoleMutation onMutate - AFTER update, ref should be:', userRolesRef.current) }, onSuccess: async (_, roleName: string) => { // Don't overwrite state with server data - keep optimistic update // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) // Use functional update to get current state setUserRoles(current => { console.log('assignRoleMutation onSuccess - roleName:', roleName, 'current userRoles:', current) return current }) }, onError: (error: any, roleName: string) => { console.error('Failed to assign role:', error, roleName) // Rollback: remove role from state if API call failed setUserRoles(prev => prev.filter(r => r !== roleName)) alert(error.response?.data?.error || error.message || 'Failed to assign role') }, }) const removeRoleMutation = useMutation({ mutationFn: (roleName: string) => iamApi.removeRoleFromUser(user.id, roleName), onMutate: async (roleName: string) => { // Store previous state for rollback const previousRoles = userRoles // Optimistic update: remove role from state immediately setUserRoles(prev => prev.filter(r => r !== roleName)) return { previousRoles } }, onSuccess: async (_, roleName: string) => { // Don't overwrite state with server data - keep optimistic update // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) console.log('Role removed successfully:', roleName, 'Current userRoles:', userRoles) }, onError: (error: any, _roleName: string, context: any) => { console.error('Failed to remove role:', error) // Rollback: restore previous state if API call failed if (context?.previousRoles) { setUserRoles(context.previousRoles) } alert(error.response?.data?.error || error.message || 'Failed to remove role') }, }) const assignGroupMutation = useMutation({ mutationFn: (groupName: string) => iamApi.assignGroupToUser(user.id, groupName), onMutate: async (groupName: string) => { // Optimistic update: add group to state immediately console.log('assignGroupMutation onMutate - BEFORE update, current userGroups:', userGroupsRef.current) setUserGroups(prev => { const newGroups = prev.includes(groupName) ? prev : [...prev, groupName] console.log('assignGroupMutation onMutate - prev:', prev, 'groupName:', groupName, 'newGroups:', newGroups) // Also update ref immediately userGroupsRef.current = newGroups return newGroups }) setSelectedGroup('') console.log('assignGroupMutation onMutate - AFTER update, ref should be:', userGroupsRef.current) }, onSuccess: async (_, groupName: string) => { // Don't overwrite state with server data - keep optimistic update // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) // Use functional update to get current state setUserGroups(current => { console.log('assignGroupMutation onSuccess - groupName:', groupName, 'current userGroups:', current) return current }) }, onError: (error: any, groupName: string) => { console.error('Failed to assign group:', error, groupName) // Rollback: remove group from state if API call failed setUserGroups(prev => prev.filter(g => g !== groupName)) alert(error.response?.data?.error || error.message || 'Failed to assign group') }, }) const removeGroupMutation = useMutation({ mutationFn: (groupName: string) => iamApi.removeGroupFromUser(user.id, groupName), onMutate: async (groupName: string) => { // Store previous state for rollback const previousGroups = userGroups // Optimistic update: remove group from state immediately setUserGroups(prev => prev.filter(g => g !== groupName)) return { previousGroups } }, onSuccess: async (_, groupName: string) => { // Don't overwrite state with server data - keep optimistic update // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) console.log('Group removed successfully:', groupName, 'Current userGroups:', userGroups) }, onError: (error: any, _groupName: string, context: any) => { console.error('Failed to remove group:', error) // Rollback: restore previous state if API call failed if (context?.previousGroups) { setUserGroups(context.previousGroups) } alert(error.response?.data?.error || error.message || 'Failed to remove group') }, }) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() // Use refs to get the latest state values (avoid closure issues) const currentRoles = userRolesRef.current const currentGroups = userGroupsRef.current const payload = { email: email.trim(), full_name: fullName.trim() || undefined, is_active: isActive, roles: currentRoles, groups: currentGroups, } console.log('EditUserForm - Submitting payload:', payload) console.log('EditUserForm - currentRoles from ref:', currentRoles) console.log('EditUserForm - currentGroups from ref:', currentGroups) console.log('EditUserForm - userRoles from state:', userRoles) console.log('EditUserForm - userGroups from state:', userGroups) updateMutation.mutate(payload) } return (
{/* Modal Header */}

Edit User

Edit user account: {user.username}

{/* Modal Content */}

Username cannot be changed

setEmail(e.target.value)} placeholder="john.doe@example.com" className="w-full px-4 py-3 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-colors" required />
setFullName(e.target.value)} placeholder="John Doe" className="w-full px-4 py-3 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-colors" />
{/* Roles Section */}
{userRoles.length > 0 ? ( userRoles.map((role) => (
{role}
)) ) : (

No roles assigned

)}
{/* Groups Section */}
{userGroups.length > 0 ? ( userGroups.map((group) => (
{group}
)) ) : (

No groups assigned

)}

{isActive ? 'User can log in and access the system' : 'User account is disabled'}

{/* Action Buttons */}
) } // Groups Tab Component function GroupsTab() { const queryClient = useQueryClient() const [searchQuery, setSearchQuery] = useState('') const [showCreateForm, setShowCreateForm] = useState(false) const [showEditForm, setShowEditForm] = useState(false) const [selectedGroup, setSelectedGroup] = useState(null) const [openActionMenu, setOpenActionMenu] = useState(null) const { data: groups, isLoading } = useQuery({ queryKey: ['iam-groups'], queryFn: iamApi.listGroups, }) const filteredGroups = groups?.filter(group => group.name.toLowerCase().includes(searchQuery.toLowerCase()) || (group.description && group.description.toLowerCase().includes(searchQuery.toLowerCase())) ) || [] const deleteGroupMutation = useMutation({ mutationFn: iamApi.deleteGroup, onSuccess: async () => { queryClient.invalidateQueries({ queryKey: ['iam-groups'] }) await queryClient.refetchQueries({ queryKey: ['iam-groups'] }) queryClient.invalidateQueries({ queryKey: ['iam-users'] }) await queryClient.refetchQueries({ queryKey: ['iam-users'] }) alert('Group deleted successfully!') }, onError: (error: any) => { console.error('Failed to delete group:', error) alert(error.response?.data?.error || error.message || 'Failed to delete group') }, }) const handleDeleteGroup = (groupId: string, groupName: string) => { if (confirm(`Are you sure you want to delete group "${groupName}"? This action cannot be undone.`)) { deleteGroupMutation.mutate(groupId) } } return ( <> {/* Toolbar */}
setSearchQuery(e.target.value)} className="w-full bg-card-dark border border-border-dark rounded-lg pl-10 pr-4 py-2.5 text-white placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm" />
{/* Groups Table */}
{isLoading ? ( ) : filteredGroups.length > 0 ? ( filteredGroups.map((group) => ( )) ) : ( )}
Name Description Users Roles Type Actions
Loading groups...
{group.name} {group.description || '-'} {group.user_count} {group.role_count} {group.is_system ? ( System ) : ( Custom )}
{openActionMenu === group.id && ( <>
setOpenActionMenu(null)} />
)}
No groups found
{/* Pagination */}
Showing 1-{filteredGroups.length} of{' '} {filteredGroups.length} groups
{/* Create Group Form Modal */} {showCreateForm && ( setShowCreateForm(false)} onSuccess={async () => { setShowCreateForm(false) queryClient.invalidateQueries({ queryKey: ['iam-groups'] }) await queryClient.refetchQueries({ queryKey: ['iam-groups'] }) }} /> )} {/* Edit Group Form Modal */} {showEditForm && selectedGroup && ( { setShowEditForm(false) setSelectedGroup(null) }} onSuccess={async () => { setShowEditForm(false) setSelectedGroup(null) queryClient.invalidateQueries({ queryKey: ['iam-groups'] }) await queryClient.refetchQueries({ queryKey: ['iam-groups'] }) queryClient.invalidateQueries({ queryKey: ['iam-users'] }) await queryClient.refetchQueries({ queryKey: ['iam-users'] }) }} /> )} ) } interface CreateGroupFormProps { onClose: () => void onSuccess: () => void } function CreateGroupForm({ onClose, onSuccess }: CreateGroupFormProps) { const [name, setName] = useState('') const [description, setDescription] = useState('') const createMutation = useMutation({ mutationFn: iamApi.createGroup, onSuccess: () => { onSuccess() }, onError: (error: any) => { console.error('Failed to create group:', error) const errorMessage = error.response?.data?.error || error.message || 'Failed to create group' alert(errorMessage) }, }) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) { alert('Name is required') return } const groupData = { name: name.trim(), description: description.trim() || '', } console.log('Creating group:', groupData) createMutation.mutate(groupData) } return (
{/* Modal Header */}

Create Group

Create a new user group

{/* Modal Content */}
setName(e.target.value)} placeholder="operators" className="w-full px-4 py-3 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm placeholder-text-secondary/50 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-colors" required />