feat!: remove the shared demo account
The demo account — one shared user that whitelisted emails entered from the login page, reset on a schedule — goes end to end (owner decision, E34): demo.py and the reset job, users.is_demo and demo_whitelist (migration 0098 deletes the demo user, its audit rows, the reset job's override and notices, then drops the column and table; the downgrade restores the empty schema), the /auth/demo entry, the admin whitelist and reset, the Demo tab, the banner, the login page's "Try the demo", and every is_demo branch in routes, userscope, scheduler and the UI. require_human only ever refused the demo account, so its 87 uses become current_user (owner-approved); require_write_scope keeps its own check. The login page's "or" divider now shows only beside the Google button: without the demo entry under it, an instance without Google would have said "or" to nothing. A contract test (removedFeatures) fails on the word anywhere in the backend, frontend, e2e or scripts — seen red on a planted hit. Size ceilings tightened for the modules that shrank; admin.py is back under the limit.
This commit is contained in:
@@ -61,7 +61,7 @@ const SUBMIT_KEY: Record<Mode, string> = {
|
||||
};
|
||||
|
||||
// Public landing + authentication. One scrollable page: a hero with the auth card (email+
|
||||
// password, Google, explicit demo) over a short marketing pitch with feature cards. Shown
|
||||
// password, Google) over a short marketing pitch with feature cards. Shown
|
||||
// whenever the user isn't signed in (see App.tsx). The auth forms talk to /auth/* (see the
|
||||
// backend); errors are surfaced inline (the api calls use the quiet flag).
|
||||
export default function Welcome({ verify }: { verify: VerifyEntry }) {
|
||||
@@ -211,7 +211,7 @@ function FeatureCard({ icon: Icon, k }: { icon: LucideIcon; k: string }) {
|
||||
}
|
||||
|
||||
// Image with a graceful fallback: if the file isn't present yet, show a tasteful placeholder
|
||||
// frame instead of a broken image (the demo screenshots drop into frontend/public/welcome/).
|
||||
// frame instead of a broken image (the screenshots drop into frontend/public/welcome/).
|
||||
// When loaded it's a button that opens the full-size lightbox (onOpen).
|
||||
function Preview({
|
||||
src,
|
||||
@@ -446,13 +446,6 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
setDeadReset(false);
|
||||
}
|
||||
|
||||
// Explicit demo entry (replaces the old hidden probe): a labelled email field, still gated
|
||||
// by the demo whitelist server-side.
|
||||
const [showDemo, setShowDemo] = useState(false);
|
||||
const [demoEmail, setDemoEmail] = useState("");
|
||||
const [demoBusy, setDemoBusy] = useState(false);
|
||||
const [demoError, setDemoError] = useState("");
|
||||
|
||||
const reset = () => {
|
||||
setError("");
|
||||
setDone("");
|
||||
@@ -533,25 +526,6 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
async function onDemo(e: React.SubmitEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setDemoError("");
|
||||
const mail = demoEmail.trim().toLowerCase();
|
||||
if (!EMAIL_RE.test(mail)) {
|
||||
setDemoError(t("welcome.auth.invalidEmail"));
|
||||
return;
|
||||
}
|
||||
setDemoBusy(true);
|
||||
try {
|
||||
const r = await api.demoLogin(mail);
|
||||
if (r.authenticated) window.location.reload();
|
||||
else setDemoError(t("welcome.auth.demoDenied"));
|
||||
} catch {
|
||||
setDemoError(t("welcome.auth.demoDenied"));
|
||||
}
|
||||
setDemoBusy(false);
|
||||
}
|
||||
|
||||
// A named state owns the card's heading while it is up, so it reads as the card's subject rather
|
||||
// than a box sitting inside a heading that still says "Set a new password".
|
||||
const titleKey = blocked
|
||||
@@ -736,46 +710,18 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mode === "signin" && (
|
||||
{/* The divider separates two ways in, so it goes with the second one: on an instance with
|
||||
no Google sign-in there is nothing to say "or" to. */}
|
||||
{mode === "signin" && googleEnabled && (
|
||||
<>
|
||||
<div className="flex items-center gap-3 my-4 text-2xs uppercase tracking-wide text-muted">
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
{t("welcome.auth.or")}
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
{googleEnabled && (
|
||||
<LinkButton href="/auth/login" size="lg" fullWidth>
|
||||
{t("welcome.auth.google")}
|
||||
</LinkButton>
|
||||
)}
|
||||
{!showDemo ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
fullWidth
|
||||
className="mt-2"
|
||||
onClick={() => setShowDemo(true)}
|
||||
>
|
||||
{t("welcome.auth.tryDemo")}
|
||||
</Button>
|
||||
) : (
|
||||
<form onSubmit={onDemo} className="mt-2 flex flex-col gap-2">
|
||||
<p className="text-2xs text-muted">{t("welcome.auth.demoHint")}</p>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="email"
|
||||
value={demoEmail}
|
||||
onChange={(e) => setDemoEmail(e.target.value)}
|
||||
placeholder={t("welcome.auth.demoEmail")}
|
||||
/>
|
||||
<Button type="submit" variant="outline" size="lg" disabled={demoBusy}>
|
||||
{demoBusy ? t("welcome.auth.working") : t("welcome.auth.demoEnter")}
|
||||
</Button>
|
||||
</div>
|
||||
{demoError && <p className="text-xs text-danger">{demoError}</p>}
|
||||
</form>
|
||||
)}
|
||||
<LinkButton href="/auth/login" size="lg" fullWidth>
|
||||
{t("welcome.auth.google")}
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user