Files
dogfather-universe/server/test-webdesign-gate.mjs
DogFatherGitandClaude Opus 5 4b3ec450d5 Webdesign-Bereich: Fundament, oeffentliche Seiten, Zugangsschutz, PayPal
Umsetzung des "Website Masterplan" (16 Seiten) unter /webdesign.

Zugangsschutz mit EIGENER Schranke (server/webdesign-gate.js) statt gate.js:
gate.js laesst seit dem oeffentlichen Start am 21.08.2026 jeden durch, weil
die Pruefung auf SITE_PUBLIC_LAUNCH_AT als allererste Zeile steht. Haette man
/webdesign dahintergehaengt, waere der ausdruecklich nicht-oeffentliche Bereich
inklusive Preisen und spaeteren Kundendaten ab der ersten Sekunde fuer jeden
lesbar gewesen. Eigenes Sitzungs-Cookie, bereich="webdesign" im Token, damit
ein gueltiges Universe-Cookie hier NICHT gilt. 37/37 Tests.

Sieben oeffentliche Seiten in fuenf Sprachen (de, de-CH mit echtem Dialekt, en,
fr, pt). Preise, Zeitrahmen, Paketnamen und die 30-%-Regel stehen an genau
EINER Stelle in wd-core.js -- der Masterplan verlangt "ueberall
widerspruchsfrei", und vier Kopien laufen bei der ersten Preisaenderung
auseinander.

Als App installierbar auf Handy und PC. Der Service Worker speichert bewusst
KEINE HTML-Seite zwischen: nach dem Abmelden wuerden sonst geschuetzte Seiten
weiter ausgeliefert, ohne dass der Server je gefragt wird. 18/18 Tests.

Handy-Abnahme ueber alle Seiten in fuenf Breiten (320-1440) und fuenf Sprachen:
40/40. Der Test fand 35 echte Fehler (Touch-Ziele unter 44px), behoben im
Designsystem statt einzeln pro Seite.

PayPal (Wunsch 22.08.2026 "sofort auf meinem paypal"): Orders API mit
intent=CAPTURE, also sofortiger Einzug statt blosser Reservierung. Gebuehr und
Nettobetrag getrennt gespeichert. Betraege durchgehend als Ganzzahl in Cent.
Gefaelschte Webhooks werden abgewiesen. Fail closed solange Zugangsdaten
fehlen. 28/28 Tests gegen einen nachgebauten PayPal-Server.

Datenbank: 15 Tabellen mit Praefix wd_, fachlich vollstaendig vom Universe
getrennt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 18:06:52 +02:00

239 lines
10 KiB
JavaScript

/* =====================================================================
test-webdesign-gate.mjs — Selbsttest der Webdesign-Zugangsschranke.
Ausführen (aus dem Ordner server/):
node test-webdesign-gate.mjs
Startet einen eigenen Server auf einem freien Port mit Test-Codes und
spielt die sicherheitskritischen Fälle durch. Rührt weder die echte
.env noch den laufenden Dienst an.
Warum ein eigener Test und nicht "einmal im Browser klicken":
Die gefährlichen Fälle sind genau die, die man beim Klicken NICHT sieht —
ein Universe-Cookie, das hier akzeptiert wird; ein "next", das auf eine
fremde Domain zeigt; eine Sperre, die nicht greift.
===================================================================== */
process.env.SITE_ACCESS_SECRET = "test-geheimnis-nur-fuer-diesen-lauf";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1";
process.env.WEBDESIGN_STATUS = "vorabpruefung";
// Wichtig für den Kernfall: das Universe ist öffentlich, /webdesign nicht.
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const { default: express } = await import("express");
const { default: cookieParser } = await import("cookie-parser");
const { webdesignGate, wdSigniere, wdPruefe } = await import("./webdesign-gate.js");
const { signSession } = await import("./gate.js");
const app = express();
app.set("trust proxy", 1);
app.use(cookieParser());
app.use(express.json());
app.use(webdesignGate);
// Ersatz für express.static — antwortet nur, wenn die Schranke durchlässt.
app.use((req, res) => res.status(200).send("GEHEIMER INHALT"));
const server = app.listen(0, "127.0.0.1");
await new Promise((r) => server.once("listening", r));
const basis = `http://127.0.0.1:${server.address().port}`;
let bestanden = 0;
let gescheitert = 0;
function pruefe(name, bedingung, zusatz = "") {
if (bedingung) {
bestanden++;
console.log(` ok ${name}`);
} else {
gescheitert++;
console.log(` FEHLT ${name} ${zusatz}`);
}
}
async function hole(pfad, optionen = {}) {
const res = await fetch(basis + pfad, { redirect: "manual", ...optionen });
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch { /* kein JSON, egal */ }
return { res, text, json, cookies: res.headers.getSetCookie?.() || [] };
}
function cookieAus(cookies, name) {
const treffer = cookies.find((c) => c.startsWith(name + "="));
return treffer ? treffer.split(";")[0].split("=").slice(1).join("=") : null;
}
console.log("\n=== Webdesign-Zugangsschranke ===\n");
/* --- 1. Der Kernfall: Universe ist öffentlich, /webdesign trotzdem dicht --- */
console.log("1. Grundschutz");
{
const a = await hole("/webdesign/");
pruefe("ohne Code -> Umleitung zur Zugangswand", a.res.status === 302, `(war ${a.res.status})`);
pruefe("Umleitungsziel ist die Zugangswand",
(a.res.headers.get("location") || "").startsWith("/webdesign/zugang.html"));
pruefe("Inhalt wird NICHT mitgeschickt", !a.text.includes("GEHEIMER INHALT"));
const b = await hole("/webdesign/preise.html");
pruefe("auch Unterseiten sind dicht", b.res.status === 302);
const c = await hole("/webdesign/zugang.html");
pruefe("Zugangswand selbst ist erreichbar", c.res.status === 200);
const d = await hole("/index.html");
pruefe("Universe bleibt unberührt öffentlich", d.res.status === 200);
const e = await hole("/webdesign/");
pruefe("noindex-Header ist gesetzt",
(e.res.headers.get("x-robots-tag") || "").includes("noindex"));
}
/* --- 2. Anmeldung --- */
console.log("\n2. Anmeldung");
let sitzungDogi = null;
{
const falsch = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "irgendwas" }),
});
pruefe("falscher Code -> 401", falsch.res.status === 401);
const richtig = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "DOGI-TEST-1" }),
});
pruefe("richtiger Code -> ok", richtig.res.status === 200 && richtig.json?.ok === true);
pruefe("Rolle wird gemeldet", richtig.json?.rolle === "dogfather");
sitzungDogi = cookieAus(richtig.cookies, "dogiwd_session");
pruefe("Sitzungs-Cookie wird gesetzt", !!sitzungDogi);
pruefe("Cookie ist httpOnly", richtig.cookies.some((c) => c.startsWith("dogiwd_session=") && /httponly/i.test(c)));
pruefe("Cookie ist secure", richtig.cookies.some((c) => c.startsWith("dogiwd_session=") && /secure/i.test(c)));
pruefe("Cookie hat KEIN Ablaufdatum (nur Browser-Sitzung)",
richtig.cookies.some((c) => c.startsWith("dogiwd_session=") && !/max-age|expires/i.test(c)));
const vanvan = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "VANVAN-TEST-1" }),
});
pruefe("VanVans Code funktioniert gleichwertig", vanvan.json?.rolle === "vanvan");
const mit = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${sitzungDogi}` } });
pruefe("mit gültiger Sitzung -> Inhalt kommt", mit.res.status === 200 && mit.text.includes("GEHEIMER INHALT"));
}
/* --- 3. Weiterleitungsziel kann nicht auf eine fremde Seite zeigen --- */
console.log("\n3. Weiterleitungsziel");
{
const faelle = [
["//boese-seite.de", "protokoll-relative fremde Domain"],
["https://boese-seite.de", "vollstaendige fremde URL"],
["/webdesign/../../etc/passwd", "Pfadwechsel nach oben"],
["/index.html", "ausserhalb des Bereichs"],
["/webdesign\\boese", "Rueckwaertsschraegstrich"],
];
for (const [wert, was] of faelle) {
const a = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "DOGI-TEST-1", next: wert }),
});
pruefe(`abgewehrt: ${was}`, a.json?.next === "/webdesign/", `(wurde ${a.json?.next})`);
}
const gut = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "DOGI-TEST-1", next: "/webdesign/preise.html" }),
});
pruefe("eigener Pfad wird uebernommen", gut.json?.next === "/webdesign/preise.html");
}
/* --- 4. Ein Universe-Cookie darf hier NICHT gelten --- */
console.log("\n4. Trennung vom Universe-Zugang");
{
const universeToken = await signSession("dogi", process.env.SITE_ACCESS_SECRET);
const a = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${universeToken}` } });
pruefe("Universe-Sitzung oeffnet den Webdesign-Bereich NICHT", a.res.status === 302);
pruefe("wdPruefe lehnt Universe-Token ab", wdPruefe(universeToken, process.env.SITE_ACCESS_SECRET) === null);
const fremd = wdSigniere("dogfather", "ein-ganz-anderes-geheimnis");
const b = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${fremd}` } });
pruefe("mit falschem Geheimnis signiert -> abgelehnt", b.res.status === 302);
const verdreht = sitzungDogi.split(".")[0] + "." + "AAAA";
const c = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${verdreht}` } });
pruefe("verfaelschte Signatur -> abgelehnt", c.res.status === 302);
const abgelaufen = (() => {
const inhalt = Buffer.from(JSON.stringify({ bereich: "webdesign", rolle: "dogfather", exp: Date.now() - 1000 }))
.toString("base64url");
// absichtlich ohne gueltige Signatur -- muss doppelt scheitern
return inhalt + ".xxx";
})();
const d = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${abgelaufen}` } });
pruefe("abgelaufene/ungueltige Sitzung -> abgelehnt", d.res.status === 302);
}
/* --- 5. Fehlversuchs-Sperre --- */
console.log("\n5. Fehlversuchs-Sperre");
{
let letzte = null;
for (let i = 0; i < 6; i++) {
letzte = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "falsch-" + i }),
});
}
pruefe("nach fuenf Fehlversuchen greift die Sperre (429)", letzte.res.status === 429, `(war ${letzte.res.status})`);
const trotzRichtig = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: "DOGI-TEST-1" }),
});
pruefe("Sperre gilt auch fuer den richtigen Code", trotzRichtig.res.status === 429);
pruefe("Meldung nennt die Wartezeit", /Minute/.test(trotzRichtig.json?.error || ""));
const andere = await hole("/webdesign/zugang-pruefen", {
method: "POST",
headers: { "Content-Type": "application/json", "CF-Connecting-IP": "203.0.113.77" },
body: JSON.stringify({ code: "DOGI-TEST-1" }),
});
pruefe("andere Besucher sind NICHT mitgesperrt", andere.res.status === 200,
"(genau dieser Fehler trat am 19.08.2026 im Universe auf)");
}
/* --- 6. Statusmodi --- */
console.log("\n6. Zugangsstatus");
{
process.env.WEBDESIGN_STATUS = "entwurf";
const a = await hole("/webdesign/zugang-pruefen", {
method: "POST", headers: { "Content-Type": "application/json", "CF-Connecting-IP": "203.0.113.90" },
body: JSON.stringify({ code: "DOGI-TEST-1" }),
});
pruefe("ENTWURF: auch der richtige Code oeffnet nicht", a.res.status === 403);
process.env.WEBDESIGN_STATUS = "freigegeben";
const b = await hole("/webdesign/preise.html");
pruefe("FREIGEGEBEN: oeffentliche Seite ohne Code erreichbar", b.res.status === 200);
const c = await hole("/webdesign/verwaltung.html");
pruefe("FREIGEGEBEN: Verwaltung bleibt trotzdem geschuetzt", c.res.status === 302);
const d = await hole("/webdesign/preise.html");
pruefe("FREIGEGEBEN: kein noindex mehr", !(d.res.headers.get("x-robots-tag") || "").includes("noindex"));
process.env.WEBDESIGN_STATUS = "vorabpruefung";
}
/* --- 7. Ohne Geheimnis muss zugesperrt werden, nicht aufgesperrt --- */
console.log("\n7. Fehlende Einstellung");
{
const gemerkt = process.env.SITE_ACCESS_SECRET;
delete process.env.SITE_ACCESS_SECRET;
const a = await hole("/webdesign/");
pruefe("ohne Geheimnis -> 503 statt offener Tuer", a.res.status === 503, `(war ${a.res.status})`);
pruefe("Inhalt bleibt geheim", !a.text.includes("GEHEIMER INHALT"));
process.env.SITE_ACCESS_SECRET = gemerkt;
}
server.close();
console.log(`\n=== ${bestanden} bestanden, ${gescheitert} gescheitert ===\n`);
process.exit(gescheitert === 0 ? 0 : 1);