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:
@@ -0,0 +1,321 @@
|
||||
-- =====================================================================
|
||||
-- 0011_webdesign.sql — Datenbank für den Bereich /webdesign
|
||||
-- (Masterplan S.10 Lead-System, S.12 Kundenportal, S.13 Verwaltung)
|
||||
--
|
||||
-- Warum alles mit dem Präfix "wd_":
|
||||
-- Diese Datenbank trägt bereits das Universe (Team, Bewerbungen,
|
||||
-- Supporter, Abstimmungen, Testimonials). Der Webdesign-Bereich ist ein
|
||||
-- eigenes Geschäft mit eigenen Kunden — ohne Präfix wäre in einem Jahr
|
||||
-- nicht mehr erkennbar, welche "users"-Tabelle wozu gehört, und ein
|
||||
-- versehentlicher JOIN über zwei fachlich getrennte Welten ist der
|
||||
-- Anfang jedes Datenlecks.
|
||||
--
|
||||
-- Grundsätze:
|
||||
-- - Löschen wird vermieden. Kritische Datensätze werden archiviert
|
||||
-- (Masterplan S.13: "Kritische Löschungen werden zunächst archiviert").
|
||||
-- - Jede Tabelle mit Kundenbezug hat eine kunde_id, über die serverseitig
|
||||
-- gefiltert wird. Ohne diese Spalte gibt es keine saubere Trennung.
|
||||
-- - Zeitstempel immer als ISO-8601-Text in UTC (wie im restlichen
|
||||
-- Projekt), damit Sortierung und Vergleich rein textuell funktionieren.
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- ANFRAGEN (Masterplan S.10)
|
||||
-- Entsteht durch das öffentliche Formular. Wird NICHT gelöscht, sondern
|
||||
-- archiviert — eine Anfrage ist auch dann noch belegrelevant, wenn daraus
|
||||
-- nichts wurde.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_anfragen (
|
||||
id TEXT PRIMARY KEY,
|
||||
-- Kurze, sprechende Nummer für die Kommunikation ("A-2608-0001").
|
||||
nummer TEXT NOT NULL UNIQUE,
|
||||
|
||||
-- Kontakt
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
telefon TEXT,
|
||||
kontaktweg TEXT NOT NULL DEFAULT 'email', -- email | telefon
|
||||
|
||||
-- Projekt
|
||||
paket TEXT NOT NULL, -- onepager | website | shop | verwaltung | betreuung | unklar
|
||||
ziel TEXT, -- Was soll die Seite erreichen
|
||||
zielgruppe TEXT,
|
||||
wirkung TEXT,
|
||||
seiten TEXT, -- gewünschte Seiten/Bereiche
|
||||
funktionen TEXT, -- JSON-Array angekreuzter Funktionen
|
||||
-- Kategoriespezifische Folgefragen als JSON, damit neue Fragen keine
|
||||
-- Schemaänderung brauchen (Masterplan S.10 "Intelligente Felder").
|
||||
details TEXT,
|
||||
|
||||
-- Vorhandene Inhalte
|
||||
hat_texte TEXT, -- ja | teilweise | nein
|
||||
hat_bilder TEXT,
|
||||
hat_logo TEXT,
|
||||
hat_domain TEXT,
|
||||
|
||||
-- Rahmen
|
||||
budget TEXT,
|
||||
termin TEXT,
|
||||
bemerkung TEXT,
|
||||
|
||||
-- Verwaltung
|
||||
status TEXT NOT NULL DEFAULT 'neu', -- neu | pruefung | rueckfrage | angebot | angenommen | abgeschlossen | abgelehnt
|
||||
archiviert INTEGER NOT NULL DEFAULT 0,
|
||||
kunde_id TEXT, -- gesetzt, sobald daraus ein Kunde wurde
|
||||
sprache TEXT NOT NULL DEFAULT 'de', -- in welcher Sprache angefragt wurde -> so wird geantwortet
|
||||
|
||||
-- Nachvollziehbarkeit
|
||||
erstellt_am TEXT NOT NULL,
|
||||
aktualisiert_am TEXT,
|
||||
ip_hash TEXT, -- nur Hash, nie die IP selbst (Datensparsamkeit)
|
||||
|
||||
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_status ON wd_anfragen(status, archiviert);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_erstellt ON wd_anfragen(erstellt_am DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_anfragen_email ON wd_anfragen(email);
|
||||
|
||||
-- Interne Notizen und Antworten an der Anfrage (Masterplan S.10).
|
||||
CREATE TABLE IF NOT EXISTS wd_anfrage_notizen (
|
||||
id TEXT PRIMARY KEY,
|
||||
anfrage_id TEXT NOT NULL,
|
||||
autor TEXT NOT NULL, -- dogfather | vanvan
|
||||
text TEXT NOT NULL,
|
||||
intern INTEGER NOT NULL DEFAULT 1, -- 1 = nur intern sichtbar
|
||||
erstellt_am TEXT NOT NULL,
|
||||
FOREIGN KEY (anfrage_id) REFERENCES wd_anfragen(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_anfrage_notizen ON wd_anfrage_notizen(anfrage_id, erstellt_am);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- KUNDEN (Masterplan S.12)
|
||||
-- Ein Kunde entsteht erst, wenn ein Projekt gekauft oder eine Betreuung
|
||||
-- vereinbart wurde. Registrierung allein reicht ausdrücklich nicht:
|
||||
-- "Projektkauf oder laufende Betreuung wird vor Freischaltung geprüft".
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_kunden (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
firma TEXT,
|
||||
telefon TEXT,
|
||||
sprache TEXT NOT NULL DEFAULT 'de',
|
||||
|
||||
-- Anmeldung
|
||||
passwort_hash TEXT, -- NULL = noch kein Passwort gesetzt
|
||||
email_bestaetigt INTEGER NOT NULL DEFAULT 0,
|
||||
-- Freischaltung ist bewusst getrennt von der E-Mail-Bestätigung:
|
||||
-- bestätigte E-Mail heißt nur "die Adresse gehört dir", nicht
|
||||
-- "du darfst hier hinein".
|
||||
freigeschaltet INTEGER NOT NULL DEFAULT 0,
|
||||
gesperrt INTEGER NOT NULL DEFAULT 0,
|
||||
gesperrt_grund TEXT,
|
||||
|
||||
-- Betreuung
|
||||
betreuung_aktiv INTEGER NOT NULL DEFAULT 0,
|
||||
betreuung_paket TEXT,
|
||||
betreuung_start TEXT,
|
||||
betreuung_ende TEXT,
|
||||
|
||||
benachrichtigungen INTEGER NOT NULL DEFAULT 1,
|
||||
erstellt_am TEXT NOT NULL,
|
||||
aktualisiert_am TEXT,
|
||||
letzter_login TEXT,
|
||||
archiviert INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_kunden_email ON wd_kunden(email);
|
||||
|
||||
-- Einmal-Kennungen für E-Mail-Bestätigung, Einladung und Passwort-Reset.
|
||||
-- Getrennt von der Kundentabelle, damit ein Kunde mehrere offene Vorgänge
|
||||
-- haben kann und abgelaufene Kennungen einfach wegzuräumen sind.
|
||||
CREATE TABLE IF NOT EXISTS wd_kunden_token (
|
||||
token_hash TEXT PRIMARY KEY, -- nur der Hash, nie das Token selbst
|
||||
kunde_id TEXT NOT NULL,
|
||||
zweck TEXT NOT NULL, -- bestaetigung | einladung | passwort
|
||||
laeuft_ab TEXT NOT NULL,
|
||||
benutzt_am TEXT,
|
||||
erstellt_am TEXT NOT NULL,
|
||||
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_kunden_token_kunde ON wd_kunden_token(kunde_id, zweck);
|
||||
|
||||
-- Aktive Anmeldungen. Serverseitig gespeichert, damit sich Sitzungen im
|
||||
-- Verwaltungsbereich gezielt beenden lassen (Masterplan S.13:
|
||||
-- "Aktive Sitzungen abmelden").
|
||||
CREATE TABLE IF NOT EXISTS wd_kunden_sitzungen (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
kunde_id TEXT NOT NULL,
|
||||
erstellt_am TEXT NOT NULL,
|
||||
laeuft_ab TEXT NOT NULL,
|
||||
letzte_aktion TEXT,
|
||||
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_sitzungen_kunde ON wd_kunden_sitzungen(kunde_id);
|
||||
|
||||
-- Fehlversuche bei der Kundenanmeldung (Sperre nach zu vielen Versuchen).
|
||||
CREATE TABLE IF NOT EXISTS wd_login_versuche (
|
||||
schluessel TEXT PRIMARY KEY, -- E-Mail in Kleinschreibung
|
||||
anzahl INTEGER NOT NULL DEFAULT 0,
|
||||
gesperrt_bis TEXT,
|
||||
zuletzt TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- PROJEKTE (Masterplan S.12/S.13)
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_projekte (
|
||||
id TEXT PRIMARY KEY,
|
||||
nummer TEXT NOT NULL UNIQUE, -- "P-2608-0001"
|
||||
kunde_id TEXT NOT NULL,
|
||||
anfrage_id TEXT,
|
||||
|
||||
titel TEXT NOT NULL,
|
||||
paket TEXT NOT NULL,
|
||||
beschreibung TEXT,
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'briefing', -- briefing | angebot | design | entwicklung | tests | abnahme | uebergeben | pausiert | abgebrochen
|
||||
naechster_schritt TEXT,
|
||||
wartet_auf TEXT, -- kunde | dogfather -> wer gerade am Zug ist
|
||||
|
||||
-- Geld. In CENT als Ganzzahl, nie als Kommazahl: 0.1 + 0.2 ergibt in
|
||||
-- Gleitkomma nicht 0.3, und bei Rechnungsbeträgen ist das inakzeptabel.
|
||||
preis_cent INTEGER,
|
||||
anzahlung_cent INTEGER,
|
||||
anzahlung_bezahlt INTEGER NOT NULL DEFAULT 0,
|
||||
rest_bezahlt INTEGER NOT NULL DEFAULT 0,
|
||||
waehrung TEXT NOT NULL DEFAULT 'EUR',
|
||||
|
||||
start_am TEXT,
|
||||
richttermin TEXT,
|
||||
uebergabe_am TEXT,
|
||||
|
||||
-- Portfoliofreigabe ist bewusst ein EIGENES Feld, getrennt von der
|
||||
-- Veröffentlichung der Kundenseite (Masterplan S.13: "Portfoliofreigabe
|
||||
-- separat dokumentieren"). Beides darf nie aneinander hängen.
|
||||
portfolio_freigabe INTEGER NOT NULL DEFAULT 0,
|
||||
portfolio_freigabe_am TEXT,
|
||||
|
||||
archiviert INTEGER NOT NULL DEFAULT 0,
|
||||
erstellt_am TEXT NOT NULL,
|
||||
aktualisiert_am TEXT,
|
||||
|
||||
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (anfrage_id) REFERENCES wd_anfragen(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_projekte_kunde ON wd_projekte(kunde_id, archiviert);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_projekte_status ON wd_projekte(status);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- DATEIEN (Masterplan S.12: Versionen und Zeitstempel)
|
||||
-- Die Datei selbst liegt im Dateisystem neben der Datenbank, hier steht
|
||||
-- nur der Verweis. Der gespeicherte Name ist eine zufällige Kennung —
|
||||
-- ein hochgeladener Originalname darf nie in einen Pfad wandern.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_dateien (
|
||||
id TEXT PRIMARY KEY,
|
||||
projekt_id TEXT,
|
||||
kunde_id TEXT NOT NULL,
|
||||
anfrage_id TEXT,
|
||||
|
||||
anzeigename TEXT NOT NULL, -- was der Mensch sieht
|
||||
speichername TEXT NOT NULL, -- zufällig, im Dateisystem
|
||||
typ TEXT NOT NULL, -- MIME-Typ
|
||||
groesse INTEGER NOT NULL,
|
||||
|
||||
kategorie TEXT NOT NULL DEFAULT 'sonstiges', -- logo | bild | text | angebot | freigabe | sonstiges
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
ersetzt_id TEXT, -- Vorgängerversion
|
||||
hochgeladen_von TEXT NOT NULL, -- kunde | dogfather | vanvan
|
||||
-- Kunden dürfen nur sehen, was für sie bestimmt ist. Interne Arbeitsdateien
|
||||
-- bleiben damit unsichtbar, ohne sie in einer zweiten Tabelle zu führen.
|
||||
fuer_kunde INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
erstellt_am TEXT NOT NULL,
|
||||
geloescht INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_dateien_projekt ON wd_dateien(projekt_id, geloescht);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_dateien_kunde ON wd_dateien(kunde_id, geloescht);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- NACHRICHTEN am Projekt (Masterplan S.12: "projektbezogene Kommunikation
|
||||
-- bleibt vollständig nachvollziehbar")
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_nachrichten (
|
||||
id TEXT PRIMARY KEY,
|
||||
projekt_id TEXT NOT NULL,
|
||||
kunde_id TEXT NOT NULL,
|
||||
autor TEXT NOT NULL, -- kunde | dogfather | vanvan
|
||||
text TEXT NOT NULL,
|
||||
intern INTEGER NOT NULL DEFAULT 0, -- 1 = Kunde sieht es NICHT
|
||||
gelesen_kunde INTEGER NOT NULL DEFAULT 0,
|
||||
gelesen_admin INTEGER NOT NULL DEFAULT 0,
|
||||
erstellt_am TEXT NOT NULL,
|
||||
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_nachrichten_projekt ON wd_nachrichten(projekt_id, erstellt_am);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- ÄNDERUNGSWÜNSCHE / ZUSATZANGEBOTE (Masterplan S.12)
|
||||
-- Der Kunde meldet eine Idee an, Dogfather beziffert Aufwand und Preis,
|
||||
-- der Kunde nimmt an oder lehnt ab. Ein eigener Datensatz statt einer
|
||||
-- Nachricht, damit der Zustand eindeutig ist und nichts im Verlauf untergeht.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_aenderungen (
|
||||
id TEXT PRIMARY KEY,
|
||||
projekt_id TEXT NOT NULL,
|
||||
kunde_id TEXT NOT NULL,
|
||||
|
||||
titel TEXT NOT NULL,
|
||||
beschreibung TEXT NOT NULL,
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'eingereicht', -- eingereicht | geprueft | angeboten | angenommen | abgelehnt | umgesetzt
|
||||
aufwand_text TEXT,
|
||||
zusatzpreis_cent INTEGER,
|
||||
zusatzdauer_text TEXT,
|
||||
antwort TEXT,
|
||||
|
||||
entschieden_am TEXT,
|
||||
erstellt_am TEXT NOT NULL,
|
||||
aktualisiert_am TEXT,
|
||||
|
||||
FOREIGN KEY (projekt_id) REFERENCES wd_projekte(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (kunde_id) REFERENCES wd_kunden(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_aenderungen_projekt ON wd_aenderungen(projekt_id, status);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- INHALTE, DIE IN DER VERWALTUNG PFLEGBAR SIND (Masterplan S.13)
|
||||
-- Ein einfacher Schlüssel-Wert-Speicher mit mehrsprachigem JSON-Wert.
|
||||
-- Bewusst kein eigenes Feld je Text: sonst braucht jeder neue Satz auf der
|
||||
-- Seite eine Datenbankänderung.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_inhalte (
|
||||
schluessel TEXT PRIMARY KEY,
|
||||
wert_json TEXT NOT NULL, -- {"de":"...","de-CH":"...","en":"...","fr":"...","pt":"..."}
|
||||
bereich TEXT, -- zur Gruppierung in der Oberfläche
|
||||
aktualisiert_am TEXT NOT NULL,
|
||||
aktualisiert_von TEXT
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- ÄNDERUNGSVERLAUF (Masterplan S.13: "Änderungsverlauf und verantwortliche
|
||||
-- Person protokollieren")
|
||||
-- Absichtlich eine eigene Tabelle statt Spalten in jeder Tabelle: so ist
|
||||
-- der Verlauf an einer Stelle lesbar und überlebt auch das Archivieren
|
||||
-- des Datensatzes, auf den er sich bezieht.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wd_verlauf (
|
||||
id TEXT PRIMARY KEY,
|
||||
bereich TEXT NOT NULL, -- anfrage | kunde | projekt | inhalt | zugang | datei
|
||||
objekt_id TEXT,
|
||||
aktion TEXT NOT NULL, -- angelegt | geaendert | archiviert | freigegeben | ...
|
||||
wer TEXT NOT NULL, -- dogfather | vanvan | kunde:<id> | system
|
||||
details TEXT, -- JSON: was genau sich geändert hat
|
||||
erstellt_am TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_verlauf_objekt ON wd_verlauf(bereich, objekt_id, erstellt_am DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_verlauf_zeit ON wd_verlauf(erstellt_am DESC);
|
||||
Reference in New Issue
Block a user