Files
threadcount-community/app/ops/login/LoginForm.tsx
T
ThreadCount 1bc2de655a ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
2026-09-13 08:45:19 +10:00

74 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
/* The break-glass door.
*
* Single sign-on through Cloudflare Access and Authentik is the front door (page.tsx hands off to
* it); this password form is the fire escape, for the day one of those is down. So it must not
* depend on either of them — which is why there is no Turnstile here. The sitekey is bound to the
* product's domain and compiled in at build time; on this hostname the widget would refuse and
* report a generic "security check failed", discovered during the exact incident in which this
* form is needed. The route behind it rate-limits failures under its own keys instead.
*
* A full navigation on success, not a router push: /ops is server-rendered behind the cookie
* that was just set, and a prefetched copy fetched before it existed would land a signed-in
* person on the sign-in screen. */
export default function LoginForm({ ssoFailed }: { ssoFailed: boolean }) {
const [email, setEmail] = useState("");
const [pw, setPw] = useState("");
const [code, setCode] = useState("");
const [needCode, setNeedCode] = useState(false);
const [err, setErr] = useState("");
const [busy, setBusy] = useState(false);
async function submit(e: React.FormEvent) {
e.preventDefault();
if (!email.trim() || !pw) { setErr("Enter your email and password."); return; }
setBusy(true); setErr("");
try {
const r = await fetch("/api/ops/auth/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password: pw, code: code || undefined }) });
const j = await r.json().catch(() => ({}));
if (!r.ok) {
if (j.needCode) { setNeedCode(true); setErr(j.error || "Enter the code from your authenticator app."); return; }
setErr(j.error || "Email or password doesnt match."); return;
}
window.location.assign("/ops");
} catch {
setErr("No connection — check the network and try again.");
} finally {
setBusy(false);
}
}
const label: React.CSSProperties = { display: "block", fontSize: 11, fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--color-neutral-600)", margin: "14px 0 6px" };
return (
<div style={{ maxWidth: 400, margin: "64px auto", padding: "0 16px" }}>
<div className="sec">Operations console</div>
<h1 className="h1" style={{ marginTop: 12 }}>Sign in</h1>
{ssoFailed && (
<div role="alert" style={{ border: "2px solid var(--color-accent)", padding: "8px 12px", fontSize: 13, fontWeight: 600, color: "var(--color-accent-700)", marginTop: 12 }}>
Single sign-on could not complete. Sign in with your operator password below.
</div>
)}
<form onSubmit={submit}>
<label style={label} htmlFor="ops-email">Email</label>
<input id="ops-email" className="input" type="email" autoComplete="username" autoCapitalize="none" spellCheck={false} value={email} onChange={(e) => { setEmail(e.target.value); setErr(""); }} style={{ width: "100%" }} />
<label style={label} htmlFor="ops-pw">Password</label>
<input id="ops-pw" className="input" type="password" autoComplete="current-password" value={pw} onChange={(e) => { setPw(e.target.value); setErr(""); }} style={{ width: "100%" }} />
{needCode && (
<>
<label style={label} htmlFor="ops-code">Authenticator code</label>
<input id="ops-code" className="input" inputMode="numeric" autoComplete="one-time-code" value={code} onChange={(e) => { setCode(e.target.value); setErr(""); }} placeholder="000000" style={{ width: "100%" }} />
</>
)}
{err && <div role="alert" style={{ border: "2px solid var(--color-accent)", padding: "8px 12px", fontSize: 13, fontWeight: 600, color: "var(--color-accent-700)", marginTop: 12 }}>{err}</div>}
<button className="btn btn-primary" type="submit" disabled={busy} style={{ marginTop: 16, width: "100%" }}>{busy ? "Signing in…" : "Sign in"}</button>
</form>
<p style={{ fontSize: 12, color: "var(--color-neutral-700)", marginTop: 16, lineHeight: 1.6 }}>
This is the password door. Single sign-on is the usual way in; use this when it isnt available.
</p>
</div>
);
}