diff --git a/functions/_middleware.js b/functions/_middleware.js index 1d5918e..7627fcf 100644 --- a/functions/_middleware.js +++ b/functions/_middleware.js @@ -44,6 +44,8 @@ - API-Routen (/api/*) bekommen bei fehlender Sitzung eine 401-JSON-Antwort statt der 302-Weiterleitung zur Zugangscode-Seite — ein fetch() im Checkout/Verwaltung-Skript soll einen klaren Fehler sehen, nicht versehentlich HTML als Antwort bekommen. + - POST /gate-logout (04.08.2026, Abmelde-Button auf /verwaltung/) löscht alle drei + Zugangs-Cookies auf einmal (normale Sitzung + Rollen-Anzeige + tägliche Verwaltungs-Sitzung). Die Signatur (HMAC-SHA256 über Web Crypto, in jeder Workers/Pages-Runtime eingebaut) verhindert, dass jemand sich einfach selbst ein @@ -161,6 +163,21 @@ async function handleVerwaltungAuth(request, env) { return new Response(JSON.stringify({ ok: true, next }), { status: 200, headers }); } +/* Gegenstück zu handleAuth()/handleVerwaltungAuth() oben: löscht ALLE drei Zugangs-Cookies auf + einmal (normale 90-Tage-Sitzung, die Rollen-Anzeige dafür, UND die tägliche Verwaltungs-Sitzung) + — auf ausdrücklichen Wunsch (04.08.2026) als eigener Abmelde-Button auf /verwaltung/. Löscht + bewusst auch die Verwaltungs-Sitzung mit, nicht nur die normale, damit "abmelden" wirklich + "abmelden von allem" bedeutet und nicht nur einen Teil der Schranke offen lässt. */ +function handleLogout() { + const headers = new Headers({ "Content-Type": "application/json" }); + const loeschen = (name, httpOnly) => + `${name}=; Path=/; Max-Age=0; ${httpOnly ? "HttpOnly; " : ""}Secure; SameSite=Lax`; + headers.append("Set-Cookie", loeschen(COOKIE_NAME, true)); + headers.append("Set-Cookie", loeschen(ROLE_COOKIE_NAME, false)); + headers.append("Set-Cookie", loeschen(VERWALTUNG_COOKIE_NAME, true)); + return new Response(JSON.stringify({ ok: true }), { status: 200, headers }); +} + export async function onRequest(context) { const { request, next, env } = context; const url = new URL(request.url); @@ -170,6 +187,13 @@ export async function onRequest(context) { return next(); } + // Abmelden muss IMMER funktionieren — unabhängig von SITE_PUBLIC, Hostname oder ob die + // Sitzung technisch schon abgelaufen/ungültig ist. Deshalb ganz oben geprüft, vor jeder + // sonstigen Weiche unten. + if (url.pathname === "/gate-logout" && request.method === "POST") { + return handleLogout(); + } + // Rechtstexte (Impressum, Datenschutz, AGB, Widerruf) bleiben IMMER ohne Zugangscode // erreichbar. Grund: PayPal prüft vor der Freischaltung von "Log in with PayPal" die // angegebene Datenschutz- und AGB-Adresse tatsächlich nach — hinter der Zugangsschranke diff --git a/src/pages/verwaltung/index.astro b/src/pages/verwaltung/index.astro index 61516fc..9da75a4 100644 --- a/src/pages/verwaltung/index.astro +++ b/src/pages/verwaltung/index.astro @@ -50,6 +50,17 @@ const lang: Locale = "de"; so, auch wenn der Rest der Seite mal öffentlich wird. Alles unten sind ECHTE Bestellungen aus der Datenbank, live vom Checkout befüllt.

+ {/* Abmelde-Button (04.08.2026, auf ausdrücklichen Wunsch): rechts über der Hero-Kiste, + eigens "speziell" gestaltet (wandernder Verlaufsrand + Funkeln, siehe .verwaltung-logout-btn + in global.css) — passend zum Rest der Seite, statt eines schlichten Standard-Buttons. + Löscht beim Klick ALLE drei Zugangs-Cookies (normale Sitzung, Rollen-Anzeige, tägliche + Verwaltungs-Sitzung) über POST /gate-logout und springt danach zur Zugangscode-Seite. */} +
+ +
@@ -480,6 +491,18 @@ const lang: Locale = "de"; }); aktualisiereToggleUI(); + // ── Abmelden: löscht serverseitig alle drei Zugangs-Cookies (siehe POST /gate-logout in + // functions/_middleware.js) und springt danach zur Zugangscode-Seite. Läuft bewusst OHNE + // Rückfragedialog (gleiches Verhalten wie der Logout-Button im Kundenkonto). ── + const logoutBtn = document.getElementById("verwaltung-logout-btn"); + logoutBtn?.addEventListener("click", () => { + logoutBtn.disabled = true; + logoutBtn.textContent = "Wird abgemeldet …"; + fetch("/gate-logout", { method: "POST" }) + .catch(() => {}) + .finally(() => { window.location.href = "/gate"; }); + }); + // ── Meilenstein-Feier: Konfetti + Banner, sobald eine runde Bestell-Gesamtzahl erreicht wird // (10er-Schritte bis 100, 25er bis 500, danach 50er). Zählt ALLE Bestellungen (auch // stornierte/noch unbezahlte) — es geht um "die wievielte Bestellung ist reingekommen", diff --git a/src/styles/global.css b/src/styles/global.css index a4b7e5c..a179514 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -3453,6 +3453,52 @@ a:focus-visible, button:focus-visible { .verwaltung-box-chevron { transition: none; } } +/* Abmelde-Button (04.08.2026, auf ausdrücklichen Wunsch): sitzt in einer eigenen Zeile rechts + über der Hero-Kiste. Eigene Zeile statt einfach in die Hero-Kiste gepackt — damit er auch dann + sofort auffindbar bleibt, wenn die Kiste selbst mal umgebaut wird. */ +.verwaltung-topbar { display: flex; justify-content: flex-end; margin-top: 0.8rem; } + +/* Bewusst genauso "besonders" gestaltet wie die Hero-Kiste direkt darunter (wandernder + Verlaufsrand in denselben drei Markenfarben + Funkeln) statt eines schlichten Standard-Buttons + — auf ausdrücklichen Wunsch ("der Button soll sehr speziell sein"). */ +.verwaltung-logout-btn { + position: relative; + overflow: hidden; + display: inline-flex; + align-items: center; + gap: 0.5em; + padding: 0.65rem 1.3rem; + border-radius: 999px; + border: 1.5px solid transparent; + background: + linear-gradient(155deg, var(--c-anthracite-light), var(--c-anthracite)) padding-box, + linear-gradient(120deg, #5fd3ec, #9b7fd6, #e3b23c, #5fd3ec) border-box; + background-size: 100% 100%, 300% 100%; + animation: verwaltung-hero-shift 10s ease-in-out infinite; + color: var(--c-text); + font-family: inherit; + font-weight: 600; + font-size: 0.92rem; + cursor: pointer; + transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease); +} +.verwaltung-logout-btn:hover { + transform: translateY(-1px); + box-shadow: 0 8px 22px -8px color-mix(in srgb, #9b7fd6 55%, transparent); +} +.verwaltung-logout-btn:disabled { cursor: default; opacity: 0.75; transform: none; box-shadow: none; } +.verwaltung-logout-sparkle { + position: absolute; + top: 0.2rem; + right: 0.5rem; + font-size: 0.7rem; + pointer-events: none; + animation: account-nav-sparkle-twinkle 2.6s ease-in-out infinite; +} +@media (prefers-reduced-motion: reduce) { + .verwaltung-logout-btn, .verwaltung-logout-sparkle { animation: none; } +} + /* Hero-Kopf: dieselbe "besondere" Formsprache wie die anderen Premium-Karten der Seite (wanderndes Verlaufslicht + pulsierender Rand + Funkeln), passend zu einer Seite, die "alles übertrifft" — nur dezenter/edler gehalten, weil es ein Arbeits-Dashboard ist, kein Verkaufs-