Files
app/frontend/src/lib/portal.ts
T
stroblme 184ef8b00c Pair a wall panel through the portal
A screen somewhere this installation is not reachable from asks the portal for
a code instead, and the portal mints its credential — because a token signed
here is one such a device could never present.

Where it was minted changes nothing about what it may do. The panel gate moved
off the branch that decodes a local panel token and onto whatever claims name
a panel, so the portal's and this installation's are bounded by the same check
against the same panel's dashboards. A token of that scope naming no panel is
refused rather than left holding the account it borrows.

The connector marks what arrives on its socket, since that is the only thing
that makes it true, and the approval screen now names what is holding a code —
approving adopts whatever answers, so it is worth a look first.
2026-08-20 23:42:58 +02:00

84 lines
2.9 KiB
TypeScript

/**
* Runtime configuration injected when this app is served through a portal.
*
* A normal installation ships the same bundle and finds nothing here, so every
* portal-aware branch in the app collapses to its ordinary behaviour. When a
* portal serves the page it writes this object into the document first: the
* API lives under the installation's own path, the credential comes with the
* page rather than from storage, and there is somewhere to go "back" to.
*
* Deliberately not localStorage: two installations open in one browser share
* an origin, and a single `access_token` key would have them overwrite each
* other's session.
*/
export type PortalConfig = {
installationId: string
installationName: string
/** Router basepath, e.g. `/i/{id}`. */
basePath: string
/** Origin-relative API base; the SDK appends `/api/v1/...`. */
apiBase: string
/** Where "back to portal" goes. */
portalUrl: string
/** Short-lived token scoped to this installation. */
token: string
}
declare global {
interface Window {
__FLUKSIO__?: PortalConfig
}
}
export function portalConfig(): PortalConfig | undefined {
return typeof window === "undefined" ? undefined : window.__FLUKSIO__
}
export function isPortal(): boolean {
return portalConfig() !== undefined
}
/** The bearer token for API calls, from whichever channel supplied one. */
export function apiToken(): string {
return portalConfig()?.token ?? localStorage.getItem("access_token") ?? ""
}
/**
* Hand a freshly paired credential to the hub, which keeps it in its cookie.
*
* Under the portal the page is served with its token injected, so there is
* nowhere in this app to put one a device just collected — and a credential
* that lasts a year is exactly what must not ride in a query string, where
* access logs and browser history would keep it.
*/
export async function openPortalSession(token: string): Promise<void> {
const config = portalConfig()
if (!config) return
const response = await fetch(`${config.basePath}/session`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token }),
})
if (!response.ok) throw new Error(`The portal refused the credential`)
}
/**
* A path in this app, spelled the way the browser has to spell it.
*
* The router is given the portal's basepath, so every `Link` and `navigate`
* carries it already. Anything that steps outside the router — a plain
* anchor, a `location.href` — has to add it back, or it lands on the portal
* itself, which serves nothing at that address.
*/
export function appPath(path: string): string {
return `${portalConfig()?.basePath ?? ""}${path}`
}
/** This page's route inside the app, with any portal prefix taken off again. */
export function appRoute(): string {
const base = portalConfig()?.basePath ?? ""
const path = window.location.pathname
if (!base || !path.startsWith(base)) return path
return path.slice(base.length) || "/"
}