Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/shared/src/user-menu/identity.ts
iris cd41e1d510 swarm-ui, hive dashboard, agent page: one profile menu with the signed-in user, avatar and authelia links
The swarm UI's user menu moves to @hive/shared and takes the whoami URL and
authelia's URL as props; the swarm UI keeps a thin wrapper, and the hive
dashboard and the per-agent page mount the same component. The dashboard's
nginx vhost proxies /api/whoami to authelia (the agent page is served from
the same vhost), and hive-c0re's state carries authelia_public_url, set from
services.hyperhive.swarm.authelia.url. The agent page shows the menu only
behind the gateway.
2026-10-25 15:25:20 +01:00

63 lines
2.2 KiB
TypeScript

// What <UserMenu> shows of the signed-in user: the name and e-mail authelia
// reports, the initials avatar built from them, and the authelia pages the
// menu links. Pure, so the swarm UI's tests can run it.
export interface WhoAmI {
displayName: string | null;
email: string | null;
}
// The seven chromatic theme colours the avatar is picked from, by name.
const AVATAR_ACCENTS = [
"var(--red)",
"var(--yellow)",
"var(--green)",
"var(--cyan)",
"var(--blue)",
"var(--purple)",
"var(--pink)",
];
/** A colour for `seed`: the same name always gets the same one. */
export function pickAccent(seed: string): string {
let hash = 0;
for (let i = 0; i < seed.length; i++)
hash = (hash * 31 + seed.charCodeAt(i)) | 0;
return AVATAR_ACCENTS[Math.abs(hash) % AVATAR_ACCENTS.length];
}
/**
* Lenient parse of authelia's `GET /api/user/info`, which wraps its answer in
* a `{status, data}` envelope. The menu is cosmetic, so a shape that is not
* recognised gives no name rather than an error.
*/
export function parseWhoAmI(body: unknown): WhoAmI {
const record = (body ?? {}) as Record<string, unknown>;
const data = (record.data ?? record) as Record<string, unknown>;
const displayName =
typeof data.display_name === "string" ? data.display_name : null;
const emails = Array.isArray(data.emails) ? data.emails : null;
const email =
emails && typeof emails[0] === "string" ? (emails[0] as string) : null;
return { displayName, email };
}
/** The name to show: the display name, else the e-mail, else none. */
export function labelOf(who: WhoAmI | null): string | null {
return who?.displayName || who?.email || null;
}
/** The letter in the avatar, or "?" when there is no name. */
export function initialOf(label: string | null): string {
return label ? label.trim().charAt(0).toUpperCase() : "?";
}
/** Authelia's settings and sign-out pages under `autheliaUrl`, if it is known. */
export function accountLinks(
autheliaUrl: string | null,
): { settings: string; logout: string } | null {
if (!autheliaUrl) return null;
// The trailing slash comes off so `/settings` appends cleanly.
const base = autheliaUrl.replace(/\/+$/, "");
return { settings: `${base}/settings`, logout: `${base}/logout` };
}