import type { FormEvent } from "react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { AppBackground } from "../components/AppBackground"; import { Alert } from "../components/ui"; import { ThemeSwitch } from "../theme"; import type { AdminSession, PublicBranding } from "../types"; export function LoginPage({ onLogin }: { onLogin: (session: AdminSession) => void }) { // Deliberately not pre-filled: the built-in operator is a default name, not a // default identity, and typing it is the difference between choosing to use // it and drifting into it. The server rejects a blank username either way. const [username, setUsername] = useState(""); const [secret, setSecret] = useState(""); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); // Whose server this is. Fetched without a session -- the name and icon are // already public from owpengram-server's client endpoints -- and left null on // failure so the panel simply keeps its own branding. const [branding, setBranding] = useState(null); const [iconFailed, setIconFailed] = useState(false); useEffect(() => { api.publicBranding().then(setBranding).catch(() => undefined); }, []); const serverName = branding?.name?.trim() || "OwpenGram"; const iconSrc = branding?.has_icon && !iconFailed ? api.publicIconURL() : "/logo.png"; async function submit(event: FormEvent) { event.preventDefault(); setBusy(true); setError(""); try { // api.login remembers the CSRF token and tells us the sign-in worked. const result = await api.login(secret, username); // The session itself is then read from /api/session rather than assembled // out of the login answer. The login response carries only the actor and // the permissions, so building a session from it silently dropped the // build info, the API layers and the third-party-verification flag -- // which is why the sidebar footer was blank until the page was reloaded. // One endpoint decides what a session is. try { onLogin(await api.session()); } catch { // Signed in, but the follow-up read failed. Falling back to what the // login answer does carry beats bouncing someone back to a login form // they have already passed; a reload fills in the rest. onLogin({ actor: result.actor, permissions: result.permissions ?? [] }); } } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } return (
{serverName} setIconFailed(true)} /> {serverName} {"Admin Console"}

{"Operations Admin"}

{"Enter credentials to open the console."}

{error && {error}}
); }