Verwaltung: eigens gestalteter Abmelde-Button rechts über der Hero-Kiste

Neuer Endpunkt POST /gate-logout (functions/_middleware.js) löscht alle drei Zugangs-Cookies auf
einmal (normale 90-Tage-Sitzung, Rollen-Anzeige, tägliche Verwaltungs-Sitzung) — "abmelden"
bedeutet also wirklich abmelden von allem, nicht nur einen Teil der Schranke offen lassen. Button
im selben "besonderen" Look wie die Hero-Kiste darunter (wandernder Verlaufsrand in den drei
Markenfarben + Funkeln) statt eines schlichten Standard-Buttons.

Per curl (Cookie-Löschung + anschließende 302-Sperre auf /verwaltung/ und /) und per echtem
Browser-Klicktest (Playwright) end-to-end verifiziert.
This commit is contained in:
qcigano
2026-08-04 15:49:00 +02:00
parent ea9a2954d0
commit 99ddd43519
3 changed files with 93 additions and 0 deletions
+24
View File
@@ -44,6 +44,8 @@
- API-Routen (/api/*) bekommen bei fehlender Sitzung eine 401-JSON-Antwort statt der - 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 302-Weiterleitung zur Zugangscode-Seite — ein fetch() im Checkout/Verwaltung-Skript soll
einen klaren Fehler sehen, nicht versehentlich HTML als Antwort bekommen. 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 Die Signatur (HMAC-SHA256 über Web Crypto, in jeder Workers/Pages-Runtime
eingebaut) verhindert, dass jemand sich einfach selbst ein 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 }); 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) { export async function onRequest(context) {
const { request, next, env } = context; const { request, next, env } = context;
const url = new URL(request.url); const url = new URL(request.url);
@@ -170,6 +187,13 @@ export async function onRequest(context) {
return next(); 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 // Rechtstexte (Impressum, Datenschutz, AGB, Widerruf) bleiben IMMER ohne Zugangscode
// erreichbar. Grund: PayPal prüft vor der Freischaltung von "Log in with PayPal" die // 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 // angegebene Datenschutz- und AGB-Adresse tatsächlich nach — hinter der Zugangsschranke
+23
View File
@@ -50,6 +50,17 @@ const lang: Locale = "de";
so, auch wenn der Rest der Seite mal öffentlich wird. Alles unten sind ECHTE so, auch wenn der Rest der Seite mal öffentlich wird. Alles unten sind ECHTE
Bestellungen aus der Datenbank, live vom Checkout befüllt. Bestellungen aus der Datenbank, live vom Checkout befüllt.
</p> </p>
{/* 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. */}
<div class="verwaltung-topbar">
<button type="button" class="verwaltung-logout-btn" id="verwaltung-logout-btn">
<span class="verwaltung-logout-sparkle" aria-hidden="true">✨</span>
<span aria-hidden="true">🚪</span> Abmelden
</button>
</div>
<div class="verwaltung-hero"> <div class="verwaltung-hero">
<span class="verwaltung-hero-sparkle verwaltung-hero-sparkle-1" aria-hidden="true">✨</span> <span class="verwaltung-hero-sparkle verwaltung-hero-sparkle-1" aria-hidden="true">✨</span>
<span class="verwaltung-hero-sparkle verwaltung-hero-sparkle-2" aria-hidden="true">✨</span> <span class="verwaltung-hero-sparkle verwaltung-hero-sparkle-2" aria-hidden="true">✨</span>
@@ -480,6 +491,18 @@ const lang: Locale = "de";
}); });
aktualisiereToggleUI(); 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 // ── 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 // (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", // stornierte/noch unbezahlte) — es geht um "die wievielte Bestellung ist reingekommen",
+46
View File
@@ -3453,6 +3453,52 @@ a:focus-visible, button:focus-visible {
.verwaltung-box-chevron { transition: none; } .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 /* Hero-Kopf: dieselbe "besondere" Formsprache wie die anderen Premium-Karten der Seite
(wanderndes Verlaufslicht + pulsierender Rand + Funkeln), passend zu einer Seite, die "alles (wanderndes Verlaufslicht + pulsierender Rand + Funkeln), passend zu einer Seite, die "alles
übertrifft" — nur dezenter/edler gehalten, weil es ein Arbeits-Dashboard ist, kein Verkaufs- übertrifft" — nur dezenter/edler gehalten, weil es ein Arbeits-Dashboard ist, kein Verkaufs-