// — the profile affordance in a header's top-right corner: an // initials avatar button opening a popover with the signed-in name, a link // to authelia's own account settings, and a sign-out link. The swarm UI, the // hive dashboard and the per-agent page all mount this one, so they look and // behave the same. Same icon-button + popover chrome as `SettingsMenu`. // // Who: `auth_request` only answers yes or no, never *who*. The name comes from // authelia's `GET /api/user/info`, which each UI's nginx vhost proxies at // `/api/whoami` on its own origin, so the browser sends the session cookie // and no CORS is involved. A fetch that fails leaves the avatar as a "?". // // Avatar: authelia has no photo field, so it is generated from the initials // (no third-party call: gravatar would leak a hashed e-mail on every load). // // Styles live in `@hive/shared/user-menu.css`, which the mounting page // imports or `@import`s itself: the dashboard's JS bundle loads `.css` // imports as text, so a component that imported it would not style there. import { useEffect, useRef, useState } from "preact/hooks"; import { accountLinks, initialOf, labelOf, parseWhoAmI, pickAccent, type WhoAmI, } from "./identity.js"; export function UserMenu({ whoamiUrl, autheliaUrl, }: { // Where the signed-in user is asked for, on this page's own origin. whoamiUrl: string; // Authelia's base URL, for the settings and sign-out links; none when it // is not known, and the menu then shows only the name. autheliaUrl: string | null; }) { const [who, setWho] = useState(null); const [open, setOpen] = useState(false); const rootRef = useRef(null); useEffect(() => { let cancelled = false; fetch(whoamiUrl) .then((r) => r.ok ? r.json() : Promise.reject(new Error(`http ${r.status}`)), ) .then((body) => { if (!cancelled) setWho(parseWhoAmI(body)); }) .catch(() => { if (!cancelled) setWho(null); }); return () => { cancelled = true; }; }, [whoamiUrl]); // Close on an outside click or Escape, as `SettingsMenu` does. useEffect(() => { if (!open) return; function onPointerDown(e: MouseEvent) { if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false); } function onKeyDown(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } document.addEventListener("pointerdown", onPointerDown); document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("pointerdown", onPointerDown); document.removeEventListener("keydown", onKeyDown); }; }, [open]); const label = labelOf(who); const links = accountLinks(autheliaUrl); return (
{open ? ( ) : null}
); }