feat(demo): login trigger, feature gating + admin UI (HU/EN/DE)

Login page quietly probes /auth/demo (debounced) as a valid email is
typed/pasted and reloads into the app on a match — no visible button.
Demo sessions default to the whole library, get a one-time shared-account
warning, never see the YouTube-connect onboarding/access UI or sync
actions, and admins get a demo whitelist + reset panel in Settings.
This commit is contained in:
npeter83 2026-06-16 09:17:34 +02:00
parent 747c8d20ce
commit 281062fe33
11 changed files with 307 additions and 62 deletions

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { api } from "../lib/api";
import { setLanguage, type LangCode } from "../i18n";
@ -6,6 +6,8 @@ import LanguageSwitcher from "./LanguageSwitcher";
type Phase = "idle" | "sending" | "requested" | "approved" | "error";
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
export default function Login() {
const { t, i18n } = useTranslation();
@ -18,6 +20,28 @@ export default function Login() {
const [phase, setPhase] = useState<Phase>(bounced === "requested" ? "requested" : "idle");
const [error, setError] = useState("");
// Hidden demo entry: a whitelisted email signs straight into the shared demo account, with
// no button to give it away. We watch the field and — once it holds a syntactically valid
// email and typing has settled — quietly probe /auth/demo. A match sets the session, so we
// reload into the app; a non-match does nothing visible (the normal "request access" flow
// stays available). The server is rate-limited and answers uniformly, so this can't be
// hammered as an oracle. Skipped while the request-access form is mid-submit/done.
useEffect(() => {
const candidate = email.trim().toLowerCase();
const settled = phase === "requested" || phase === "approved";
if (phase === "sending" || settled || !EMAIL_RE.test(candidate)) return;
const timer = setTimeout(() => {
api
.demoLogin(candidate)
.then((r) => {
if (r.authenticated) window.location.reload();
})
.catch(() => {});
}, 700);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [email, phase]);
async function submit(e: React.FormEvent) {
e.preventDefault();
setError("");