Settings: connect this installation to a portal

A superuser-only tab that redeems a claim code and shows the link's state. It
names the account a remote session acts as, because that is the thing being
granted and the person granting it should see it spelled out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtBzdDyLsmDaF1W7DLYtYM
This commit is contained in:
2026-08-19 17:39:14 +02:00
co-authored by Claude Opus 5
parent 32d8b42682
commit 9ed0caa14f
5 changed files with 318 additions and 2 deletions
@@ -0,0 +1,211 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useState } from "react"
import { CloudService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
const DEFAULT_PORTAL = "https://hub.fluksio.com"
type CloudStatus = {
enrolled: boolean
connected: boolean
portal_url: string | null
portal_account: string | null
installation_id: string | null
last_error: string | null
connected_since: number | null
}
/**
* Connecting this installation to a Fluksio portal, or cutting it loose.
*
* Deliberately blunt about what it grants: a remote session acts as the
* account that enrolled, and this screen says which one. Everything here is
* optional — an installation nobody enrolls never contacts anything.
*/
export function RemoteAccess() {
const queryClient = useQueryClient()
const { showErrorToast, showSuccessToast } = useCustomToast()
const [portalUrl, setPortalUrl] = useState(DEFAULT_PORTAL)
const [code, setCode] = useState("")
const [confirmDisconnect, setConfirmDisconnect] = useState(false)
const { data: status } = useQuery<CloudStatus>({
queryKey: ["cloud", "status"],
// The endpoint returns a plain object; the generated type is `unknown`
// because it has no response model of its own.
queryFn: async () => (await CloudService.readStatus()) as CloudStatus,
// Often enough that "connecting…" resolves while someone is watching it.
refetchInterval: 5000,
})
const invalidate = () =>
queryClient.invalidateQueries({ queryKey: ["cloud", "status"] })
const connect = useMutation({
mutationFn: () =>
CloudService.enroll({
requestBody: { portal_url: portalUrl.trim(), claim_code: code.trim() },
}),
onSuccess: () => {
setCode("")
showSuccessToast("Connected to the portal")
invalidate()
},
onError: handleError.bind(showErrorToast),
})
const disconnect = useMutation({
mutationFn: () => CloudService.disconnect(),
onSuccess: () => {
setConfirmDisconnect(false)
showSuccessToast("Disconnected from the portal")
invalidate()
},
onError: handleError.bind(showErrorToast),
})
if (!status) return null
return (
<div className="flex max-w-2xl flex-col gap-6 py-4">
<div>
<h2 className="text-lg font-medium">Remote access</h2>
<p className="text-sm text-muted-foreground">
Reach this installation from fluksio.com. Entirely optional without
it, this installation talks to nothing outside your network.
</p>
</div>
{status.enrolled ? (
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
<dl className="grid gap-3 sm:grid-cols-2">
<Field label="Status">
{status.connected
? "Connected"
: status.last_error
? `Reconnecting — ${status.last_error}`
: "Reconnecting…"}
</Field>
<Field label="Portal">{status.portal_url ?? "—"}</Field>
<Field label="Acting as">
{status.portal_account ?? "—"}
<span className="mt-1 block text-xs text-muted-foreground">
Anyone signed in to the portal for this installation gets this
account's rights here.
</span>
</Field>
<Field label="Installation">
<span className="font-mono text-xs">
{status.installation_id ?? "—"}
</span>
</Field>
</dl>
<div>
<Button
variant="destructive"
className="rounded-full"
onClick={() => setConfirmDisconnect(true)}
>
Disconnect
</Button>
</div>
</div>
) : (
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
<div className="grid gap-2">
<Label htmlFor="portal-url">Portal</Label>
<Input
id="portal-url"
value={portalUrl}
onChange={(event) => setPortalUrl(event.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="claim-code">Code</Label>
<Input
id="claim-code"
value={code}
placeholder="XXXX-XXXX"
className="font-mono tracking-widest"
onChange={(event) => setCode(event.target.value.toUpperCase())}
/>
<p className="text-xs text-muted-foreground">
Get a code at fluksio.com → Installations → Add installation.
</p>
</div>
<div>
<Button
className="rounded-full bg-[--brand-secondary] text-white hover:opacity-90"
disabled={!code.trim() || !portalUrl.trim() || connect.isPending}
onClick={() => connect.mutate()}
>
{connect.isPending ? "Connecting…" : "Connect"}
</Button>
</div>
</div>
)}
<Dialog open={confirmDisconnect} onOpenChange={setConfirmDisconnect}>
<DialogContent>
<DialogHeader>
<DialogTitle>Disconnect from the portal?</DialogTitle>
<DialogDescription>
Remote access ends immediately and the portal's credentials stop
working here. Nothing on this installation is changed or deleted,
and you can connect again with a new code.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
className="rounded-full"
onClick={() => setConfirmDisconnect(false)}
>
Cancel
</Button>
<Button
variant="destructive"
className="rounded-full"
disabled={disconnect.isPending}
onClick={() => disconnect.mutate()}
>
Disconnect
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
function Field({
label,
children,
}: {
label: string
children: React.ReactNode
}) {
return (
<div>
<dt className="text-xs uppercase tracking-wide text-muted-foreground">
{label}
</dt>
<dd className="mt-1 text-sm">{children}</dd>
</div>
)
}
export default RemoteAccess