BEFUND ZUERST, DENN ER WAR ANDERS ALS ERWARTET
Es gab sehr wohl eine Benachrichtigung -- ueber einen Discord-Webhook,
sauber gebaut, bewusst ohne Kundendaten im Text. Sie begann aber mit:
const url = process.env.DISCORD_WEBHOOK_WEBDESIGN;
if (!url) return;
Und diese Variable ist in .env.example nirgends aufgefuehrt. Eine
Variable, die niemand kennt, wird nicht gesetzt; dann kehrte die
Funktion wortlos zurueck. Kein Fehler, keine Protokollzeile. Es gab
eine Benachrichtigung, die nur im Quelltext existierte -- und niemand
konnte das bemerken, weil "funktioniert" und "kaputt" identisch
aussehen, solange nichts passiert.
WAS JETZT DA IST
Push aufs Handy, ohne Fremdpaket. Der interne Dienst liegt unter
/home/dogiintern mit Rechten 700 -- dort ist kein npm erreichbar, jede
Abhaengigkeit haette dauerhafte Handarbeit bedeutet. Node bringt P-256,
HKDF und AES-128-GCM selbst mit.
Die Schluessel erzeugt der Server beim ersten Start selbst und legt den
privaten Teil verschluesselt in der Datenbank ab (derselbe Weg wie die
PayPal-Zugangsdaten). Damit gibt es keinen Einrichtungsschritt, der
vergessen werden kann.
GEPRUEFT
Gegen RFC 8291 statt gegen ein Bauchgefuehl: alle fuenf Zwischenwerte
aus Anhang A stimmen (ECDH-Geheimnis, PRK_key, IKM, CEK, NONCE), und
die fertige Nachricht ist Byte fuer Byte die aus Abschnitt 5 der Norm.
Bei Kryptographie erzeugt ein Ableitungsfehler keinen Absturz, sondern
Bytes, die genauso zufaellig aussehen wie richtige.
Dazu ein Kettentest mit einem echten Empfaenger, der entschluesselt:
Kopfzeilen, Inhalt, keine Kundendaten in der Meldung, 410 loescht das
Geraet, 500 loescht es NICHT (sonst kostet eine einzelne Stoerung die
Anmeldung). 50 Pruefungen, alle gruen.
DREI ENTSCHEIDUNGEN
1. In der Meldung stehen nur Nummer und Paket. Sie erscheint auf einem
Sperrbildschirm, den auch jemand sieht, der zufaellig danebensteht.
2. Ist der Schluessel unlesbar, wird KEIN neuer erzeugt. Das waere der
bequeme Weg und der schlimmste: Ein neuer oeffentlicher Schluessel
macht schlagartig jede Anmeldung wertlos, ohne dass jemand erfaehrt,
warum nichts mehr ankommt.
3. Die Verwaltung zeigt den Zustand an und hat einen Testknopf. Genau
das fehlte dem Discord-Weg. Bei verweigerter Erlaubnis erscheint
kein Knopf, der nichts bewirkt, sondern der Weg ueber die
Browsereinstellungen.
Das stille "if (!url) return;" ist ersetzt: Jeder Weg wird einzeln
protokolliert -- auch und gerade, wenn er uebersprungen wird.
Co-Authored-By: Claude Opus 5 <[email protected]>
93 lines
4.2 KiB
JavaScript
93 lines
4.2 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",
|
|
// Aufgabenliste je Projekt ("was ist schon gemacht von dem, was im Plan
|
|
// war") und ein allgemeines Postfach fuer Nachrichten OHNE Projektbezug
|
|
// (22.08.2026). Begruendung fuer die getrennte Postfach-Tabelle steht
|
|
// im Kopf der Migration.
|
|
"0013_webdesign_aufgaben.sql",
|
|
// Elektronische Widerrufsfunktion (§ 356a BGB, seit 19.06.2026) und der
|
|
// Nachweis der Zustimmung zum vorzeitigen Leistungsbeginn (§ 356 Abs. 4
|
|
// BGB). Ausfuehrliche Begruendung im Kopf der Migration -- kurz: Im
|
|
// Portal werden Zusatzangebote per Klick verbindlich angenommen, das ist
|
|
// ein Vertragsschluss ueber eine Online-Benutzeroberflaeche.
|
|
"0014_webdesign_widerruf.sql",
|
|
// Annehmen und Ablehnen einer Anfrage: echter Liefertermin (termin_am)
|
|
// fuer den Countdown, Zeitpunkt der Annahme, Ablehnungsgrund.
|
|
"0015_webdesign_annahme.sql",
|
|
// Die Uhr laeuft erst bei Anzahlung; Abbruch mit Erstattung;
|
|
// Benachrichtigungen fuer die Verwaltung.
|
|
"0016_webdesign_automatik.sql",
|
|
// Angebote mit Zusage im Portal.
|
|
"0017_webdesign_angebote.sql",
|
|
// Altfaelle: Projekte, die abgebrochen wurden, BEVOR es das
|
|
// Archiv-Kennzeichen gab, stehen sonst weiter in der laufenden Liste.
|
|
"0018_webdesign_altabbrueche_archivieren.sql",
|
|
// Push-Benachrichtigung aufs Handy: ein Eintrag je angemeldetem
|
|
// Geraet, damit Handy und Rechner nebeneinander bestehen koennen.
|
|
"0019_webdesign_push.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}`);
|
|
}
|
|
}
|