"use client"; /* Single sign-on for a facility, set up by its admin. * * Three states, top to bottom: not available on this server (the panel says so and stops); * not connected (paste the identity provider's metadata and the email domains, connect); and * connected (the switches — require it, let staff use it — the domains, the break-glass admins, * and disconnect). The identity provider's metadata is handed to the SSO service and never shown * back; what this panel shows is what the facility decided. */ import { useCallback, useEffect, useState } from "react"; import { Field } from "@/components/ui"; import type { UserRec } from "@/lib/compute"; type Status = { enabled: boolean; required: boolean; staff: boolean; domains: string[]; connected: boolean | null; idp: string | null }; type Props = { isAdmin: boolean; demo: boolean; sso: { enabled: boolean; required: boolean; staff: boolean; domains: string[] }; users: UserRec[]; onChanged: () => void; mutate: (op: string, payload: unknown) => Promise<{ ok: boolean; error?: string }>; }; export default function SsoSettings({ isAdmin, demo, sso, users, onChanged, mutate }: Props) { const [st, setSt] = useState(null); const [busy, setBusy] = useState(false); const [err, setErr] = useState(""); const [msg, setMsg] = useState(""); const [metaUrl, setMetaUrl] = useState(""); const [metaXml, setMetaXml] = useState(""); const [domains, setDomains] = useState(sso.domains.join(", ")); const [confirmOff, setConfirmOff] = useState(false); const load = useCallback(async () => { try { const r = await fetch("/api/sso"); if (r.status === 404) { setSt("none"); return; } if (!r.ok) { setErr("Couldn’t read the single sign-on settings."); return; } const j = (await r.json()) as Status; setSt(j); setDomains(j.domains.join(", ")); } catch { setErr("Couldn’t reach the server."); } }, []); useEffect(() => { void load(); }, [load]); async function call(method: "POST" | "PATCH" | "DELETE", body?: Record) { setBusy(true); setErr(""); setMsg(""); try { const r = await fetch("/api/sso", { method, headers: body ? { "content-type": "application/json" } : undefined, body: body ? JSON.stringify(body) : undefined }); const j = await r.json().catch(() => ({})); if (!r.ok) { setErr(j.error || "That didn’t work."); return false; } await load(); onChanged(); return true; } catch { setErr("No connection — check the network and try again."); return false; } finally { setBusy(false); } } const note: React.CSSProperties = { fontSize: 13, color: "var(--color-neutral-700)", lineHeight: 1.6, marginTop: "var(--space-2)" }; const box: React.CSSProperties = { border: "2px solid var(--color-text)", padding: "var(--space-3)", marginTop: "var(--space-3)", maxWidth: 720 }; if (demo) return
Single sign-on is set up per facility and isn’t part of the shared demo.
; if (st === "none") return
Single sign-on isn’t available on this server.
; if (!isAdmin) return
{sso.enabled ? `Your facility ${sso.required ? "signs in" : "can sign in"} with single sign-on.` : "Your facility signs in with passwords. An admin can connect single sign-on here."}
; if (!st) return
{err || "Loading…"}
; const admins = users.filter((u) => u.role === "ADMIN" && !u.inactive); if (!st.enabled) { return (
Let your people sign in with the account they already have — Microsoft Entra, Okta, Google Workspace or any provider that speaks SAML or OpenID Connect. Paste the provider’s metadata, list your email domains, and connect.
Your provider will ask for the service’s details. ACS / redirect URL: {typeof window === "undefined" ? "" : window.location.origin}/api/auth/sso/callback. Entity ID: https://sso.threadcount.tech. Your admin at ThreadCount can supply the SP metadata if the provider wants a file.
{(c) => setMetaUrl(e.target.value)} placeholder="https://login.microsoftonline.com/…/federationmetadata/2007-06/federationmetadata.xml" />} {(c) =>