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]>
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
/* =====================================================================
|
||||
sw.js — Service Worker NUR für /webdesign/
|
||||
Macht den Bereich auf Handy und Computer als App installierbar und
|
||||
sorgt dafür, dass die Oberfläche auch bei wackeliger Verbindung
|
||||
sofort da ist.
|
||||
|
||||
⚠️ SICHERHEITSENTSCHEIDUNG, DIE HIER DEN AUSSCHLAG GIBT
|
||||
|
||||
Der Webdesign-Bereich ist zugangsgeschützt (server/webdesign-gate.js).
|
||||
Ein Service Worker, der HTML-Seiten zwischenspeichert, würde diesen
|
||||
Schutz aushebeln: Nach dem Abmelden — oder nachdem jemand anders das
|
||||
Gerät in die Hand nimmt — lieferte der Zwischenspeicher die geschützten
|
||||
Seiten samt Preisen, Anfragen und Kundendaten weiterhin aus, ohne dass
|
||||
der Server je gefragt wird. Genau das ist der klassische Fehler bei
|
||||
PWAs hinter einer Anmeldung.
|
||||
|
||||
Deshalb gilt hier strikt:
|
||||
- HTML-Seiten werden NIEMALS zwischengespeichert. Immer Netz.
|
||||
Ist kein Netz da, kommt eine schlichte Offline-Meldung — nicht die
|
||||
zuletzt gesehene, möglicherweise vertrauliche Seite.
|
||||
- Zwischengespeichert werden ausschließlich die neutralen Bausteine
|
||||
unter /assets/ (Stilvorlagen, Skripte, Logo, Icons). Die enthalten
|
||||
keine Kunden- oder Projektdaten und liegen ohnehin außerhalb der
|
||||
Zugangsschranke.
|
||||
- Antworten der API (postfach.dogfather-universe.com) werden
|
||||
ebenfalls nie zwischengespeichert.
|
||||
|
||||
Das kostet Offline-Komfort. Der ist hier aber weniger wert als die
|
||||
Zusicherung, dass nach dem Schließen der Sitzung wirklich nichts mehr
|
||||
einsehbar ist.
|
||||
===================================================================== */
|
||||
|
||||
/* Version im Namen: eine Änderung dieser Zeichenkette wirft beim nächsten
|
||||
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
|
||||
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
|
||||
Stand fest. */
|
||||
const CACHE_NAME = "dogfather-webdesign-v1";
|
||||
|
||||
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
|
||||
HTML-Datei in dieser Liste. */
|
||||
const GRUNDGERUEST = [
|
||||
"/assets/css/main.css",
|
||||
"/assets/css/webdesign.css",
|
||||
"/assets/js/wd-core.js",
|
||||
"/assets/img/logo-dogfather-transparent.png",
|
||||
"/assets/img/wd-app-192.png",
|
||||
"/assets/img/wd-app-512.png",
|
||||
];
|
||||
|
||||
self.addEventListener("install", (ereignis) => {
|
||||
ereignis.waitUntil(
|
||||
caches.open(CACHE_NAME).then((speicher) =>
|
||||
/* addAll bricht komplett ab, wenn EINE Datei fehlt — dann wäre der
|
||||
Service Worker gar nicht erst installiert und die App nicht
|
||||
installierbar. Deshalb jede Datei einzeln und Fehler schlucken. */
|
||||
Promise.all(
|
||||
GRUNDGERUEST.map((pfad) =>
|
||||
speicher.add(new Request(pfad, { cache: "reload" })).catch(() => {})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
/* Sofort übernehmen statt auf das Schließen aller Tabs zu warten —
|
||||
sonst bleibt nach einem Deploy tagelang die alte Fassung aktiv. */
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (ereignis) => {
|
||||
ereignis.waitUntil(
|
||||
caches.keys()
|
||||
.then((namen) => Promise.all(
|
||||
namen.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
/* Schlichte Offline-Meldung. Bewusst als erzeugte Antwort statt als
|
||||
Datei — so kann sie nie mit echtem Seiteninhalt verwechselt werden.
|
||||
Fünf Sprachen wären hier Überinstrumentierung: der Text erscheint nur
|
||||
ohne Internet und ist absichtlich sehr kurz gehalten. */
|
||||
function offlineSeite() {
|
||||
return new Response(
|
||||
`<!DOCTYPE html><html lang="de"><head><meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Keine Verbindung</title>
|
||||
<style>
|
||||
body{margin:0;min-height:100svh;display:flex;align-items:center;justify-content:center;
|
||||
background:#05070b;color:#eef4fb;font-family:system-ui,sans-serif;text-align:center;padding:2rem}
|
||||
.k{max-width:34ch}
|
||||
h1{font-size:1.4rem;margin:0 0 .6rem}
|
||||
p{color:#b3c2d4;line-height:1.6;margin:0 0 1.4rem}
|
||||
button{min-height:48px;padding:.8rem 1.6rem;border:none;border-radius:999px;
|
||||
background:#7fd0e8;color:#04070c;font-weight:700;font-size:1rem;cursor:pointer}
|
||||
</style></head><body>
|
||||
<div class="k">
|
||||
<h1>Keine Verbindung</h1>
|
||||
<p>Dogfather Webdesign braucht eine Internetverbindung. Sobald du wieder online bist, geht es normal weiter.</p>
|
||||
<button onclick="location.reload()">Erneut versuchen</button>
|
||||
</div></body></html>`,
|
||||
{ status: 503, headers: { "Content-Type": "text/html; charset=utf-8" } }
|
||||
);
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (ereignis) => {
|
||||
const anfrage = ereignis.request;
|
||||
|
||||
// Nur einfache Abrufe. Absendungen (POST) gehen immer direkt ans Netz.
|
||||
if (anfrage.method !== "GET") return;
|
||||
|
||||
const url = new URL(anfrage.url);
|
||||
|
||||
// Fremde Server (API, externe Bilder) nie anfassen.
|
||||
if (url.origin !== self.location.origin) return;
|
||||
|
||||
/* --- HTML: immer Netz, niemals zwischenspeichern (siehe Kopf) --- */
|
||||
const willHtml = anfrage.mode === "navigate" ||
|
||||
(anfrage.headers.get("accept") || "").includes("text/html");
|
||||
if (willHtml) {
|
||||
ereignis.respondWith(
|
||||
fetch(anfrage).catch(() => offlineSeite())
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/* --- Zugangs-Endpunkte niemals zwischenspeichern --- */
|
||||
if (url.pathname.startsWith("/webdesign/zugang-")) return;
|
||||
|
||||
/* --- Neutrale Bausteine: erst Zwischenspeicher, dann Netz ---
|
||||
"stale-while-revalidate": die gespeicherte Fassung geht sofort raus
|
||||
(die Seite baut sich ohne Wartezeit auf), im Hintergrund wird eine
|
||||
frische geholt und für das nächste Mal abgelegt. */
|
||||
if (url.pathname.startsWith("/assets/")) {
|
||||
ereignis.respondWith(
|
||||
caches.open(CACHE_NAME).then(async (speicher) => {
|
||||
const gespeichert = await speicher.match(anfrage);
|
||||
const ausDemNetz = fetch(anfrage)
|
||||
.then((antwort) => {
|
||||
/* Nur vollständige, eigene Antworten ablegen. Teilantworten
|
||||
(206, z.B. bei Video-Sprüngen) lassen sich nicht sinnvoll
|
||||
zwischenspeichern und würden den Speicher beschädigen. */
|
||||
if (antwort && antwort.status === 200 && antwort.type === "basic") {
|
||||
speicher.put(anfrage, antwort.clone());
|
||||
}
|
||||
return antwort;
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
return gespeichert || (await ausDemNetz) || Response.error();
|
||||
})
|
||||
);
|
||||
}
|
||||
/* Alles Übrige: kein respondWith -> der Browser macht es ganz normal
|
||||
selbst. Das ist die sicherste Voreinstellung. */
|
||||
});
|
||||
Reference in New Issue
Block a user