import { t } from "@lingui/core/macro" import { Trans, useLingui } from "@lingui/react/macro" import { redirectPage } from "@nanostores/router" import { CopyIcon, FingerprintIcon, KeyIcon, MoreHorizontalIcon, RotateCwIcon, ServerIcon, Trash2Icon, ExternalLinkIcon, } from "lucide-react" import { memo, useEffect, useMemo, useState } from "react" import { copyDockerCompose, copyDockerRun, copyLinuxCommand, copyWindowsCommand, type DropdownItem, InstallDropdown, } from "@/components/install-dropdowns" import { $router } from "@/components/router" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons" import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/use-toast" import { isReadOnlyUser, pb } from "@/lib/api" import { $publicKey } from "@/lib/stores" import { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from "@/lib/utils" import type { FingerprintRecord } from "@/types" const pbFingerprintOptions = { expand: "system", fields: "id,fingerprint,token,system,expand.system.name", } function sortFingerprints(fingerprints: FingerprintRecord[]) { return fingerprints.sort((a, b) => a.expand.system.name.localeCompare(b.expand.system.name)) } const SettingsFingerprintsPage = memo(() => { if (isReadOnlyUser()) { redirectPage($router, "settings", { name: "general" }) } const [fingerprints, setFingerprints] = useState([]) // Get fingerprint records on mount useEffect(() => { pb.collection("fingerprints") .getFullList(pbFingerprintOptions) .then((prints) => { setFingerprints(sortFingerprints(prints)) }) }, []) // Subscribe to fingerprint updates useEffect(() => { let unsubscribe: (() => void) | undefined ;(async () => { // subscribe to fingerprint updates unsubscribe = await pb.collection("fingerprints").subscribe( "*", (res) => { setFingerprints((currentFingerprints) => { if (res.action === "create") { return sortFingerprints([...currentFingerprints, res.record as FingerprintRecord]) } if (res.action === "update") { return currentFingerprints.map((fingerprint) => { if (fingerprint.id === res.record.id) { return { ...fingerprint, ...res.record } as FingerprintRecord } return fingerprint }) } if (res.action === "delete") { return currentFingerprints.filter((fingerprint) => fingerprint.id !== res.record.id) } return currentFingerprints }) }, pbFingerprintOptions ) })() // unsubscribe on unmount return () => unsubscribe?.() }, []) // Update token map whenever fingerprints change useEffect(() => { for (const fingerprint of fingerprints) { tokenMap.set(fingerprint.system, fingerprint.token) } }, [fingerprints]) return ( <> ) }) const SectionIntro = memo(() => { return (

Tokens & Fingerprints

Tokens and fingerprints are used to authenticate WebSocket connections to the hub.

Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.

) }) const SectionUniversalToken = memo(() => { const [token, setToken] = useState("") const [isLoading, setIsLoading] = useState(true) const [checked, setChecked] = useState(false) async function updateToken(enable: number = -1) { // enable: 0 for disable, 1 for enable, -1 (unset) for get current state const data = await pb.send(`/api/beszel/universal-token`, { query: { token, enable, }, }) setToken(data.token) setChecked(data.active) setIsLoading(false) } // biome-ignore lint/correctness/useExhaustiveDependencies: only on mount useEffect(() => { updateToken() }, []) return (

Universal token

When enabled, this token allows agents to self-register without prior system creation. Expires after one hour or on hub restart.

{!isLoading && ( <> { updateToken(checked ? 1 : 0) }} /> {token} )}
) }) const ActionsButtonUniversalToken = memo(({ token, checked }: { token: string; checked: boolean }) => { const { t } = useLingui() const publicKey = $publicKey.get() const port = "45876" const dropdownItems: DropdownItem[] = [ { text: t({ message: "Copy docker compose", context: "Button to copy docker compose file content" }), onClick: () => copyDockerCompose(port, publicKey, token), icons: [DockerIcon], }, { text: t({ message: "Copy docker run", context: "Button to copy docker run command" }), onClick: () => copyDockerRun(port, publicKey, token), icons: [DockerIcon], }, { text: t`Copy Linux command`, onClick: () => copyLinuxCommand(port, publicKey, token), icons: [TuxIcon], }, { text: t({ message: "Homebrew command", context: "Button to copy install command" }), onClick: () => copyLinuxCommand(port, publicKey, token, true), icons: [TuxIcon, AppleIcon], }, { text: t({ message: "Windows command", context: "Button to copy install command" }), onClick: () => copyWindowsCommand(port, publicKey, token), icons: [WindowsIcon], }, { text: t({ message: "FreeBSD command", context: "Button to copy install command" }), onClick: () => copyLinuxCommand(port, publicKey, token), icons: [FreeBsdIcon], }, { text: t`Manual setup instructions`, url: "https://beszel.dev/guide/agent-installation#binary", icons: [ExternalLinkIcon], }, ] return (
) }) const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRecord[] }) => { const { t } = useLingui() const isReadOnly = isReadOnlyUser() const headerCols = useMemo( () => [ { label: t`System`, Icon: ServerIcon, w: "11em", }, { label: t`Token`, Icon: KeyIcon, w: "20em", }, { label: t`Fingerprint`, Icon: FingerprintIcon, w: "20em", }, ], [t] ) return (
{headerCols.map((col) => ( {col.label} ))} {!isReadOnly && ( Actions )} {fingerprints.map((fingerprint) => ( {fingerprint.expand.system.name} {fingerprint.token} {fingerprint.fingerprint} {!isReadOnly && ( )} ))}
) }) async function updateFingerprint(fingerprint: FingerprintRecord, rotateToken = false) { try { await pb.collection("fingerprints").update(fingerprint.id, { fingerprint: "", token: rotateToken ? generateToken() : fingerprint.token, }) } catch (error: unknown) { toast({ title: t`Error`, description: (error as Error).message, }) } } const ActionsButtonTable = memo(({ fingerprint }: { fingerprint: FingerprintRecord }) => { const envVar = `HUB_URL=${getHubURL()}\nTOKEN=${fingerprint.token}` const copyEnv = () => copyToClipboard(envVar) const copyYaml = () => copyToClipboard(envVar.replaceAll("=", ": ")) return ( Copy YAML Copy env updateFingerprint(fingerprint, true)}> Rotate token {fingerprint.fingerprint && ( updateFingerprint(fingerprint)}> Delete fingerprint )} ) }) export default SettingsFingerprintsPage