import { KeyRound, Lock, RefreshCw, ShieldCheck, UserPlus, X } from "lucide-react"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { api, errorMessage } from "../api"; import { ActionButton } from "../components/ActionButton"; import { Alert, EmptyRow, LoadingRow, PageFrame, QueryPanel, SectionHead } from "../components/ui"; import { groupPermissions, permissionAll, permissionHint, permissionTitle } from "../permissions"; import type { AdminConsoleUser, AdminConsoleSystemOperator } from "../types"; // The operator-accounts screen. The table only reports; every change happens in // a modal and goes through the panel's usual reason + dry-run + confirm flow, // because handing somebody the run of the console deserves the same "here is // what this will do" step as freezing an account. // // Everything here is additionally enforced server-side by admins.manage -- // hiding the section is a convenience, not the boundary. export function AdminUsersPage() { const [rows, setRows] = useState([]); const [system, setSystem] = useState(null); const [available, setAvailable] = useState([]); const [busy, setBusy] = useState(false); const [loaded, setLoaded] = useState(false); const [error, setError] = useState(""); const [editing, setEditing] = useState(null); const [resetting, setResetting] = useState(null); const [creating, setCreating] = useState(false); async function load() { setBusy(true); setError(""); try { const result = await api.adminUsers(); setRows(result.rows ?? []); setSystem(result.system ?? null); setAvailable(result.available_permissions ?? []); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); setLoaded(true); } } useEffect(() => { void load(); }, []); return ( {error && {error}}
{/* The built-in operator first: it has the most rights and no database row, so a list that started with the named accounts would put the most powerful login last, or nowhere. */} {system && ( )} {rows.map((row) => ( ))} {rows.length === 0 && !system && (busy || !loaded ? : )}
{"Username"} {"Can do"} {"Status"} {"Last login"}
{system.username} {"built-in"} {"Enabled"} {"—"} {"Set in the server environment"}
{row.username} {row.enabled ? {"Enabled"} : {"Disabled"}} {row.last_login_at ? new Date(row.last_login_at).toLocaleString() : "—"}
{creating && ( setCreating(false)} onDone={() => { setCreating(false); void load(); }} /> )} {editing && ( setEditing(null)} onDone={() => { setEditing(null); void load(); }} /> )} {resetting && ( setResetting(null)} onDone={() => { setResetting(null); void load(); }} /> )}
); } function PermissionChips({ permissions }: { permissions: string[] }) { if (permissions.length === 0) { return {"nothing yet"}; } return ( {permissions.map((p) => ( {permissionTitle(p)} ))} ); } // PermissionPicker lists the rights by what they let someone do, split into the // section of the console each governs -- twenty-six checkboxes in one run is a // wall nobody reads, and the grouping is what makes "what can this person // actually touch" answerable at a glance. // // The raw permission string stays as each row's tooltip, so the screen never // hides what is actually being stored. // // "*" gets its own row rather than a box in the grid below, because // assignablePermissions() deliberately leaves it out of the assignable list // (cmd/telesrv-admin/security.go) -- without this row an operator holding the // wildcard, like the one the first-run wizard creates, renders as every box // unticked while Has() answers true for everything, and there is no way to take // it away again. While it is on the grid is disabled: normalisePermissions // collapses "*" plus anything back to just "*", so ticking a box there would be // a no-op the screen would otherwise show as a change. function PermissionPicker({ available, selected, onToggle, onToggleGroup, onToggleAll }: { available: string[]; selected: string[]; onToggle: (permission: string, on: boolean) => void; onToggleGroup: (permissions: string[], on: boolean) => void; onToggleAll: (on: boolean) => void; }) { const full = selected.includes(permissionAll); return (
{groupPermissions(available).map((group) => { const all = group.permissions.every((p) => selected.includes(p)); return (
{group.title} {group.hint}
{group.permissions.map((permission) => ( ))}
); })}
); } // OperatorModal creates a new operator, or edits an existing one's access. The // same shape either way: the only difference is whether a username and password // are being chosen. // // Laid out as head / scrolling body / action bar like every other command modal // in the panel, so a long permission list scrolls inside the dialog instead of // pushing its own confirm button off the screen. function OperatorModal({ title, available, existing, onClose, onDone }: { title: string; available: string[]; existing?: AdminConsoleUser; onClose: () => void; onDone: () => void; }) { const [username, setUsername] = useState(existing?.username ?? ""); const [password, setPassword] = useState(""); const [permissions, setPermissions] = useState(existing?.permissions ?? []); const [enabled, setEnabled] = useState(existing?.enabled ?? true); const isEdit = Boolean(existing); // Only the shape the server insists on: a username it will accept, and a // password that is actually present. Length is the operator's business. const incomplete = isEdit ? false : username.trim().length < 3 || password.trim() === ""; return createPortal(
{"Operators"}

{title}

{!isEdit && (
)} setPermissions((current) => on ? [...current, permission] : current.filter((p) => p !== permission) ) } onToggleGroup={(group, on) => setPermissions((current) => on ? [...current, ...group.filter((p) => !current.includes(p))] : current.filter((p) => !group.includes(p)) ) } onToggleAll={(on) => setPermissions((current) => on ? [permissionAll] : current.filter((p) => p !== permissionAll) ) } /> {isEdit && ( {"The new access applies from this operator's next request. They stay signed in."} )}
: } payload={() => isEdit ? { id: existing?.id, permissions, enabled } : { username: username.trim(), password, permissions, enabled } } onDone={onDone} />
, document.body ); } function PasswordModal({ operator, onClose, onDone }: { operator: AdminConsoleUser; onClose: () => void; onDone: () => void; }) { const [password, setPassword] = useState(""); return createPortal(
{"Operators"}

{`Password for ${operator.username}`}

{"Changing the password signs this operator out of any session they already have."}
} payload={() => ({ id: operator.id, password })} onDone={onDone} />
, document.body ); }