'use client'; import React, { useState, useEffect, useMemo } from 'react'; import { getLoginUrl } from '@/lib/config/storage'; import { Users, Plus, Search, MoreHorizontal, UserCheck, UserX, Trash2, Pencil, ChevronRight, ChevronDown, HardDrive, Eye, EyeOff } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { toast } from 'sonner'; import { logger } from '@/lib/utils'; interface WorkspaceInfo { id: string; name: string; owner_id: string; max_projects: number; max_deployments: number; max_storage_mb: number; role: string; created_at: string; updated_at: string; } interface UserInfo { id: string; email: string; displayName: string | null; isAdmin: boolean; active: boolean; workspaces: WorkspaceInfo[]; projectCount: number; storageMb: number; lastActive: string | null; createdAt: string; updatedAt: string; } export function UsersView() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [editUser, setEditUser] = useState(null); const [showEditDialog, setShowEditDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false); const [saving, setSaving] = useState(false); // Edit form state const [editForm, setEditForm] = useState({ displayName: '', active: true, }); // Create form state const [createForm, setCreateForm] = useState({ email: '', password: '', displayName: '', workspaceAssignment: 'new' as 'new' | 'existing' | 'none', workspaceId: '', }); const [showPassword, setShowPassword] = useState(false); const [availableWorkspaces, setAvailableWorkspaces] = useState>([]); // Expandable workspace detail state const [expandedUserId, setExpandedUserId] = useState(null); const isServerMode = process.env.NEXT_PUBLIC_SERVER_MODE === 'true'; const isManagedMode = !!process.env.NEXT_PUBLIC_GATEWAY_URL; useEffect(() => { loadUsers(); }, []); useEffect(() => { if (showCreateDialog) { fetch('/api/admin/workspaces') .then(res => res.ok ? res.json() : null) .then(data => { if (data?.workspaces) { setAvailableWorkspaces(data.workspaces.map((w: { id: string; name: string }) => ({ id: w.id, name: w.name }))); } }) .catch(() => {}); } }, [showCreateDialog]); const loadUsers = async () => { setLoading(true); try { const res = await fetch('/api/admin/users'); if (res.status === 401) { window.location.href = getLoginUrl(); return; } if (!res.ok) throw new Error('Failed to load users'); const data = await res.json(); setUsers(data.users); } catch (err) { logger.error('[UsersView] Failed to load users:', err); toast.error('Failed to load users'); } finally { setLoading(false); } }; const handleOpenEdit = (user: UserInfo) => { setEditUser(user); setEditForm({ displayName: user.displayName || '', active: user.active, }); setShowEditDialog(true); }; const handleSaveEdit = async () => { if (!editUser) return; setSaving(true); try { const res = await fetch(`/api/admin/users/${editUser.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: editForm.displayName || undefined, active: editForm.active, }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to update user'); } toast.success('User updated'); setShowEditDialog(false); setEditUser(null); await loadUsers(); } catch (err) { logger.error('[UsersView] Failed to update user:', err); toast.error(err instanceof Error ? err.message : 'Failed to update user'); } finally { setSaving(false); } }; const handleToggleActive = async (user: UserInfo) => { try { const res = await fetch(`/api/admin/users/${user.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ active: !user.active }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to update user'); } toast.success(user.active ? 'User deactivated' : 'User reactivated'); await loadUsers(); } catch (err) { logger.error('[UsersView] Failed to toggle user status:', err); toast.error(err instanceof Error ? err.message : 'Failed to update user'); } }; const handleDelete = async (user: UserInfo) => { if (!confirm(`Deactivate user "${user.email}"? This will soft-delete their account.`)) { return; } try { const res = await fetch(`/api/admin/users/${user.id}`, { method: 'DELETE', }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to delete user'); } toast.success('User deactivated'); await loadUsers(); } catch (err) { logger.error('[UsersView] Failed to delete user:', err); toast.error(err instanceof Error ? err.message : 'Failed to delete user'); } }; const handleCreate = async () => { if (!createForm.email || !createForm.password) { toast.error('Email and password are required'); return; } setSaving(true); try { const res = await fetch('/api/admin/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: createForm.email, password: createForm.password, displayName: createForm.displayName || undefined, workspaceAssignment: createForm.workspaceAssignment, workspaceId: createForm.workspaceId || undefined, }), }); if (!res.ok) { const error = await res.json(); throw new Error(error.error || 'Failed to create user'); } toast.success('User created'); setShowCreateDialog(false); setCreateForm({ email: '', password: '', displayName: '', workspaceAssignment: 'new', workspaceId: '' }); setShowPassword(false); await loadUsers(); } catch (err) { logger.error('[UsersView] Failed to create user:', err); toast.error(err instanceof Error ? err.message : 'Failed to create user'); } finally { setSaving(false); } }; const handleToggleExpand = (userId: string) => { setExpandedUserId(expandedUserId === userId ? null : userId); }; const filteredUsers = useMemo(() => { if (!searchQuery) return users; const query = searchQuery.toLowerCase(); return users.filter( user => user.email.toLowerCase().includes(query) || user.displayName?.toLowerCase().includes(query) ); }, [users, searchQuery]); const formatDate = (dateStr: string) => { try { return new Date(dateStr).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); } catch { return dateStr; } }; const roleBadgeVariant = (role: string): 'default' | 'secondary' | 'destructive' | 'outline' => { switch (role) { case 'owner': return 'default'; case 'editor': return 'secondary'; default: return 'outline'; } }; if (!isServerMode) { return (

User management is only available in Server Mode

); } if (loading) { return (

Loading users...

); } return ( <>
{/* Toolbar */}
{/* Search */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Controls */}
{isManagedMode ? (

Users are managed externally

) : ( )}
{/* User List */}
{filteredUsers.length === 0 ? (
{users.length === 0 ? ( <>

No Users Yet

Create your first user by clicking the "New User" button above.

) : ( <>

No users found

Try adjusting your search criteria

)}
) : (
{filteredUsers.map((user) => { const isExpanded = expandedUserId === user.id; return (
handleToggleExpand(user.id)} > {/* Expand chevron */}
{isExpanded ? : }
{/* User Info */}
{user.email} {user.isAdmin && ( admin )} {!user.active && ( inactive )} {user.workspaces.map(ws => ( {ws.name} ({ws.role}) ))}
{user.displayName && ( {user.displayName} )} {user.projectCount} projects {user.storageMb} MB {user.lastActive ? ( Active {formatDate(user.lastActive)} ) : ( Created {formatDate(user.createdAt)} )}
{/* Actions */} handleOpenEdit(user)}> Edit handleToggleActive(user)}> {user.active ? ( <> Deactivate ) : ( <> Reactivate )} handleDelete(user)} className="text-destructive focus:text-destructive" > Delete
{/* Expanded workspace details */} {isExpanded && (
{user.workspaces.length > 0 ? (
Workspaces ({user.workspaces.length})
{user.workspaces.map((ws) => (
{ws.name}
{ws.max_projects} projects {ws.max_deployments} deployments Created {formatDate(ws.created_at)}
))}
) : (
No workspaces assigned
)}
)}
); })}
)}
{/* Edit User Dialog */} { setShowEditDialog(open); if (!open) setEditUser(null); }}> Edit User
setEditForm(f => ({ ...f, displayName: e.target.value }))} placeholder="Display name" />
setEditForm(f => ({ ...f, active: checked }))} />
{/* Create User Dialog */} Create User
setCreateForm(f => ({ ...f, email: e.target.value }))} placeholder="user@example.com" />
setCreateForm(f => ({ ...f, password: e.target.value }))} placeholder="Minimum 8 characters" className="pr-9" />
setCreateForm(f => ({ ...f, displayName: e.target.value }))} placeholder="Display name (optional)" />
{/* Workspace Assignment */}
{createForm.workspaceAssignment === 'existing' && (
)}
); }