import { useState } from 'react' import { useTranslation } from 'react-i18next' 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 { t } = useTranslation() 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(t('users.inviteSent')) } else { toast.success(t('users.inviteCreated')) } }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || t('users.inviteFailed')) }, }) const copyInviteLink = () => { if (inviteResult?.token) { const link = `${window.location.origin}/accept-invite?token=${inviteResult.token}` navigator.clipboard.writeText(link) toast.success(t('users.inviteLinkCopied')) } } 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 (

{t('users.inviteUser')}

{inviteResult ? (
{t('users.inviteSuccess')}
{inviteResult.emailSent ? (

{t('users.inviteEmailSent')}

) : (

{t('users.inviteEmailNotSent')}

)}
{!inviteResult.emailSent && (

{t('users.expires')}: {new Date(inviteResult.expiresAt).toLocaleString()}

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

{permissionMode === 'allow_all' ? t('users.allowAllDescription') : t('users.denyAllDescription')}

{proxyHosts.length === 0 ? (

{t('users.noProxyHosts')}

) : ( proxyHosts.map((host) => ( )) )}
)}
)}
) } interface PermissionsModalProps { isOpen: boolean onClose: () => void user: User | null proxyHosts: ProxyHost[] } function PermissionsModal({ isOpen, onClose, user, proxyHosts }: PermissionsModalProps) { const { t } = useTranslation() 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(t('users.permissionsUpdated')) onClose() }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || t('users.permissionsUpdateFailed')) }, }) const toggleHost = (hostId: number) => { setSelectedHosts((prev) => prev.includes(hostId) ? prev.filter((id) => id !== hostId) : [...prev, hostId] ) } if (!isOpen || !user) return null return (

{t('users.editPermissions')} - {user.name || user.email}

{permissionMode === 'allow_all' ? t('users.allowAllDescription') : t('users.denyAllDescription')}

{proxyHosts.length === 0 ? (

{t('users.noProxyHosts')}

) : ( proxyHosts.map((host) => ( )) )}
) } export default function UsersPage() { const { t } = useTranslation() 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(t('users.userUpdated')) }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || t('users.userUpdateFailed')) }, }) const deleteMutation = useMutation({ mutationFn: deleteUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }) toast.success(t('users.userDeleted')) }, onError: (error: unknown) => { const err = error as { response?: { data?: { error?: string } } } toast.error(err.response?.data?.error || t('users.userDeleteFailed')) }, }) const openPermissions = (user: User) => { setSelectedUser(user) setPermissionsModalOpen(true) } if (isLoading) { return (
) } return (

{t('users.title')}

{users?.map((user) => ( ))}
{t('users.columnUser')} {t('users.columnRole')} {t('common.status')} {t('users.columnPermissions')} {t('common.enabled')} {t('common.actions')}

{user.name || t('users.noName')}

{user.email}

{user.role} {user.invite_status === 'pending' ? ( {t('users.pendingInvite')} ) : user.invite_status === 'expired' ? ( {t('users.inviteExpired')} ) : ( {t('common.active')} )} {user.permission_mode === 'deny_all' ? t('users.whitelist') : t('users.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} />
) }