import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Card } from '../components/ui/Card' import { Button } from '../components/ui/Button' import { Input } from '../components/ui/Input' import { Switch } from '../components/ui/Switch' import { toast } from '../utils/toast' import { listUsers, inviteUser, deleteUser, updateUser, updateUserPermissions, } from '../api/users' import type { User, InviteUserRequest, PermissionMode, UpdateUserPermissionsRequest } from '../api/users' import { getProxyHosts } from '../api/proxyHosts' import type { ProxyHost } from '../api/proxyHosts' import { Users, UserPlus, Mail, Shield, Trash2, Settings, X, Check, AlertCircle, Clock, Copy, Loader2, } from 'lucide-react' interface InviteModalProps { isOpen: boolean onClose: () => void proxyHosts: ProxyHost[] } function InviteModal({ isOpen, onClose, proxyHosts }: InviteModalProps) { const queryClient = useQueryClient() const [email, setEmail] = useState('') const [role, setRole] = useState<'user' | 'admin'>('user') const [permissionMode, setPermissionMode] = useState('allow_all') const [selectedHosts, setSelectedHosts] = useState([]) const [inviteResult, setInviteResult] = useState<{ token: string emailSent: boolean expiresAt: string } | null>(null) const inviteMutation = useMutation({ mutationFn: async () => { const request: InviteUserRequest = { email, role, permission_mode: permissionMode, permitted_hosts: selectedHosts, } return inviteUser(request) }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['users'] }) setInviteResult({ token: data.invite_token, emailSent: data.email_sent, expiresAt: data.expires_at, }) if (data.email_sent) { toast.success('Invitation email sent') } else { toast.success('User invited - copy the invite link below') } }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || 'Failed to invite user') }, }) const copyInviteLink = () => { if (inviteResult?.token) { const link = `${window.location.origin}/accept-invite?token=${inviteResult.token}` navigator.clipboard.writeText(link) toast.success('Invite link copied to clipboard') } } const handleClose = () => { setEmail('') setRole('user') setPermissionMode('allow_all') setSelectedHosts([]) setInviteResult(null) onClose() } const toggleHost = (hostId: number) => { setSelectedHosts((prev) => prev.includes(hostId) ? prev.filter((id) => id !== hostId) : [...prev, hostId] ) } if (!isOpen) return null return (

Invite User

{inviteResult ? (
User Invited Successfully
{inviteResult.emailSent ? (

An invitation email has been sent to the user.

) : (

Email was not sent. Share the invite link manually.

)}
{!inviteResult.emailSent && (

Expires: {new Date(inviteResult.expiresAt).toLocaleString()}

)}
) : ( <> setEmail(e.target.value)} placeholder="user@example.com" />
{role === 'user' && ( <>

{permissionMode === 'allow_all' ? 'User can access all hosts EXCEPT those selected below' : 'User can ONLY access hosts selected below'}

{proxyHosts.length === 0 ? (

No proxy hosts configured

) : ( proxyHosts.map((host) => ( )) )}
)}
)}
) } interface PermissionsModalProps { isOpen: boolean onClose: () => void user: User | null proxyHosts: ProxyHost[] } function PermissionsModal({ isOpen, onClose, user, proxyHosts }: PermissionsModalProps) { const queryClient = useQueryClient() const [permissionMode, setPermissionMode] = useState('allow_all') const [selectedHosts, setSelectedHosts] = useState([]) // Update state when user changes useState(() => { if (user) { setPermissionMode(user.permission_mode || 'allow_all') setSelectedHosts(user.permitted_hosts || []) } }) const updatePermissionsMutation = useMutation({ mutationFn: async () => { if (!user) return const request: UpdateUserPermissionsRequest = { permission_mode: permissionMode, permitted_hosts: selectedHosts, } return updateUserPermissions(user.id, request) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }) toast.success('Permissions updated') onClose() }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || 'Failed to update permissions') }, }) const toggleHost = (hostId: number) => { setSelectedHosts((prev) => prev.includes(hostId) ? prev.filter((id) => id !== hostId) : [...prev, hostId] ) } if (!isOpen || !user) return null return (

Edit Permissions - {user.name || user.email}

{permissionMode === 'allow_all' ? 'User can access all hosts EXCEPT those selected below' : 'User can ONLY access hosts selected below'}

{proxyHosts.length === 0 ? (

No proxy hosts configured

) : ( proxyHosts.map((host) => ( )) )}
) } export default function UsersPage() { const queryClient = useQueryClient() const [inviteModalOpen, setInviteModalOpen] = useState(false) const [permissionsModalOpen, setPermissionsModalOpen] = useState(false) const [selectedUser, setSelectedUser] = useState(null) const { data: users, isLoading } = useQuery({ queryKey: ['users'], queryFn: listUsers, }) const { data: proxyHosts = [] } = useQuery({ queryKey: ['proxyHosts'], queryFn: getProxyHosts, }) const toggleEnabledMutation = useMutation({ mutationFn: async ({ id, enabled }: { id: number; enabled: boolean }) => { return updateUser(id, { enabled }) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }) toast.success('User updated') }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || 'Failed to update user') }, }) const deleteMutation = useMutation({ mutationFn: deleteUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }) toast.success('User deleted') }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || 'Failed to delete user') }, }) const openPermissions = (user: User) => { setSelectedUser(user) setPermissionsModalOpen(true) } if (isLoading) { return (
) } return (

User Management

{users?.map((user) => ( ))}
User Role Status Permissions Enabled Actions

{user.name || '(No name)'}

{user.email}

{user.role} {user.invite_status === 'pending' ? ( Pending Invite ) : user.invite_status === 'expired' ? ( Invite Expired ) : ( Active )} {user.permission_mode === 'deny_all' ? 'Whitelist' : 'Blacklist'} toggleEnabledMutation.mutate({ id: user.id, enabled: !user.enabled, }) } disabled={user.role === 'admin'} />
{user.role !== 'admin' && ( )}
setInviteModalOpen(false)} proxyHosts={proxyHosts} /> { setPermissionsModalOpen(false) setSelectedUser(null) }} user={selectedUser} proxyHosts={proxyHosts} />
) }