Auf ausdrücklichen Wunsch: /verwaltung/ + /api/verwaltung/* verlangen jetzt ZUSÄTZLICH zum normalen 90-Tage-Seitenzugang einen zweiten, eigenen Code, den nur qciga und VanVan kennen — und der nach 24 Stunden automatisch abläuft (muss also einmal täglich neu eingegeben werden). Der Rest der Seite ist davon unberührt, nur die Bestellübersicht ist betroffen. Bewusst als eigenständiges System (eigenes Cookie vandiy_verwaltung_session, eigenes Geheimnis VERWALTUNG_SESSION_SECRET, eigener HMAC-Signaturcode statt geteiltem mit dem normalen Seitenzugang) — gleiches Trennungsprinzip wie bei den Kundenkonten: ein Leck bei einer Sitzungsart darf nie die andere gefährden. Neue Dateien: functions/_shared/verwaltung-auth.js (Session-Logik), public/verwaltung-code.html (Eingabemaske im selben Stil wie die Haupt-Zugangsseite). functions/_middleware.js prüft nach erfolgreicher Haupt-Sitzung zusätzlich diese zweite Sitzung für /verwaltung*-Pfade. Beide API-Routen (orders.js, orders/[id].js) prüfen die zweite Schranke zusätzlich eigenständig, nach demselben Verteidigungs-in-der-Tiefe-Prinzip wie die bestehende Haupt-Prüfung. Lokal vollständig durchgetestet (wrangler pages dev): kompletter Ablauf ohne jeden Zugang -> Haupt-Gate -> Redirect zum Tages-Code -> falscher Code abgelehnt -> richtiger Code setzt 24-Stunden-Cookie -> Seite und API danach erreichbar. Bestätigt: der Rest der Seite bleibt unberührt (kein Tages-Code nötig), UND dass VanVan mit nur dem Haupt-Zugang (ohne Tages-Code) ebenfalls noch ausgesperrt bleibt, wie gewünscht. Zwei neue Cloudflare-Secrets in Produktion gesetzt: VERWALTUNG_SESSION_SECRET (zufällig erzeugt) und VERWALTUNG_ACCESS_CODE (der eigentliche Tages-Code, an qciga und VanVan zu verteilen). Co-Authored-By: Claude Sonnet 5 <[email protected]>
175 lines
5.6 KiB
HTML
175 lines
5.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Tages-Code — Bestellverwaltung</title>
|
|
<meta name="robots" content="noindex, nofollow" />
|
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
<style>
|
|
:root {
|
|
--c-black: #0a0910;
|
|
--c-anthracite: #1b1a22;
|
|
--c-anthracite-light: #252333;
|
|
--c-text: #f4f1f7;
|
|
--c-text-muted: #b7aec4;
|
|
--c-accent: #e3b23c;
|
|
--c-accent-hover: #c99527;
|
|
--c-purple-glow: #9b7fd6;
|
|
--radius-l: 22px;
|
|
--radius-m: 16px;
|
|
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { height: 100%; }
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1.5rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
background: var(--c-black);
|
|
background-image: radial-gradient(60% 50% at 15% 10%, color-mix(in srgb, var(--c-purple-glow) 22%, transparent), transparent 70%),
|
|
radial-gradient(50% 45% at 85% 90%, color-mix(in srgb, var(--c-accent) 16%, transparent), transparent 70%);
|
|
color: var(--c-text);
|
|
font-family: "Inter", "Manrope", system-ui, sans-serif;
|
|
line-height: 1.6;
|
|
}
|
|
.gate-box {
|
|
position: relative;
|
|
max-width: 430px;
|
|
width: 100%;
|
|
text-align: center;
|
|
padding: 2.4rem 2rem;
|
|
border-radius: var(--radius-l);
|
|
border: 1.5px solid transparent;
|
|
background:
|
|
linear-gradient(165deg, var(--c-anthracite-light), var(--c-anthracite)) padding-box,
|
|
linear-gradient(120deg, #e3b23c, #9b7fd6, #5fd3ec, #e3b23c) border-box;
|
|
background-size: 100% 100%, 300% 100%;
|
|
animation: gate-border-shift 9s ease-in-out infinite;
|
|
box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.7);
|
|
}
|
|
@keyframes gate-border-shift {
|
|
0%, 100% { background-position: 0 0, 0% 50%; }
|
|
50% { background-position: 0 0, 100% 50%; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) { .gate-box { animation: none; } }
|
|
.gate-box > * { position: relative; z-index: 1; }
|
|
.gate-eyebrow {
|
|
display: inline-block;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
color: var(--c-accent);
|
|
background: color-mix(in srgb, var(--c-accent) 16%, transparent);
|
|
border: 1px solid color-mix(in srgb, var(--c-accent) 40%, transparent);
|
|
border-radius: 999px;
|
|
padding: 0.3rem 0.9rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.gate-box h1 {
|
|
font-family: "Fraunces", "Playfair Display", Georgia, serif;
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin: 0 0 0.6rem;
|
|
line-height: 1.2;
|
|
}
|
|
.gate-box p {
|
|
margin: 0 0 1.4rem;
|
|
color: var(--c-text-muted);
|
|
font-size: 0.94rem;
|
|
}
|
|
.gate-box input {
|
|
width: 100%;
|
|
padding: 0.9rem 1rem;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
letter-spacing: 0.08em;
|
|
font-size: 1.05rem;
|
|
margin-bottom: 0.9rem;
|
|
background: rgba(244, 241, 247, 0.05);
|
|
border: 1.5px solid rgba(244, 241, 247, 0.16);
|
|
color: var(--c-text);
|
|
font-family: inherit;
|
|
}
|
|
.gate-box input:focus {
|
|
outline: none;
|
|
border-color: var(--c-accent);
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 25%, transparent);
|
|
}
|
|
.gate-box button {
|
|
width: 100%;
|
|
padding: 0.85rem 1rem;
|
|
border-radius: 12px;
|
|
border: none;
|
|
background: linear-gradient(120deg, var(--c-accent), var(--c-accent-hover));
|
|
color: var(--c-black);
|
|
font-weight: 800;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
|
}
|
|
.gate-box button:hover { transform: translateY(-2px); box-shadow: 0 10px 24px -10px color-mix(in srgb, var(--c-accent) 60%, transparent); }
|
|
.gate-fehler {
|
|
color: #ff9a9a;
|
|
font-size: 0.88rem;
|
|
margin: 0.9rem 0 0;
|
|
display: none;
|
|
}
|
|
.gate-fehler.zeige { display: block; }
|
|
.gate-zurueck { display: inline-block; margin-top: 1.4rem; font-size: 0.85rem; color: var(--c-text-muted); }
|
|
.gate-zurueck a { color: inherit; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="gate-box">
|
|
<span class="gate-eyebrow">🔒 Tages-Code</span>
|
|
<h1>Bestellverwaltung</h1>
|
|
<p>Diese Seite ist zusätzlich mit einem eigenen Code geschützt, den nur ihr beide kennt —
|
|
er muss einmal pro Tag neu eingegeben werden.</p>
|
|
<form id="vw-form">
|
|
<input type="text" id="vw-code" placeholder="Tages-Code" autocomplete="off" autocapitalize="off" spellcheck="false" autofocus />
|
|
<button type="submit">Weiter</button>
|
|
</form>
|
|
<p class="gate-fehler" id="vw-fehler"></p>
|
|
<p class="gate-zurueck"><a href="/">← Zurück zur Startseite</a></p>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
const params = new URLSearchParams(location.search);
|
|
const next = params.get("next") || "/verwaltung/";
|
|
const form = document.getElementById("vw-form");
|
|
const fehlerEl = document.getElementById("vw-fehler");
|
|
form.addEventListener("submit", async function (e) {
|
|
e.preventDefault();
|
|
fehlerEl.classList.remove("zeige");
|
|
const code = document.getElementById("vw-code").value.trim();
|
|
if (!code) return;
|
|
try {
|
|
const res = await fetch("/verwaltung-auth", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ code, next }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok || !data.ok) {
|
|
fehlerEl.textContent = (data && data.error) || "Falscher Tages-Code.";
|
|
fehlerEl.classList.add("zeige");
|
|
return;
|
|
}
|
|
location.href = data.next || "/verwaltung/";
|
|
} catch {
|
|
fehlerEl.textContent = "Verbindung fehlgeschlagen. Bitte nochmal versuchen.";
|
|
fehlerEl.classList.add("zeige");
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|