Zweite, tägliche Zugangsschranke nur für die Bestellverwaltung
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]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7e02879f66
commit
aff961816b
@@ -31,6 +31,10 @@
|
||||
ist nie für Kund:innen gedacht. /api/orders (Bestellung ANLEGEN beim Checkout) ist bewusst
|
||||
NICHT dauerhaft geschützt — das muss auch für echte Kund:innen erreichbar sein, sobald der
|
||||
Shop öffentlich ist, genau wie der Checkout selbst.
|
||||
- /verwaltung/* + /api/verwaltung/* verlangen ZUSÄTZLICH zum normalen 90-Tage-Seitenzugang
|
||||
noch einen zweiten, eigenen Tages-Code (siehe functions/_shared/verwaltung-auth.js,
|
||||
/verwaltung-code.html) — auf ausdrücklichen Wunsch, damit die Bestellübersicht auch bei
|
||||
einer kompromittierten/geteilten normalen Sitzung nicht dauerhaft offen bleibt.
|
||||
- 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.
|
||||
@@ -41,6 +45,7 @@
|
||||
===================================================================== */
|
||||
|
||||
import { COOKIE_NAME, ROLE_COOKIE_NAME, SESSION_TAGE, signSession, verifySession, getCookie } from "./_shared/auth.js";
|
||||
import { VERWALTUNG_COOKIE_NAME, VERWALTUNG_SESSION_STUNDEN, signVerwaltungSession, verifyVerwaltungSession } from "./_shared/verwaltung-auth.js";
|
||||
|
||||
/* Seiten, die auch ohne Zugangscode ausgeliefert werden (siehe Begründung unten in onRequest).
|
||||
Bewusst als feste, vollständige Liste statt "alles was mit X anfängt" — so kann hier nicht
|
||||
@@ -103,6 +108,39 @@ async function handleAuth(request, env) {
|
||||
return new Response(JSON.stringify({ ok: true, next }), { status: 200, headers });
|
||||
}
|
||||
|
||||
/* Gegenstück zu handleAuth() oben, aber für den zweiten, verwaltungseigenen Tages-Code —
|
||||
bewusst EIN gemeinsamer Code (kein Unterschied qciga/VanVan), da es hier nur um die
|
||||
zusätzliche Hürde geht, nicht um eine rollenabhängige Anzeige. */
|
||||
async function handleVerwaltungAuth(request, env) {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ ok: false, error: "Ungültige Anfrage." }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const code = (body.code || "").trim();
|
||||
const next = typeof body.next === "string" && body.next.startsWith("/") ? body.next : "/verwaltung/";
|
||||
|
||||
if (!env.VERWALTUNG_ACCESS_CODE || code !== env.VERWALTUNG_ACCESS_CODE) {
|
||||
return new Response(JSON.stringify({ ok: false, error: "Falscher Tages-Code." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const session = await signVerwaltungSession(env.VERWALTUNG_SESSION_SECRET);
|
||||
const maxAge = VERWALTUNG_SESSION_STUNDEN * 60 * 60;
|
||||
const headers = new Headers({ "Content-Type": "application/json" });
|
||||
headers.append(
|
||||
"Set-Cookie",
|
||||
`${VERWALTUNG_COOKIE_NAME}=${encodeURIComponent(session)}; Path=/; Max-Age=${maxAge}; HttpOnly; Secure; SameSite=Lax`
|
||||
);
|
||||
return new Response(JSON.stringify({ ok: true, next }), { status: 200, headers });
|
||||
}
|
||||
|
||||
export async function onRequest(context) {
|
||||
const { request, next, env } = context;
|
||||
const url = new URL(request.url);
|
||||
@@ -148,7 +186,44 @@ export async function onRequest(context) {
|
||||
const payload = sessionToken ? await verifySession(sessionToken, env.SITE_ACCESS_SECRET) : null;
|
||||
|
||||
if (payload) {
|
||||
return next();
|
||||
// Normaler Seitenzugang ist gültig — für alles außerhalb von /verwaltung/ reicht das.
|
||||
if (!immerGeschuetzterPfad) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Ab hier: /verwaltung/* oder /api/verwaltung/* MIT gültigem normalem Seitenzugang.
|
||||
// Die Eingabemaske für den zweiten Code + ihr eigener Auswertungs-Endpunkt müssen erreichbar
|
||||
// sein, BEVOR der zweite Code selbst geprüft wird — sonst käme hier niemand überhaupt zur
|
||||
// Eingabe. Gleiches "/verwaltung-code.html" <-> "/verwaltung-code"-Doppel wie bei gate.html
|
||||
// oben, aus demselben Grund (Cloudflare Pages html_handling).
|
||||
if (url.pathname === "/verwaltung-code.html" || url.pathname === "/verwaltung-code") {
|
||||
return next();
|
||||
}
|
||||
if (url.pathname === "/verwaltung-auth" && request.method === "POST") {
|
||||
return handleVerwaltungAuth(request, env);
|
||||
}
|
||||
|
||||
// Fail-open auch hier: fehlt das Secret komplett, lieber die zweite Schranke nicht scharf
|
||||
// schalten, als sich selbst auszusperren — die erste Schranke (normaler Seitenzugang)
|
||||
// schützt /verwaltung/ in diesem Fall ja immer noch.
|
||||
if (!env.VERWALTUNG_SESSION_SECRET || !env.VERWALTUNG_ACCESS_CODE) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const vwToken = getCookie(request, VERWALTUNG_COOKIE_NAME);
|
||||
const vwPayload = vwToken ? await verifyVerwaltungSession(vwToken, env.VERWALTUNG_SESSION_SECRET) : null;
|
||||
if (vwPayload) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return new Response(JSON.stringify({ ok: false, error: "Tages-Code für die Verwaltung erforderlich." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const vwNext = url.pathname + url.search;
|
||||
return Response.redirect(`${url.origin}/verwaltung-code.html?next=${encodeURIComponent(vwNext)}`, 302);
|
||||
}
|
||||
|
||||
// gate.html selbst muss ohne Session ladbar sein, sonst kommt niemand überhaupt bis zur
|
||||
|
||||
Reference in New Issue
Block a user