import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useState } from "react" import { CloudService } from "@/client" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Separator } from "@/components/ui/separator" 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 instance_id: string | null last_error: string | null connected_since: number | null } /** * Connecting this instance to a Fluksio portal, or cutting it loose, and * admitting other portal accounts to it. * * Deliberately blunt about what it grants: the account that enrolled is what * the portal owner's sessions act as, and this screen says which one. Anyone * else gets in only by being added here, as a local user of their own. * Everything here is optional — an instance 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 [joinCode, setJoinCode] = useState("") const [confirmDisconnect, setConfirmDisconnect] = useState(false) const { data: status } = useQuery({ 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 addRemoteUser = useMutation({ mutationFn: () => CloudService.addRemoteUser({ requestBody: { code: joinCode.trim() } }), onSuccess: (user) => { setJoinCode("") showSuccessToast(`Added ${user.email}`) // They are an ordinary user from here on, and the Admin page lists them. queryClient.invalidateQueries({ queryKey: ["users"] }) }, 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 ( Remote access Reach this instance from fluksio.com. Entirely optional — without it, this instance talks to nothing outside your network. {status.enrolled ? ( <>
{status.connected ? "Connected" : status.last_error ? `Reconnecting — ${status.last_error}` : "Reconnecting…"} {status.portal_url ?? "—"} {status.portal_account ?? "—"} Portal sessions of this account get this account's rights here. Anyone else gets in only once added below, as their own user. {status.instance_id ?? "—"}

Remote users

Let someone else reach this instance through the portal. They get a user of their own here — not yours, and never a superuser, so they cannot pass access on.

setJoinCode(event.target.value.toUpperCase()) } />

They get a code at fluksio.com → Instances → Join an instance. Added users appear under Admin → Users; deleting them there ends their access.

) : (
setPortalUrl(event.target.value)} />
setCode(event.target.value.toUpperCase())} />

Get a code at fluksio.com → Instances → Add instance.

)}
Disconnect from the portal? Remote access ends immediately and the portal's credentials stop working here. Nothing on this instance is changed or deleted, and you can connect again with a new code.
) } function Field({ label, children, }: { label: string children: React.ReactNode }) { return (
{label}
{children}
) } export default RemoteAccess