Spaces:
Build error
Build error
| 'use client' | |
| import { useCallback, useEffect, useState } from 'react' | |
| import { useCurrentUser } from '@/lib/hooks/usePermissions' | |
| import { broadcastPermissionsRefresh, usePermissions } from '@/lib/hooks/usePermissions' | |
| interface Permission { | |
| id: string | |
| resource: string | |
| action: string | |
| description: string | null | |
| } | |
| interface Role { | |
| id: string | |
| name: string | |
| description: string | null | |
| } | |
| interface User { | |
| id: string | |
| name: string | |
| email: string | |
| isActive: boolean | |
| role: Role | null | |
| roleId: string | null | |
| createdAt: string | |
| } | |
| export default function UserManagementClient() { | |
| const [users, setUsers] = useState<User[]>([]) | |
| const [roles, setRoles] = useState<Role[]>([]) | |
| const [loading, setLoading] = useState(true) | |
| const [error, setError] = useState<string | null>(null) | |
| const [showCreateModal, setShowCreateModal] = useState(false) | |
| const [newUserName, setNewUserName] = useState('') | |
| const [newUserEmail, setNewUserEmail] = useState('') | |
| const [newUserRoleId, setNewUserRoleId] = useState('') | |
| const [newUserPassword, setNewUserPassword] = useState('') | |
| const [newUserPasswordConfirm, setNewUserPasswordConfirm] = useState('') | |
| // Bulk role assignment | |
| const [selectedUsers, setSelectedUsers] = useState<string[]>([]) | |
| const [bulkRoleId, setBulkRoleId] = useState<string>('') | |
| const { userId } = useCurrentUser() | |
| const { hasPermission, loading: permsLoading } = usePermissions() | |
| const fetchUsers = useCallback(async () => { | |
| if (!userId) return | |
| try { | |
| const res = await fetch('/api/users', { | |
| headers: { | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| }) | |
| if (!res.ok) throw new Error('Failed to fetch users') | |
| const data = await res.json() | |
| setUsers(data) | |
| setLoading(false) | |
| } catch (err: any) { | |
| setError(err.message) | |
| setLoading(false) | |
| } | |
| }, [userId]) | |
| const fetchRoles = useCallback(async () => { | |
| if (!userId) return | |
| try { | |
| const res = await fetch('/api/roles', { | |
| headers: { | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| }) | |
| if (!res.ok) throw new Error('Failed to fetch roles') | |
| const data = await res.json() | |
| setRoles(data) | |
| } catch (err: any) { | |
| console.error('Error fetching roles:', err) | |
| } | |
| }, [userId]) | |
| useEffect(() => { | |
| if (!userId) return | |
| fetchUsers() | |
| fetchRoles() | |
| }, [fetchRoles, fetchUsers, userId]) | |
| const handleToggleUserStatus = async (user: User) => { | |
| try { | |
| const res = await fetch(`/api/users/${user.id}`, { | |
| method: 'PATCH', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| body: JSON.stringify({ | |
| isActive: !user.isActive, | |
| }), | |
| }) | |
| if (!res.ok) throw new Error('Failed to update user status') | |
| await fetchUsers() | |
| broadcastPermissionsRefresh() | |
| } catch (err: any) { | |
| alert('Error updating user: ' + err.message) | |
| } | |
| } | |
| const toggleUserSelection = (userId: string) => { | |
| setSelectedUsers(prev => | |
| prev.includes(userId) | |
| ? prev.filter(id => id !== userId) | |
| : [...prev, userId] | |
| ) | |
| } | |
| const toggleSelectAll = () => { | |
| if (selectedUsers.length === users.length) { | |
| setSelectedUsers([]) | |
| } else { | |
| setSelectedUsers(users.map(u => u.id)) | |
| } | |
| } | |
| const handleBulkAssignRole = async () => { | |
| if (selectedUsers.length === 0) { | |
| alert('Please select at least one user') | |
| return | |
| } | |
| if (!bulkRoleId) { | |
| alert('Please select a role to assign') | |
| return | |
| } | |
| try { | |
| // Update all selected users | |
| await Promise.all( | |
| selectedUsers.map(selectedUserId => | |
| fetch(`/api/users/${selectedUserId}`, { | |
| method: 'PATCH', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| body: JSON.stringify({ roleId: bulkRoleId }), | |
| }) | |
| ) | |
| ) | |
| setSelectedUsers([]) | |
| setBulkRoleId('') | |
| await fetchUsers() | |
| broadcastPermissionsRefresh() | |
| alert(`Successfully assigned role to ${selectedUsers.length} user(s)`) | |
| } catch (err: any) { | |
| alert('Error assigning roles: ' + err.message) | |
| } | |
| } | |
| const handleCreateUser = async () => { | |
| if (!newUserName || !newUserEmail) { | |
| alert('Please enter name and email') | |
| return | |
| } | |
| const wantsToSetPassword = Boolean(newUserPassword || newUserPasswordConfirm) | |
| const canManagePasswords = !permsLoading && hasPermission('users', 'manage') | |
| if (wantsToSetPassword && !canManagePasswords) { | |
| alert('You do not have permission to set passwords. Ask an admin to set it after the user is created.') | |
| return | |
| } | |
| if (wantsToSetPassword) { | |
| if (newUserPassword.length < 8) { | |
| alert('Password must be at least 8 characters') | |
| return | |
| } | |
| if (newUserPassword !== newUserPasswordConfirm) { | |
| alert('Passwords do not match') | |
| return | |
| } | |
| } | |
| try { | |
| const res = await fetch('/api/users', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| body: JSON.stringify({ | |
| name: newUserName, | |
| email: newUserEmail, | |
| roleId: newUserRoleId || null, | |
| }), | |
| }) | |
| if (!res.ok) { | |
| const data = await res.json() | |
| throw new Error(data.error || 'Failed to create user') | |
| } | |
| const data = await res.json().catch(() => ({} as any)) | |
| const createdUser: User | undefined = data?.user | |
| if (wantsToSetPassword && createdUser?.id) { | |
| const pwRes = await fetch(`/api/users/${createdUser.id}/password`, { | |
| method: 'PATCH', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| body: JSON.stringify({ password: newUserPassword }), | |
| }) | |
| const pwData = await pwRes.json().catch(() => ({})) | |
| if (!pwRes.ok) { | |
| throw new Error(pwData?.error || 'User created, but failed to set password') | |
| } | |
| } | |
| setShowCreateModal(false) | |
| setNewUserName('') | |
| setNewUserEmail('') | |
| setNewUserRoleId('') | |
| setNewUserPassword('') | |
| setNewUserPasswordConfirm('') | |
| await fetchUsers() | |
| broadcastPermissionsRefresh() | |
| alert(wantsToSetPassword ? 'User created and password set successfully!' : 'User created successfully!') | |
| } catch (err: any) { | |
| alert('Error creating user: ' + err.message) | |
| } | |
| } | |
| const handleDeleteUser = async (user: User) => { | |
| if (!confirm(`Are you sure you want to delete ${user.name}? This action cannot be undone.`)) { | |
| return | |
| } | |
| try { | |
| const res = await fetch(`/api/users/${user.id}`, { | |
| method: 'DELETE', | |
| headers: { | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| }) | |
| if (!res.ok) { | |
| const data = await res.json() | |
| throw new Error(data.error || 'Failed to delete user') | |
| } | |
| await fetchUsers() | |
| broadcastPermissionsRefresh() | |
| alert('User deleted successfully!') | |
| } catch (err: any) { | |
| alert('Error deleting user: ' + err.message) | |
| } | |
| } | |
| const handleSetUserPassword = async (user: User) => { | |
| if (!userId) { | |
| alert('Missing user session. Please sign in again.') | |
| return | |
| } | |
| const pw1 = prompt(`Set a new password for ${user.email} (min 8 chars):`) || '' | |
| if (!pw1) return | |
| if (pw1.length < 8) { | |
| alert('Password must be at least 8 characters') | |
| return | |
| } | |
| const pw2 = prompt('Confirm new password:') || '' | |
| if (pw1 !== pw2) { | |
| alert('Passwords do not match') | |
| return | |
| } | |
| try { | |
| const res = await fetch(`/api/users/${user.id}/password`, { | |
| method: 'PATCH', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-user-id': userId || '', | |
| }, | |
| cache: 'no-store', | |
| body: JSON.stringify({ password: pw1 }), | |
| }) | |
| const data = await res.json().catch(() => ({})) | |
| if (!res.ok) { | |
| alert(data?.error || 'Failed to set password') | |
| return | |
| } | |
| alert('Password set successfully') | |
| } catch (err: any) { | |
| alert('Error setting password: ' + (err?.message || String(err))) | |
| } | |
| } | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center justify-center min-h-screen"> | |
| <div className="text-lg">Loading users...</div> | |
| </div> | |
| ) | |
| } | |
| if (error) { | |
| return ( | |
| <div className="flex items-center justify-center min-h-screen"> | |
| <div className="text-red-600">Error: {error}</div> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="p-8"> | |
| <div className="flex justify-between items-center mb-6"> | |
| <h1 className="text-3xl font-bold">User Management</h1> | |
| <button | |
| onClick={() => setShowCreateModal(true)} | |
| className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium" | |
| > | |
| + Add New User | |
| </button> | |
| </div> | |
| {/* Bulk Role Assignment */} | |
| {selectedUsers.length > 0 && ( | |
| <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6 flex items-center gap-4"> | |
| <span className="font-medium text-blue-900"> | |
| {selectedUsers.length} user(s) selected | |
| </span> | |
| <select | |
| value={bulkRoleId} | |
| onChange={(e) => setBulkRoleId(e.target.value)} | |
| className="border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| > | |
| <option value="">Select a role to assign</option> | |
| {roles.map((role) => ( | |
| <option key={role.id} value={role.id}> | |
| {role.name} | |
| </option> | |
| ))} | |
| </select> | |
| <button | |
| onClick={handleBulkAssignRole} | |
| disabled={!bulkRoleId} | |
| className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed" | |
| > | |
| Assign Role | |
| </button> | |
| <button | |
| onClick={() => setSelectedUsers([])} | |
| className="px-4 py-2 text-gray-600 hover:text-gray-800" | |
| > | |
| Clear Selection | |
| </button> | |
| </div> | |
| )} | |
| <div className="bg-white shadow rounded-lg overflow-hidden"> | |
| <table className="min-w-full divide-y divide-gray-200"> | |
| <thead className="bg-gray-50"> | |
| <tr> | |
| <th className="px-6 py-3 text-left"> | |
| <input | |
| type="checkbox" | |
| checked={selectedUsers.length === users.length && users.length > 0} | |
| onChange={toggleSelectAll} | |
| className="w-4 h-4 rounded border-gray-300 focus:ring-2 focus:ring-blue-500 cursor-pointer" | |
| /> | |
| </th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
| User | |
| </th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
| </th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
| Role | |
| </th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
| Status | |
| </th> | |
| <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> | |
| Actions | |
| </th> | |
| </tr> | |
| </thead> | |
| <tbody className="bg-white divide-y divide-gray-200"> | |
| {users.map((user) => ( | |
| <tr | |
| key={user.id} | |
| className={selectedUsers.includes(user.id) ? 'bg-blue-50' : 'hover:bg-gray-50'} | |
| > | |
| <td className="px-6 py-4 whitespace-nowrap"> | |
| <input | |
| type="checkbox" | |
| checked={selectedUsers.includes(user.id)} | |
| onChange={() => toggleUserSelection(user.id)} | |
| className="w-4 h-4 rounded border-gray-300 focus:ring-2 focus:ring-blue-500 cursor-pointer" | |
| /> | |
| </td> | |
| <td className="px-6 py-4 whitespace-nowrap"> | |
| <div className="text-sm font-medium text-gray-900">{user.name}</div> | |
| </td> | |
| <td className="px-6 py-4 whitespace-nowrap"> | |
| <div className="text-sm text-gray-500">{user.email}</div> | |
| </td> | |
| <td className="px-6 py-4 whitespace-nowrap"> | |
| <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"> | |
| {user.role?.name || 'No Role'} | |
| </span> | |
| </td> | |
| <td className="px-6 py-4 whitespace-nowrap"> | |
| <span | |
| className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${ | |
| user.isActive | |
| ? 'bg-green-100 text-green-800' | |
| : 'bg-red-100 text-red-800' | |
| }`} | |
| > | |
| {user.isActive ? 'Active' : 'Inactive'} | |
| </span> | |
| </td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> | |
| {!permsLoading && hasPermission('users', 'manage') && ( | |
| <button | |
| onClick={() => handleSetUserPassword(user)} | |
| className="text-blue-600 hover:text-blue-900 mr-4" | |
| > | |
| Set Password | |
| </button> | |
| )} | |
| <button | |
| onClick={() => handleToggleUserStatus(user)} | |
| className={ | |
| user.isActive | |
| ? 'text-orange-600 hover:text-orange-900 mr-4' | |
| : 'text-green-600 hover:text-green-900 mr-4' | |
| } | |
| > | |
| {user.isActive ? 'Deactivate' : 'Activate'} | |
| </button> | |
| <button | |
| onClick={() => handleDeleteUser(user)} | |
| className="text-red-600 hover:text-red-900" | |
| > | |
| Delete | |
| </button> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| {/* Create User Modal */} | |
| {showCreateModal && ( | |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> | |
| <div className="bg-white rounded-lg p-8 max-w-md w-full"> | |
| <h2 className="text-2xl font-bold mb-6">Add New User</h2> | |
| <div className="space-y-4 mb-6"> | |
| <div> | |
| <label className="block text-sm font-medium mb-2">Full Name *</label> | |
| <input | |
| type="text" | |
| value={newUserName} | |
| onChange={(e) => setNewUserName(e.target.value)} | |
| placeholder="John Doe" | |
| className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium mb-2">Email Address *</label> | |
| <input | |
| type="email" | |
| value={newUserEmail} | |
| onChange={(e) => setNewUserEmail(e.target.value)} | |
| placeholder="user@company.com" | |
| className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium mb-2">Assign Role</label> | |
| <select | |
| value={newUserRoleId} | |
| onChange={(e) => setNewUserRoleId(e.target.value)} | |
| className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| > | |
| <option value="">No Role (Assign Later)</option> | |
| {roles.map((role) => ( | |
| <option key={role.id} value={role.id}> | |
| {role.name} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| {!permsLoading && hasPermission('users', 'manage') && ( | |
| <> | |
| <div> | |
| <label className="block text-sm font-medium mb-2">Set Password (optional)</label> | |
| <input | |
| type="password" | |
| value={newUserPassword} | |
| onChange={(e) => setNewUserPassword(e.target.value)} | |
| placeholder="Minimum 8 characters" | |
| className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium mb-2">Confirm Password</label> | |
| <input | |
| type="password" | |
| value={newUserPasswordConfirm} | |
| onChange={(e) => setNewUserPasswordConfirm(e.target.value)} | |
| placeholder="Re-enter password" | |
| className="w-full border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| /> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| <div className="flex justify-end gap-3"> | |
| <button | |
| onClick={() => { | |
| setShowCreateModal(false) | |
| setNewUserName('') | |
| setNewUserEmail('') | |
| setNewUserRoleId('') | |
| setNewUserPassword('') | |
| setNewUserPasswordConfirm('') | |
| }} | |
| className="px-4 py-2 border rounded hover:bg-gray-50" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={handleCreateUser} | |
| className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" | |
| > | |
| Create User | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |