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]>
68 lines
2.8 KiB
JavaScript
68 lines
2.8 KiB
JavaScript
/* =====================================================================
|
|
db.js — SQLite-Datenbank (better-sqlite3), Ersatz für Cloudflare D1.
|
|
Führt beim ersten Start alle Migrationen aus ../cloudflare-worker/migrations/
|
|
in der ursprünglichen Reihenfolge aus (0001 Basis-Schema, 0002 app_settings,
|
|
0003 Supporter-Abo, 0004 TikTok/Discord-Spalten, 0005 Supporter-Abstimmungen)
|
|
— anders als bei VanVans Shop ist hier NIE alles in einer konsolidierten
|
|
schema.sql zusammengefasst worden, deshalb müssen wir hier tatsächlich alle
|
|
der Reihe nach anwenden.
|
|
===================================================================== */
|
|
|
|
import Database from "better-sqlite3";
|
|
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DB_PATH = process.env.DB_PATH || "/var/lib/dogfather-internal/dogfather-internal.db";
|
|
const MIGRATIONS_DIR = join(__dirname, "..", "cloudflare-worker", "migrations");
|
|
|
|
mkdirSync(dirname(DB_PATH), { recursive: true });
|
|
|
|
export const db = new Database(DB_PATH);
|
|
db.pragma("journal_mode = WAL");
|
|
db.pragma("foreign_keys = ON");
|
|
|
|
const MIGRATION_FILES = [
|
|
"0001_init.sql",
|
|
"0002_app_settings.sql",
|
|
"0003_supporter_abo.sql",
|
|
"0004_supporter_oauth_providers.sql",
|
|
"0005_supporter_polls.sql",
|
|
"0006_testimonials.sql",
|
|
"0007_team_members.sql",
|
|
"0008_team_members_rich.sql",
|
|
"0009_supporter_access_code.sql",
|
|
"0010_testimonials_avatar.sql",
|
|
// Webdesign-Bereich (22.08.2026): Anfragen, Kunden, Projekte, Dateien,
|
|
// Nachrichten, Änderungswünsche, pflegbare Inhalte, Änderungsverlauf.
|
|
// Alle Tabellen tragen das Präfix "wd_" und sind fachlich vollständig
|
|
// vom Universe getrennt — Begründung im Kopf der Migration.
|
|
"0011_webdesign.sql",
|
|
// Zahlungen im Webdesign-Bereich (22.08.2026): PayPal-Einmalzahlungen
|
|
// fuer Anzahlung/Restbetrag/Zusatzangebote, PayPal-Abo fuer die
|
|
// monatliche Betreuung, Webhook-Merkliste gegen Doppelverbuchung.
|
|
"0012_webdesign_zahlungen.sql",
|
|
];
|
|
|
|
function alreadyApplied(name) {
|
|
const row = db.prepare(`SELECT 1 FROM _migrations WHERE name = ?`).get(name);
|
|
return !!row;
|
|
}
|
|
|
|
export function initDb() {
|
|
db.exec(`CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`);
|
|
for (const file of MIGRATION_FILES) {
|
|
if (alreadyApplied(file)) continue;
|
|
const path = join(MIGRATIONS_DIR, file);
|
|
if (!existsSync(path)) {
|
|
console.warn(`⚠️ Migration ${file} nicht gefunden unter ${path}, übersprungen.`);
|
|
continue;
|
|
}
|
|
const sql = readFileSync(path, "utf-8");
|
|
db.exec(sql);
|
|
db.prepare(`INSERT INTO _migrations (name, applied_at) VALUES (?, ?)`).run(file, new Date().toISOString());
|
|
console.log(`Migration angewendet: ${file}`);
|
|
}
|
|
}
|