import { useEffect, useState, type FormEvent } from "react"; import { Link } from "react-router-dom"; import { api, type AgentPortRow, type CurrentUser, type PortForward, type PortForwardInput, type ServerRecord, } from "../api/client"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { usePagination } from "../hooks/usePagination"; import Pagination from "../components/Pagination"; import { downloadCsv } from "../utils/csv"; import { formatAgo } from "../utils/duration"; const emptyForm: PortForwardInput = { label: "", externalPort: 0, protocol: "tcp", serverId: null, destination: "", internalPort: null, source: "", comment: "", }; export default function Ports({ user }: { user: CurrentUser }) { const canEdit = user.role === "admin" || user.role === "operator"; const [agentPorts, setAgentPorts] = useState(null); const [reportingServers, setReportingServers] = useState(0); const [totalServers, setTotalServers] = useState(0); const [agentError, setAgentError] = useState(null); const [servers, setServers] = useState(null); const [forwards, setForwards] = useState(null); const [forwardError, setForwardError] = useState(null); const [editing, setEditing] = useState(null); const [adding, setAdding] = useState(false); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); function loadAgentPorts() { api.ports .agent() .then((res) => { setAgentPorts(res.ports); setReportingServers(res.reportingServers); setTotalServers(res.totalServers); }) .catch((err) => setAgentError(err instanceof Error ? err.message : String(err))); } function loadForwards() { api.ports.forwards .list() .then((res) => setForwards(res.forwards)) .catch((err) => setForwardError(err instanceof Error ? err.message : String(err))); } useEffect(() => { loadAgentPorts(); loadForwards(); api.servers .list() .then((res) => setServers(res.servers)) .catch(() => {}); }, []); const { sorted: sortedAgentPorts, sortKey: agentSortKey, sortDir: agentSortDir, requestSort: requestAgentSort } = useSortable( agentPorts, "serverName", ); const { pageItems: agentPageItems, page: agentPage, setPage: setAgentPage, pageCount: agentPageCount, totalCount: agentTotalCount } = usePagination( sortedAgentPorts, ); const { sorted: sortedForwards, sortKey: forwardSortKey, sortDir: forwardSortDir, requestSort: requestForwardSort } = useSortable( forwards, "externalPort", ); function exportAgentCsv() { downloadCsv( "agent-reported-ports.csv", ["Server", "Hostname", "Protocol", "Port", "Addresses", "Process", "Local only"], (sortedAgentPorts ?? []).map((p) => [ p.serverName, p.serverHostname ?? "", p.protocol, p.port, p.addresses.join(", "), p.process ?? "", p.localOnly ? "yes" : "no", ]), ); } function exportForwardsCsv() { downloadCsv( "port-forwards.csv", ["Label", "External port", "Protocol", "Server", "Destination", "Internal port", "Source", "Comment"], (sortedForwards ?? []).map((f) => [ f.label, f.externalPort, f.protocol, f.serverName ?? "", f.destination ?? "", f.internalPort ?? "", f.source ?? "", f.comment ?? "", ]), ); } function startAdd() { setAdding(true); setEditing(null); setForm(emptyForm); } function startEdit(fwd: PortForward) { setEditing(fwd); setAdding(false); setForm({ label: fwd.label, externalPort: fwd.externalPort, protocol: fwd.protocol, serverId: fwd.serverId, destination: fwd.destination ?? "", internalPort: fwd.internalPort, source: fwd.source ?? "", comment: fwd.comment ?? "", }); } function cancelForm() { setAdding(false); setEditing(null); setForm(emptyForm); } async function submit(e: FormEvent) { e.preventDefault(); setForwardError(null); setSaving(true); try { if (editing) { await api.ports.forwards.update(editing.id, form); } else { await api.ports.forwards.create(form); } cancelForm(); loadForwards(); } catch (err) { setForwardError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function remove(fwd: PortForward) { if (!confirm(`Delete port opening "${fwd.label}"?`)) return; setForwardError(null); try { await api.ports.forwards.remove(fwd.id); loadForwards(); } catch (err) { setForwardError(err instanceof Error ? err.message : String(err)); } } const showForm = adding || editing; return ( <>

Ports

Ports reported by agents

{reportingServers} of {totalServers} server{totalServers !== 1 ? "s" : ""} reporting
{agentError &&
{agentError}
}
label="Server" sortKeyName="serverName" activeKey={agentSortKey} direction={agentSortDir} onSort={requestAgentSort} /> label="Protocol" sortKeyName="protocol" activeKey={agentSortKey} direction={agentSortDir} onSort={requestAgentSort} /> label="Port" sortKeyName="port" activeKey={agentSortKey} direction={agentSortDir} onSort={requestAgentSort} /> label="Last report" sortKeyName="lastSeenAt" activeKey={agentSortKey} direction={agentSortDir} onSort={requestAgentSort} /> {(agentPageItems ?? []).map((p) => ( ))} {agentPorts !== null && agentPorts.length === 0 && ( )}
Address Process
{p.serverName} {p.serverHostname &&
{p.serverHostname}
}
{p.protocol} {p.port} {p.addresses.join(", ")} {p.localOnly && local only} {p.process ?? "—"} {p.lastSeenAt ? formatAgo(p.lastSeenAt) : "—"}
No servers have reported listening ports yet.
{canEdit && showForm && (

{editing ? `Edit "${editing.label}"` : "Add a manual port opening"}

setForm({ ...form, label: e.target.value })} />
setForm({ ...form, externalPort: Number(e.target.value) })} />
setForm({ ...form, internalPort: e.target.value ? Number(e.target.value) : null })} />
setForm({ ...form, destination: e.target.value })} />
setForm({ ...form, source: e.target.value })} />
setForm({ ...form, comment: e.target.value })} />
)}

Manual port openings

Router port forwards, edge firewall rules, cloud security groups — anything opened further out on the network that this app can't see on its own.

{canEdit && !showForm && ( )}
{forwardError &&
{forwardError}
}
label="Label" sortKeyName="label" activeKey={forwardSortKey} direction={forwardSortDir} onSort={requestForwardSort} /> label="External port" sortKeyName="externalPort" activeKey={forwardSortKey} direction={forwardSortDir} onSort={requestForwardSort} /> label="Source" sortKeyName="source" activeKey={forwardSortKey} direction={forwardSortDir} onSort={requestForwardSort} /> {canEdit && } {(sortedForwards ?? []).map((f) => ( {canEdit && ( )} ))} {forwards !== null && forwards.length === 0 && ( )}
Destination CommentActions
{f.label} {f.protocol} {f.externalPort} {f.internalPort && f.internalPort !== f.externalPort && ( → {f.internalPort} )} {f.serverId ? {f.serverName} : f.destination || "—"} {f.serverId && f.destination &&
{f.destination}
}
{f.source ?? "—"} {f.comment ?? "—"}
No manual port openings recorded yet.
); }