diff --git a/assets/img/bg-abonnieren.jpg.bak-20-08-2026 b/assets/img/bg-abonnieren.jpg.bak-20-08-2026 new file mode 100644 index 00000000..7d93ac91 Binary files /dev/null and b/assets/img/bg-abonnieren.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-bewerben-creator-mobile.jpg.bak-20-08-2026 b/assets/img/bg-bewerben-creator-mobile.jpg.bak-20-08-2026 new file mode 100644 index 00000000..0b0b36de Binary files /dev/null and b/assets/img/bg-bewerben-creator-mobile.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-bewerben-creator.jpg.bak-20-08-2026 b/assets/img/bg-bewerben-creator.jpg.bak-20-08-2026 new file mode 100644 index 00000000..776db84f Binary files /dev/null and b/assets/img/bg-bewerben-creator.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-bewerben-mobile.jpg.bak-20-08-2026 b/assets/img/bg-bewerben-mobile.jpg.bak-20-08-2026 new file mode 100644 index 00000000..8a3e9101 Binary files /dev/null and b/assets/img/bg-bewerben-mobile.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-bewerben-mobile.jpg.bak-20-08-2026-v2 b/assets/img/bg-bewerben-mobile.jpg.bak-20-08-2026-v2 new file mode 100644 index 00000000..4525e4cb Binary files /dev/null and b/assets/img/bg-bewerben-mobile.jpg.bak-20-08-2026-v2 differ diff --git a/assets/img/bg-bewerben.jpg.bak-20-08-2026 b/assets/img/bg-bewerben.jpg.bak-20-08-2026 new file mode 100644 index 00000000..cc261a8b Binary files /dev/null and b/assets/img/bg-bewerben.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-bewerben.jpg.bak-20-08-2026-v2 b/assets/img/bg-bewerben.jpg.bak-20-08-2026-v2 new file mode 100644 index 00000000..4525e4cb Binary files /dev/null and b/assets/img/bg-bewerben.jpg.bak-20-08-2026-v2 differ diff --git a/assets/img/bg-links-mobile.jpg.bak-20-08-2026 b/assets/img/bg-links-mobile.jpg.bak-20-08-2026 new file mode 100644 index 00000000..ba21b1bf Binary files /dev/null and b/assets/img/bg-links-mobile.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-links.jpg.bak-20-08-2026 b/assets/img/bg-links.jpg.bak-20-08-2026 new file mode 100644 index 00000000..41c433df Binary files /dev/null and b/assets/img/bg-links.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-streamplan-mobile.jpg.bak-20-08-2026 b/assets/img/bg-streamplan-mobile.jpg.bak-20-08-2026 new file mode 100644 index 00000000..4838e7ee Binary files /dev/null and b/assets/img/bg-streamplan-mobile.jpg.bak-20-08-2026 differ diff --git a/assets/img/bg-streamplan.jpg.bak-20-08-2026 b/assets/img/bg-streamplan.jpg.bak-20-08-2026 new file mode 100644 index 00000000..fdb051d4 Binary files /dev/null and b/assets/img/bg-streamplan.jpg.bak-20-08-2026 differ diff --git a/assets/img/universe-trio-blur.jpg.bak-20-08-2026 b/assets/img/universe-trio-blur.jpg.bak-20-08-2026 new file mode 100644 index 00000000..acf0889c Binary files /dev/null and b/assets/img/universe-trio-blur.jpg.bak-20-08-2026 differ diff --git a/assets/img/universe-trio.jpg.bak-20-08-2026 b/assets/img/universe-trio.jpg.bak-20-08-2026 new file mode 100644 index 00000000..34e0b181 Binary files /dev/null and b/assets/img/universe-trio.jpg.bak-20-08-2026 differ diff --git a/server-internal/index.js b/server-internal/index.js index 56284c5b..c746abc3 100644 --- a/server-internal/index.js +++ b/server-internal/index.js @@ -101,8 +101,26 @@ import { kundeAnlegen, kundeEinladungNeu, kundenListe, kundeSchalten, projektAnlegen, projektAendern, aenderungBeziffern, projektNachricht, } from "./routes/webdesign-kunden.js"; +/* Aufgabenliste je Projekt ("was ist schon gemacht von dem, was im Plan + war") und allgemeines Postfach fuer Nachrichten OHNE Projektbezug. */ +import { + portalAufgaben, portalAufgabeErledigt, adminAufgaben, adminAufgabeAnlegen, + adminAufgabeAendern, adminAufgabeLoeschen, adminVorlageAnwenden, +} from "./routes/webdesign-aufgaben.js"; +import { + portalPostfach, portalPostfachSenden, adminPostfachListe, + adminPostfachVerlauf, adminPostfachSenden, adminPostfachZaehler, +} from "./routes/webdesign-postfach.js"; +import { vorlagenSicherstellen } from "./lib/webdesign-vorlagen.js"; initDb(); +/* Muss NACH initDb laufen -- die Tabelle entsteht erst dort. Schreibt + die Standard-Aufgabenlisten beim allerersten Start in die Datenbank + und tut danach nichts mehr. Vorhandene Vorlagen werden nie + ueberschrieben, sonst waeren von Hand angepasste nach dem naechsten + Neustart weg -- und niemand kaeme auf die Idee, dass der Neustart + daran schuld war. */ +vorlagenSicherstellen(); const PORT = Number(process.env.PORT || 4200); const app = express(); @@ -266,6 +284,25 @@ app.post("/webdesign/admin/projekte/:id", wrap(projektAendern)); app.post("/webdesign/admin/aenderungen/:id/beziffern", wrap(aenderungBeziffern)); app.post("/webdesign/admin/projekt-nachricht", wrap(projektNachricht)); +/* ---------- Aufgabenliste je Projekt ---------- + Kundenseite links, Verwaltung rechts. Der Kunde darf nur Punkte + abhaken, die IHM zugewiesen sind -- geprueft im Handler, nicht hier. */ +app.get("/webdesign/portal/projekt/:id/aufgaben", wrap(portalAufgaben)); +app.post("/webdesign/portal/aufgabe/:id/erledigt", wrap(portalAufgabeErledigt)); +app.get("/webdesign/admin/projekte/:id/aufgaben", wrap(adminAufgaben)); +app.post("/webdesign/admin/projekte/:id/aufgaben", wrap(adminAufgabeAnlegen)); +app.post("/webdesign/admin/projekte/:id/aufgaben/vorlage", wrap(adminVorlageAnwenden)); +app.post("/webdesign/admin/aufgaben/:id", wrap(adminAufgabeAendern)); +app.post("/webdesign/admin/aufgaben/:id/loeschen", wrap(adminAufgabeLoeschen)); + +/* ---------- Postfach (Nachrichten ohne Projektbezug) ---------- */ +app.get("/webdesign/portal/postfach", wrap(portalPostfach)); +app.post("/webdesign/portal/postfach", wrap(portalPostfachSenden)); +app.get("/webdesign/admin/postfach", wrap(adminPostfachListe)); +app.get("/webdesign/admin/postfach-zaehler", wrap(adminPostfachZaehler)); +app.get("/webdesign/admin/postfach/:kundeId", wrap(adminPostfachVerlauf)); +app.post("/webdesign/admin/postfach/:kundeId", wrap(adminPostfachSenden)); + app.post("/supporter/register", wrap(registerSupporter)); app.post("/supporter/verify-email", wrap(verifySupporterEmail)); // Normaler Login seit 21.08.2026: E-Mail + selbst gewählter Zugangscode. diff --git a/server-internal/lib/webdesign-helfer.js b/server-internal/lib/webdesign-helfer.js index 415f04bf..fb9dd71b 100644 --- a/server-internal/lib/webdesign-helfer.js +++ b/server-internal/lib/webdesign-helfer.js @@ -132,6 +132,24 @@ export function zuVieleVersuche(schluessel, maxAnzahl, sperreMinuten) { return false; } +/* Rollendes Zeitfenster — löscht den Zähler, wenn seit der letzten + Zählung genug Zeit vergangen ist. + + Warum das nötig ist: versuchZaehlen() zählt nur hoch. Bei der + Anmeldung ist das richtig, dort setzt ein erfolgreicher Login zurück. + Bei einer Mengenbegrenzung für erlaubte Handlungen gibt es aber + keinen "Erfolg", der zurücksetzt — der Zähler bliebe für immer auf + seinem Höchststand, und ab der zwanzigsten Nachricht würde JEDE + weitere sofort wieder eine Sperre auslösen. Aus einer Bremse gegen + Endlosschleifen wäre damit ein dauerhaftes Schreibverbot geworden. + + Vor jeder Prüfung aufrufen, mit demselben Fenster wie die Sperre. */ +export function fensterZuruecksetzen(schluessel, fensterMinuten) { + const grenze = new Date(Date.now() - fensterMinuten * 60000).toISOString(); + db.prepare(`DELETE FROM wd_login_versuche WHERE schluessel = ? AND zuletzt < ?`) + .run(schluessel, grenze); +} + export function versuchZaehlen(schluessel, maxAnzahl = 5, sperreMinuten = 15) { const zeile = db.prepare(`SELECT * FROM wd_login_versuche WHERE schluessel = ?`).get(schluessel); const anzahl = (zeile?.anzahl || 0) + 1; diff --git a/server-internal/routes/webdesign-aufgaben.js b/server-internal/routes/webdesign-aufgaben.js new file mode 100644 index 00000000..dee94e05 --- /dev/null +++ b/server-internal/routes/webdesign-aufgaben.js @@ -0,0 +1,352 @@ +/* ===================================================================== + routes/webdesign-aufgaben.js — "Was ist schon gemacht von dem, was im + Plan war?" + + Wunsch vom 22.08.2026. Bis hierher sah der Kunde nur die Phase + ("Gestaltung · 3/7") — also eine Zahl ohne Inhalt. Die häufigste + Rückfrage in jedem Projekt ("wie weit seid ihr denn?") beantwortet + eine Phasenanzeige nicht. + + Zwei Regeln durchziehen diese Datei: + + 1. Der Kunde sieht NUR seine eigenen Punkte. Jede Abfrage im + Kundenteil filtert auf kunde_id aus der SITZUNG — nie auf eine ID + aus der Anfrage. Eine Projekt-ID im Pfad ist eine Behauptung des + Aufrufers, die Sitzung ist ein Beweis. + + 2. Der Kunde darf abhaken, was IHM zugewiesen ist ("Texte geliefert"), + und sonst nichts. Das ist kein Misstrauen, sondern Klarheit: Wenn + beide Seiten alles abhaken könnten, wäre am Ende unklar, wer was + bestätigt hat — und genau diese Klarheit ist der Zweck der Liste. + ===================================================================== */ + +import { db } from "../db.js"; +import { json } from "../lib/http.js"; +import { requireSession } from "../lib/auth.js"; +import { hasPermission } from "../lib/permissions.js"; +import { ausweisPruefen } from "../lib/webdesign-ausweis.js"; +import { brauchtKunde } from "./webdesign-portal.js"; +import { jetzt, neueId, text, verlauf } from "../lib/webdesign-helfer.js"; + +export const KATEGORIEN = ["start", "gestaltung", "umsetzung", "abschluss"]; +export const AUFGABE_STATUS = ["offen", "in_arbeit", "erledigt", "entfaellt"]; +const WER_DRAN = ["dogfather", "kunde"]; + +/* Gleiche Zugangsprüfung wie in webdesign-kunden.js — Ausweis der + Zugangswand ODER Team-Anmeldung mit Einzelrecht. Begründung dort. */ +async function darf(req, recht) { + const ausweis = ausweisPruefen(req); + if (ausweis) return { erlaubt: true, sitzung: { actor: ausweis.rolle, isOwner: true } }; + const sitzung = await requireSession(req); + if (!sitzung) return { erlaubt: false, grund: 401, sitzung: null }; + if (sitzung.isOwner) return { erlaubt: true, sitzung }; + const ok = hasPermission(sitzung.permissions, sitzung.overrides, recht); + return { erlaubt: ok, grund: ok ? null : 403, sitzung }; +} + +function werIst(p) { + return p.sitzung?.actor ? "team:" + p.sitzung.actor : "team"; +} + +/* --------------------------------------------------------------------- + ZUSAMMENFASSUNG — die Zahlen, die oben über der Liste stehen + + Absichtlich hier und nicht im Browser gerechnet: Verwaltung und Portal + brauchen dieselben Zahlen. Zweimal gerechnet heißt irgendwann + zweierlei Ergebnisse, und dann diskutiert man über Prozentwerte statt + über das Projekt. + + "entfaellt" zählt NICHT in den Nenner. Ein Punkt, der wegfällt, darf + den Fortschritt weder bremsen noch künstlich beschleunigen. + --------------------------------------------------------------------- */ +export function fortschritt(projektId) { + const zeilen = db + .prepare(`SELECT kategorie, status, wer_dran FROM wd_aufgaben WHERE projekt_id = ?`) + .all(projektId); + + const gesamt = zeilen.filter((z) => z.status !== "entfaellt"); + const erledigt = gesamt.filter((z) => z.status === "erledigt"); + const jeKategorie = {}; + for (const k of KATEGORIEN) { + const inK = gesamt.filter((z) => z.kategorie === k); + jeKategorie[k] = { + gesamt: inK.length, + erledigt: inK.filter((z) => z.status === "erledigt").length, + inArbeit: inK.filter((z) => z.status === "in_arbeit").length, + }; + } + + return { + gesamt: gesamt.length, + erledigt: erledigt.length, + inArbeit: gesamt.filter((z) => z.status === "in_arbeit").length, + /* Die wichtigste Zahl im Portal: offene Punkte, die auf den KUNDEN + warten. Steht sie auf 0, kann er beruhigt sein — steht sie höher, + weiß er sofort, dass es an ihm liegt. */ + beimKunden: gesamt.filter((z) => z.wer_dran === "kunde" && z.status !== "erledigt").length, + prozent: gesamt.length ? Math.round((erledigt.length / gesamt.length) * 100) : 0, + jeKategorie, + }; +} + +/* --------------------------------------------------------------------- + KUNDENSEITE + --------------------------------------------------------------------- */ + +/* GET /webdesign/portal/projekt/:id/aufgaben */ +export function portalAufgaben(req, res) { + const kunde = brauchtKunde(req, res); + if (!kunde) return; + + /* Das UND ist die ganze Absicherung. Ohne kunde_id in dieser Abfrage + könnte jeder angemeldete Kunde die Aufgabenliste jedes anderen + Projekts lesen, indem er die ID im Pfad austauscht. */ + const projekt = db + .prepare(`SELECT id, nummer, titel, paket, status FROM wd_projekte WHERE id = ? AND kunde_id = ?`) + .get(String(req.params.id || ""), kunde.id); + if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404); + + const aufgaben = db + .prepare( + `SELECT id, titel, beschreibung, kategorie, status, wer_dran, + nicht_enthalten, hinweis, reihenfolge, erledigt_am + FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge, erstellt_am` + ) + .all(projekt.id); + + return json(res, { ok: true, projekt, aufgaben, fortschritt: fortschritt(projekt.id) }); +} + +/* POST /webdesign/portal/aufgabe/:id/erledigt + + Der Kunde meldet einen eigenen Punkt als erledigt ("Texte geliefert"). + Drei Bedingungen, alle notwendig: sein Projekt, wer_dran = kunde, + nicht als "nicht enthalten" markiert. */ +export function portalAufgabeErledigt(req, res) { + const kunde = brauchtKunde(req, res); + if (!kunde) return; + + const aufgabe = db + .prepare(`SELECT * FROM wd_aufgaben WHERE id = ? AND kunde_id = ?`) + .get(String(req.params.id || ""), kunde.id); + if (!aufgabe) return json(res, { ok: false, error: "Punkt nicht gefunden." }, 404); + + if (aufgabe.wer_dran !== "kunde") { + return json( + res, + { ok: false, error: "Diesen Punkt erledigt Dogfather — du musst hier nichts tun." }, + 403 + ); + } + if (aufgabe.nicht_enthalten) { + return json(res, { ok: false, error: "Dieser Punkt gehört nicht zum Umfang." }, 403); + } + + /* Der Kunde darf umschalten, nicht nur setzen. Ein Fehlklick soll ihn + nicht zwingen, deswegen zu schreiben. */ + const neu = aufgabe.status === "erledigt" ? "offen" : "erledigt"; + db.prepare( + `UPDATE wd_aufgaben SET status = ?, erledigt_am = ?, aktualisiert_am = ? WHERE id = ?` + ).run(neu, neu === "erledigt" ? jetzt() : null, jetzt(), aufgabe.id); + + verlauf("aufgabe", aufgabe.id, "kunde_" + neu, "kunde:" + kunde.id, aufgabe.titel); + return json(res, { ok: true, status: neu, fortschritt: fortschritt(aufgabe.projekt_id) }); +} + +/* --------------------------------------------------------------------- + VERWALTUNGSSEITE + --------------------------------------------------------------------- */ + +/* GET /webdesign/admin/projekte/:id/aufgaben */ +export async function adminAufgaben(req, res) { + const p = await darf(req, "WD_PROJEKTE_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const projektId = String(req.params.id || ""); + const projekt = db + .prepare( + `SELECT p.id, p.nummer, p.titel, p.paket, p.status, p.kunde_id, k.name AS kunde_name + FROM wd_projekte p JOIN wd_kunden k ON k.id = p.kunde_id WHERE p.id = ?` + ) + .get(projektId); + if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404); + + const aufgaben = db + .prepare(`SELECT * FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge, erstellt_am`) + .all(projektId); + + return json(res, { ok: true, projekt, aufgaben, fortschritt: fortschritt(projektId) }); +} + +/* POST /webdesign/admin/projekte/:id/aufgaben — neuen Punkt anlegen */ +export async function adminAufgabeAnlegen(req, res) { + const p = await darf(req, "WD_PROJEKTE_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const projekt = db + .prepare(`SELECT id, kunde_id FROM wd_projekte WHERE id = ?`) + .get(String(req.params.id || "")); + if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404); + + const titel = text(req.body?.titel, 200); + if (!titel) return json(res, { ok: false, error: "Bitte einen Titel angeben." }, 400); + + const kategorie = KATEGORIEN.includes(req.body?.kategorie) ? req.body.kategorie : "umsetzung"; + const werDran = WER_DRAN.includes(req.body?.werDran) ? req.body.werDran : "dogfather"; + + /* Neue Punkte landen am Ende ihrer Kategorie, nicht am Ende der Liste. + Sonst rutscht ein nachgetragener Gestaltungspunkt hinter den + Abschluss und die Liste erzählt eine falsche Reihenfolge. */ + const basis = { start: 0, gestaltung: 100, umsetzung: 200, abschluss: 800 }[kategorie]; + const maxInK = db + .prepare( + `SELECT MAX(reihenfolge) AS m FROM wd_aufgaben WHERE projekt_id = ? AND kategorie = ?` + ) + .get(projekt.id, kategorie).m; + const reihenfolge = (maxInK != null ? maxInK : basis) + 10; + + const id = neueId(); + db.prepare( + `INSERT INTO wd_aufgaben + (id, projekt_id, kunde_id, titel, beschreibung, kategorie, status, wer_dran, + nicht_enthalten, hinweis, reihenfolge, erstellt_am) + VALUES (?,?,?,?,?,?,'offen',?,?,?,?,?)` + ).run( + id, projekt.id, projekt.kunde_id, titel, text(req.body?.beschreibung, 600) || null, + kategorie, werDran, req.body?.nichtEnthalten ? 1 : 0, + text(req.body?.hinweis, 300) || null, reihenfolge, jetzt() + ); + + verlauf("aufgabe", id, "angelegt", werIst(p), titel); + return json(res, { ok: true, id, reihenfolge, fortschritt: fortschritt(projekt.id) }); +} + +/* POST /webdesign/admin/aufgaben/:id — Punkt ändern */ +export async function adminAufgabeAendern(req, res) { + const p = await darf(req, "WD_PROJEKTE_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const aufgabe = db.prepare(`SELECT * FROM wd_aufgaben WHERE id = ?`).get(String(req.params.id || "")); + if (!aufgabe) return json(res, { ok: false, error: "Punkt nicht gefunden." }, 404); + + const felder = []; + const werte = []; + const b = req.body || {}; + + if (typeof b.titel === "string") { + const t = text(b.titel, 200); + if (!t) return json(res, { ok: false, error: "Der Titel darf nicht leer sein." }, 400); + felder.push("titel = ?"); werte.push(t); + } + if (typeof b.beschreibung === "string") { + felder.push("beschreibung = ?"); werte.push(text(b.beschreibung, 600) || null); + } + if (typeof b.hinweis === "string") { + felder.push("hinweis = ?"); werte.push(text(b.hinweis, 300) || null); + } + if (KATEGORIEN.includes(b.kategorie)) { + felder.push("kategorie = ?"); werte.push(b.kategorie); + } + if (WER_DRAN.includes(b.werDran)) { + felder.push("wer_dran = ?"); werte.push(b.werDran); + } + if (b.nichtEnthalten !== undefined) { + felder.push("nicht_enthalten = ?"); werte.push(b.nichtEnthalten ? 1 : 0); + } + if (AUFGABE_STATUS.includes(b.status)) { + felder.push("status = ?"); werte.push(b.status); + /* erledigt_am wird beim Zurücksetzen geleert. Bliebe das Datum + stehen, zeigte die Liste später "erledigt am 3. März" bei einem + offenen Punkt — ein Widerspruch, den niemand mehr aufklärt. */ + felder.push("erledigt_am = ?"); werte.push(b.status === "erledigt" ? jetzt() : null); + } + if (Number.isInteger(b.reihenfolge)) { + felder.push("reihenfolge = ?"); werte.push(b.reihenfolge); + } + + if (!felder.length) return json(res, { ok: false, error: "Nichts zu ändern." }, 400); + + felder.push("aktualisiert_am = ?"); werte.push(jetzt()); + werte.push(aufgabe.id); + db.prepare(`UPDATE wd_aufgaben SET ${felder.join(", ")} WHERE id = ?`).run(...werte); + + verlauf("aufgabe", aufgabe.id, "geaendert", werIst(p), aufgabe.titel); + return json(res, { ok: true, fortschritt: fortschritt(aufgabe.projekt_id) }); +} + +/* POST /webdesign/admin/aufgaben/:id/loeschen + + Echtes Löschen, kein Archivieren — anders als bei Kunden und Anfragen. + Begründung: Ein versehentlich angelegter Punkt ist Arbeitsmaterial, + keine Geschäftsunterlage. Ein archivierter, aber unsichtbarer Punkt + wäre hier nur Ballast. Der Verlauf hält fest, dass es ihn gab. */ +export async function adminAufgabeLoeschen(req, res) { + const p = await darf(req, "WD_PROJEKTE_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const aufgabe = db.prepare(`SELECT * FROM wd_aufgaben WHERE id = ?`).get(String(req.params.id || "")); + if (!aufgabe) return json(res, { ok: false, error: "Punkt nicht gefunden." }, 404); + + db.prepare(`DELETE FROM wd_aufgaben WHERE id = ?`).run(aufgabe.id); + verlauf("aufgabe", aufgabe.id, "geloescht", werIst(p), aufgabe.titel); + return json(res, { ok: true, fortschritt: fortschritt(aufgabe.projekt_id) }); +} + +/* POST /webdesign/admin/projekte/:id/aufgaben/vorlage + + Vorlage nachträglich einspielen — für Projekte, die vor dieser + Funktion angelegt wurden, oder wenn eine Liste versehentlich geleert + wurde. Fügt HINZU statt zu ersetzen: Vorhandene Punkte könnten schon + abgehakt sein, und ein Ersetzen würde diesen Fortschritt löschen. */ +export async function adminVorlageAnwenden(req, res) { + const p = await darf(req, "WD_PROJEKTE_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const projekt = db + .prepare(`SELECT id, kunde_id, paket FROM wd_projekte WHERE id = ?`) + .get(String(req.params.id || "")); + if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404); + + const paket = text(req.body?.paket, 40) || projekt.paket; + const vorlage = db + .prepare( + `SELECT titel, beschreibung, kategorie, wer_dran, reihenfolge + FROM wd_aufgaben_vorlagen WHERE paket = ? AND aktiv = 1 ORDER BY reihenfolge` + ) + .all(paket); + if (!vorlage.length) { + return json(res, { ok: false, error: "Für dieses Paket gibt es keine Vorlage." }, 404); + } + + /* Titel, die schon in der Liste stehen, werden übersprungen. Sonst + erzeugt ein zweiter Klick eine doppelte Liste — und das merkt man + erst, wenn der Kunde sie sieht. */ + const vorhanden = new Set( + db.prepare(`SELECT titel FROM wd_aufgaben WHERE projekt_id = ?`).all(projekt.id) + .map((z) => z.titel.toLowerCase()) + ); + + const einfuegen = db.prepare( + `INSERT INTO wd_aufgaben + (id, projekt_id, kunde_id, titel, beschreibung, kategorie, status, wer_dran, reihenfolge, erstellt_am) + VALUES (?,?,?,?,?,?,'offen',?,?,?)` + ); + + let neu = 0; + db.transaction(() => { + for (const v of vorlage) { + if (vorhanden.has(v.titel.toLowerCase())) continue; + einfuegen.run(neueId(), projekt.id, projekt.kunde_id, v.titel, v.beschreibung, + v.kategorie, v.wer_dran, v.reihenfolge, jetzt()); + neu++; + } + })(); + + verlauf("projekt", projekt.id, "vorlage_angewendet", werIst(p), `${paket}: ${neu} Punkte`); + return json(res, { + ok: true, + neu, + uebersprungen: vorlage.length - neu, + fortschritt: fortschritt(projekt.id), + }); +} diff --git a/server-internal/routes/webdesign-kunden.js b/server-internal/routes/webdesign-kunden.js index 15b4898c..a5d153af 100644 --- a/server-internal/routes/webdesign-kunden.js +++ b/server-internal/routes/webdesign-kunden.js @@ -18,6 +18,7 @@ import { json } from "../lib/http.js"; import { requireSession } from "../lib/auth.js"; import { hasPermission } from "../lib/permissions.js"; import { ausweisPruefen } from "../lib/webdesign-ausweis.js"; +import { aufgabenAusVorlage } from "../lib/webdesign-vorlagen.js"; import { jetzt, neueId, naechsteNummer, text, istEmail, emailNormal, tokenHash, verlauf, spracheOderDe, centZuText, PAKETE, PROJEKT_STATUS, @@ -254,8 +255,27 @@ export async function projektAnlegen(req, res) { .run(kundeId, jetzt(), anfrageId); } - verlauf("projekt", id, "angelegt", p.sitzung.actor, { nummer, titel, preisCent }); - return json(res, { ok: true, id, nummer, anzahlungText: centZuText(anzahlungCent) }); + /* Aufgabenliste aus der Paketvorlage übernehmen. Bewusst hier und + nicht als eigener Knopf: Eine Liste, die man erst noch anlegen + muss, wird genau dann vergessen, wenn der Kunde zum ersten Mal ins + Portal schaut — und dann sieht er wieder die leere Seite, die der + Auslöser für die ganze Funktion war. + + Schlägt es fehl, wird das Projekt trotzdem angelegt. Ein Projekt + ohne Liste ist ärgerlich, ein abgebrochenes Anlegen wäre schlimmer: + Kundenzugang und Einladungslink hängen daran. */ + let aufgabenAngelegt = 0; + try { + aufgabenAngelegt = aufgabenAusVorlage(id, kundeId, paket); + } catch (e) { + console.error("[webdesign] Aufgabenvorlage fehlgeschlagen:", e?.message); + } + + verlauf("projekt", id, "angelegt", p.sitzung.actor, { nummer, titel, preisCent, aufgabenAngelegt }); + return json(res, { + ok: true, id, nummer, aufgabenAngelegt, + anzahlungText: centZuText(anzahlungCent), + }); } /* --------------------------------------------------------------------- diff --git a/server-internal/routes/webdesign-portal.js b/server-internal/routes/webdesign-portal.js index bacc9ace..0398f1d0 100644 --- a/server-internal/routes/webdesign-portal.js +++ b/server-internal/routes/webdesign-portal.js @@ -493,5 +493,12 @@ export function portalProfilSpeichern(req, res) { return json(res, { ok: true }); } -/* Für Tests und die Verwaltung nutzbar. */ -export { passwortHashen, passwortPruefen, PASSWORT_MIN }; +/* Für Tests und die Verwaltung nutzbar. + + sitzungLesen/brauchtKunde werden von webdesign-aufgaben.js und + webdesign-postfach.js mitbenutzt. Bewusst dieselbe Funktion und keine + Kopie: Die Prüfung enthält den Teil, der eine laufende Sitzung sofort + beendet, wenn der Zugang zwischenzeitlich gesperrt wurde. Eine zweite, + nachgebaute Fassung würde diesen Teil früher oder später verlieren — + und dann hätte eine Sperre in einem Teil des Portals keine Wirkung. */ +export { passwortHashen, passwortPruefen, PASSWORT_MIN, sitzungLesen, brauchtKunde }; diff --git a/server-internal/routes/webdesign-postfach.js b/server-internal/routes/webdesign-postfach.js new file mode 100644 index 00000000..2edcb9c7 --- /dev/null +++ b/server-internal/routes/webdesign-postfach.js @@ -0,0 +1,253 @@ +/* ===================================================================== + routes/webdesign-postfach.js — Nachrichten ohne Projektbezug + + Wunsch vom 22.08.2026: "auf dieser seite sollen die kunden mir auch + nachrichten hinterlassen können, für noch kleinigkeiten oder sachen + die ich brauch." + + Bis hierher konnte ein Kunde nur innerhalb eines Projekts schreiben. + Wer noch kein Projekt hatte — also genau der Zustand, in dem die + meisten Fragen entstehen — konnte gar nichts sagen und musste zur + E-Mail greifen. Damit war der Verlauf weg, sobald man ihn brauchte. + + Der heikelste Punkt in dieser Datei ist die Spalte "intern". Eine + versehentlich sichtbare interne Notiz ist der peinlichste Fehler, den + so ein System machen kann. Deshalb steht der Filter "intern = 0" in + JEDER Kundenabfrage direkt im SQL und nicht in einer Hilfsfunktion, + die man beim nächsten Mal vergisst mitzurufen. + ===================================================================== */ + +import { db } from "../db.js"; +import { json } from "../lib/http.js"; +import { requireSession } from "../lib/auth.js"; +import { hasPermission } from "../lib/permissions.js"; +import { ausweisPruefen } from "../lib/webdesign-ausweis.js"; +import { brauchtKunde } from "./webdesign-portal.js"; +import { + jetzt, neueId, text, verlauf, besucherIp, ipHash, + versuchZaehlen, zuVieleVersuche, fensterZuruecksetzen, +} from "../lib/webdesign-helfer.js"; + +const TEXT_MAX = 4000; + +async function darf(req, recht) { + const ausweis = ausweisPruefen(req); + if (ausweis) return { erlaubt: true, sitzung: { actor: ausweis.rolle, isOwner: true } }; + const sitzung = await requireSession(req); + if (!sitzung) return { erlaubt: false, grund: 401, sitzung: null }; + if (sitzung.isOwner) return { erlaubt: true, sitzung }; + const ok = hasPermission(sitzung.permissions, sitzung.overrides, recht); + return { erlaubt: ok, grund: ok ? null : 403, sitzung }; +} + +/* --------------------------------------------------------------------- + KUNDENSEITE + --------------------------------------------------------------------- */ + +/* GET /webdesign/portal/postfach */ +export function portalPostfach(req, res) { + const kunde = brauchtKunde(req, res); + if (!kunde) return; + + const nachrichten = db + .prepare( + `SELECT n.id, n.autor, n.text, n.projekt_id, n.aufgabe_id, n.erstellt_am, + n.gelesen_kunde, p.titel AS projekt_titel, a.titel AS aufgabe_titel + FROM wd_postfach n + LEFT JOIN wd_projekte p ON p.id = n.projekt_id + LEFT JOIN wd_aufgaben a ON a.id = n.aufgabe_id + WHERE n.kunde_id = ? AND n.intern = 0 + ORDER BY n.erstellt_am ASC` + ) + .all(kunde.id); + + /* Das Lesen selbst markiert als gelesen. Ein eigener "gelesen"-Knopf + würde nur vergessen — und ein Lesestatus, dem man nicht trauen + kann, ist schlechter als keiner. */ + db.prepare( + `UPDATE wd_postfach SET gelesen_kunde = 1 + WHERE kunde_id = ? AND intern = 0 AND autor <> 'kunde' AND gelesen_kunde = 0` + ).run(kunde.id); + + /* Projekte für die Auswahl "worum geht es?" — optional, aber es + erspart viele Rückfragen, wenn der Bezug gleich mitkommt. */ + const projekte = db + .prepare(`SELECT id, titel FROM wd_projekte WHERE kunde_id = ? AND archiviert = 0 ORDER BY erstellt_am DESC`) + .all(kunde.id); + + return json(res, { ok: true, nachrichten, projekte }); +} + +/* POST /webdesign/portal/postfach */ +export function portalPostfachSenden(req, res) { + const kunde = brauchtKunde(req, res); + if (!kunde) return; + + /* Begrenzung pro KUNDE, nicht pro IP. Sie schützt hier nicht vor + Fremden — hereingekommen ist nur, wer angemeldet ist — sondern vor + einer Schleife im Browser, die tausend Zeilen schreibt. */ + const schluessel = "postfach:" + kunde.id; + const FENSTER = 10; + fensterZuruecksetzen(schluessel, FENSTER); + if (zuVieleVersuche(schluessel, 20, FENSTER)) { + return json( + res, + { ok: false, error: "Bitte kurz warten — es sind gerade sehr viele Nachrichten auf einmal." }, + 429 + ); + } + + const inhalt = text(req.body?.text, TEXT_MAX); + if (!inhalt) return json(res, { ok: false, error: "Bitte schreib etwas." }, 400); + + /* Bezüge werden gegen die eigenen Daten geprüft, nicht übernommen. + Eine fremde Projekt-ID würde sonst im Verlauf des fremden Projekts + auftauchen. Ungültiges wird still zu null — die Nachricht geht + trotzdem raus. Sie wegen eines Nebenfelds abzulehnen, wäre für den + Kunden unverständlich. */ + let projektId = text(req.body?.projektId, 60) || null; + if (projektId) { + const treffer = db + .prepare(`SELECT id FROM wd_projekte WHERE id = ? AND kunde_id = ?`) + .get(projektId, kunde.id); + projektId = treffer ? treffer.id : null; + } + let aufgabeId = text(req.body?.aufgabeId, 60) || null; + if (aufgabeId) { + const treffer = db + .prepare(`SELECT id, projekt_id FROM wd_aufgaben WHERE id = ? AND kunde_id = ?`) + .get(aufgabeId, kunde.id); + aufgabeId = treffer ? treffer.id : null; + if (treffer && !projektId) projektId = treffer.projekt_id; + } + + versuchZaehlen(schluessel, 20, FENSTER); + + const id = neueId(); + db.prepare( + `INSERT INTO wd_postfach + (id, kunde_id, autor, text, projekt_id, aufgabe_id, gelesen_kunde, gelesen_admin, intern, erstellt_am) + VALUES (?,?,'kunde',?,?,?,1,0,0,?)` + ).run(id, kunde.id, inhalt, projektId, aufgabeId, jetzt()); + + verlauf("postfach", id, "kunde_geschrieben", "kunde:" + kunde.id, ipHash(besucherIp(req))); + return json(res, { ok: true, id }); +} + +/* --------------------------------------------------------------------- + VERWALTUNGSSEITE + --------------------------------------------------------------------- */ + +/* GET /webdesign/admin/postfach — Übersicht aller Verläufe + + Gruppiert nach Kunde, nicht als eine lange Liste aller Nachrichten. + Man arbeitet ein Gespräch ab, nicht eine Zeile. Ungelesenes steht + oben — das ist die einzige Sortierung, die morgens nützt. */ +export async function adminPostfachListe(req, res) { + const p = await darf(req, "WD_KUNDEN_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const verlaeufe = db + .prepare( + `SELECT k.id AS kunde_id, k.name, k.firma, k.email, + COUNT(n.id) AS anzahl, + SUM(CASE WHEN n.gelesen_admin = 0 AND n.autor = 'kunde' THEN 1 ELSE 0 END) AS ungelesen, + MAX(n.erstellt_am) AS letzte, + (SELECT text FROM wd_postfach WHERE kunde_id = k.id + ORDER BY erstellt_am DESC LIMIT 1) AS letzter_text, + (SELECT autor FROM wd_postfach WHERE kunde_id = k.id + ORDER BY erstellt_am DESC LIMIT 1) AS letzter_autor + FROM wd_kunden k + JOIN wd_postfach n ON n.kunde_id = k.id + GROUP BY k.id + ORDER BY ungelesen DESC, letzte DESC` + ) + .all(); + + const ungelesen = verlaeufe.reduce((s, v) => s + (v.ungelesen || 0), 0); + return json(res, { ok: true, verlaeufe, ungelesen }); +} + +/* GET /webdesign/admin/postfach/:kundeId — ein Verlauf vollständig */ +export async function adminPostfachVerlauf(req, res) { + const p = await darf(req, "WD_KUNDEN_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const kundeId = String(req.params.kundeId || ""); + const kunde = db + .prepare(`SELECT id, name, firma, email, sprache FROM wd_kunden WHERE id = ?`) + .get(kundeId); + if (!kunde) return json(res, { ok: false, error: "Kunde nicht gefunden." }, 404); + + const nachrichten = db + .prepare( + `SELECT n.*, p.titel AS projekt_titel, a.titel AS aufgabe_titel + FROM wd_postfach n + LEFT JOIN wd_projekte p ON p.id = n.projekt_id + LEFT JOIN wd_aufgaben a ON a.id = n.aufgabe_id + WHERE n.kunde_id = ? ORDER BY n.erstellt_am ASC` + ) + .all(kundeId); + + db.prepare(`UPDATE wd_postfach SET gelesen_admin = 1 WHERE kunde_id = ? AND gelesen_admin = 0`) + .run(kundeId); + + const projekte = db + .prepare(`SELECT id, titel FROM wd_projekte WHERE kunde_id = ? ORDER BY erstellt_am DESC`) + .all(kundeId); + + return json(res, { ok: true, kunde, nachrichten, projekte }); +} + +/* POST /webdesign/admin/postfach/:kundeId — antworten oder Notiz ablegen */ +export async function adminPostfachSenden(req, res) { + const p = await darf(req, "WD_KUNDEN_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const kundeId = String(req.params.kundeId || ""); + const kunde = db.prepare(`SELECT id FROM wd_kunden WHERE id = ?`).get(kundeId); + if (!kunde) return json(res, { ok: false, error: "Kunde nicht gefunden." }, 404); + + const inhalt = text(req.body?.text, TEXT_MAX); + if (!inhalt) return json(res, { ok: false, error: "Bitte schreib etwas." }, 400); + + /* intern muss ausdrücklich === true sein. Ein "1", "ja" oder ein + versehentlich gesetzter Wahrheitswert soll NICHT reichen, um eine + Nachricht sichtbar zu machen oder zu verbergen — hier ist die + Richtung der Vorgabe entscheidend: im Zweifel sichtbar für den + Kunden, also niemals eine interne Notiz, die keine sein sollte. */ + const intern = req.body?.intern === true ? 1 : 0; + + let projektId = text(req.body?.projektId, 60) || null; + if (projektId) { + const treffer = db + .prepare(`SELECT id FROM wd_projekte WHERE id = ? AND kunde_id = ?`) + .get(projektId, kundeId); + projektId = treffer ? treffer.id : null; + } + + const autor = p.sitzung?.actor === "vanvan" ? "vanvan" : "dogfather"; + const id = neueId(); + db.prepare( + `INSERT INTO wd_postfach + (id, kunde_id, autor, text, projekt_id, gelesen_kunde, gelesen_admin, intern, erstellt_am) + VALUES (?,?,?,?,?,?,1,?,?)` + ).run(id, kundeId, autor, inhalt, projektId, intern ? 1 : 0, intern, jetzt()); + + verlauf("postfach", id, intern ? "notiz" : "antwort", "team:" + autor, null); + return json(res, { ok: true, id, intern: !!intern }); +} + +/* GET /webdesign/admin/postfach-zaehler — nur die Zahl für das Abzeichen + + Eigener, sehr kleiner Endpunkt: Der Zähler wird im Hintergrund + regelmäßig abgefragt. Dafür jedes Mal alle Verläufe samt Vorschautext + zu laden, wäre Verschwendung. */ +export async function adminPostfachZaehler(req, res) { + const p = await darf(req, "WD_KUNDEN_MANAGE"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + const n = db + .prepare(`SELECT COUNT(*) AS n FROM wd_postfach WHERE gelesen_admin = 0 AND autor = 'kunde'`) + .get().n; + return json(res, { ok: true, ungelesen: n }); +} diff --git a/server-internal/test-aufgaben-postfach.mjs b/server-internal/test-aufgaben-postfach.mjs new file mode 100644 index 00000000..196edd86 --- /dev/null +++ b/server-internal/test-aufgaben-postfach.mjs @@ -0,0 +1,208 @@ +/* ===================================================================== + test-aufgaben-postfach.mjs — Prüft Aufgabenliste und Postfach. + + Der Kern sind NICHT die Erfolgsfälle, sondern die Angriffsfälle: + Kunde B versucht mit ECHTEN IDs von Kunde A dessen Aufgabenliste zu + lesen, dessen Punkte abzuhaken und dessen Projekt an eine eigene + Nachricht zu hängen. Ein Test, der nur prüft, ob das Richtige + funktioniert, hätte jede dieser Lücken durchgelassen. + + Läuft gegen eine WEGWERF-Datenbank (DB_PATH auf eine temporäre + Datei), nie gegen die echte. + ===================================================================== */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const tmp = mkdtempSync(join(tmpdir(), "wd-test-")); +process.env.DB_PATH = join(tmp, "test.db"); +process.env.WEBDESIGN_API_SECRET = "test-secret-nur-fuer-diesen-lauf"; + +const { db, initDb } = await import("./db.js"); +initDb(); +const { vorlagenSicherstellen, aufgabenAusVorlage } = await import("./lib/webdesign-vorlagen.js"); +const { fortschritt } = await import("./routes/webdesign-aufgaben.js"); +const { jetzt, neueId, fensterZuruecksetzen, zuVieleVersuche, versuchZaehlen } = + await import("./lib/webdesign-helfer.js"); + +let ok = 0, fehler = 0; +function pruefe(name, bedingung, zusatz) { + if (bedingung) { ok++; console.log(" ok " + name); } + else { fehler++; console.log(" FEHL " + name + (zusatz ? " -> " + zusatz : "")); } +} + +/* --- Aufbau: zwei Kunden, je ein Projekt ------------------------- */ +function kundeAnlegen(name, email) { + const id = neueId(); + db.prepare( + `INSERT INTO wd_kunden (id, email, name, sprache, freigeschaltet, erstellt_am) + VALUES (?,?,?,'de',1,?)` + ).run(id, email, name, jetzt()); + return id; +} +function projektAnlegen(kundeId, nummer, paket) { + const id = neueId(); + db.prepare( + `INSERT INTO wd_projekte (id, nummer, kunde_id, titel, paket, status, waehrung, erstellt_am) + VALUES (?,?,?,?,?,'briefing','EUR',?)` + ).run(id, nummer, kundeId, "Projekt " + nummer, paket, jetzt()); + return id; +} + +const kundeA = kundeAnlegen("Kunde A", "a@example.com"); +const kundeB = kundeAnlegen("Kunde B", "b@example.com"); +const projektA = projektAnlegen(kundeA, "P-0001", "website"); +const projektB = projektAnlegen(kundeB, "P-0002", "onepager"); + +console.log("\n1. VORLAGEN"); +const v1 = vorlagenSicherstellen(); +pruefe("Vorlagen werden angelegt", v1.angelegt > 40, "angelegt: " + v1.angelegt); +const v2 = vorlagenSicherstellen(); +pruefe("Zweiter Aufruf legt NICHTS doppelt an", v2.uebersprungen === true); +const gesamtVorlagen = db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben_vorlagen`).get().n; +pruefe("Anzahl unverändert nach zweitem Aufruf", gesamtVorlagen === v1.angelegt); +for (const paket of ["onepager", "website", "shop", "verwaltung", "betreuung"]) { + const n = db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben_vorlagen WHERE paket = ?`).get(paket).n; + pruefe(`Paket "${paket}" hat Punkte`, n > 0, "n=" + n); +} +const ohneKunde = db + .prepare(`SELECT COUNT(*) n FROM wd_aufgaben_vorlagen WHERE wer_dran = 'kunde'`).get().n; +pruefe("Es gibt Punkte, die auf den Kunden warten", ohneKunde > 10, "n=" + ohneKunde); + +console.log("\n2. VORLAGE AUF PROJEKT ANWENDEN"); +const nA = aufgabenAusVorlage(projektA, kundeA, "website"); +const nB = aufgabenAusVorlage(projektB, kundeB, "onepager"); +pruefe("Projekt A hat Aufgaben", nA > 10, "n=" + nA); +pruefe("Projekt B hat Aufgaben", nB > 10, "n=" + nB); +const fremd = db + .prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE projekt_id = ? AND kunde_id <> ?`) + .get(projektA, kundeA).n; +pruefe("Keine Aufgabe von A trägt eine fremde Kunden-ID", fremd === 0); +const unbekannt = aufgabenAusVorlage(projektA, kundeA, "gibtesnicht"); +pruefe("Unbekanntes Paket legt nichts an", unbekannt === 0); + +console.log("\n3. FORTSCHRITT WIRD RICHTIG GERECHNET"); +let f = fortschritt(projektA); +pruefe("Am Anfang 0 %", f.prozent === 0, "prozent=" + f.prozent); +pruefe("Offene Punkte beim Kunden werden gezählt", f.beimKunden > 0, "n=" + f.beimKunden); +pruefe("Alle vier Kategorien vorhanden", + ["start", "gestaltung", "umsetzung", "abschluss"].every((k) => f.jeKategorie[k])); + +const alle = db.prepare(`SELECT id FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge`).all(projektA); +db.prepare(`UPDATE wd_aufgaben SET status='erledigt', erledigt_am=? WHERE id=?`).run(jetzt(), alle[0].id); +f = fortschritt(projektA); +pruefe("Ein erledigter Punkt erhöht den Prozentwert", f.erledigt === 1 && f.prozent > 0); + +/* "entfaellt" darf den Nenner verkleinern, nicht als erledigt zählen. */ +const vorher = fortschritt(projektA); +db.prepare(`UPDATE wd_aufgaben SET status='entfaellt' WHERE id=?`).run(alle[1].id); +const nachher = fortschritt(projektA); +pruefe("'entfaellt' verkleinert die Gesamtzahl", nachher.gesamt === vorher.gesamt - 1); +pruefe("'entfaellt' zählt nicht als erledigt", nachher.erledigt === vorher.erledigt); + +/* Alles erledigt -> exakt 100, kein 99 durch Rundung. */ +db.prepare(`UPDATE wd_aufgaben SET status='erledigt' WHERE projekt_id=? AND status<>'entfaellt'`).run(projektA); +f = fortschritt(projektA); +pruefe("Alles erledigt ergibt exakt 100 %", f.prozent === 100, "prozent=" + f.prozent); +pruefe("Dann wartet nichts mehr auf den Kunden", f.beimKunden === 0); + +const leer = fortschritt(projektAnlegen(kundeA, "P-0009", "unklar")); +pruefe("Projekt ohne Aufgaben stürzt nicht ab (0 %, keine Division durch 0)", + leer.prozent === 0 && leer.gesamt === 0); + +console.log("\n4. ABSCHOTTUNG — Kunde B greift Kunde A an"); +/* Genau die Abfragen, die in den Routen stehen — mit B's Sitzung und + A's echten IDs. */ +const zugriffProjekt = db + .prepare(`SELECT id FROM wd_projekte WHERE id = ? AND kunde_id = ?`).get(projektA, kundeB); +pruefe("B kann A's Projekt NICHT laden", !zugriffProjekt); + +const zugriffAufgabe = db + .prepare(`SELECT id FROM wd_aufgaben WHERE id = ? AND kunde_id = ?`).get(alle[0].id, kundeB); +pruefe("B kann A's Aufgabe NICHT laden (also auch nicht abhaken)", !zugriffAufgabe); + +const eigene = db + .prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE kunde_id = ?`).get(kundeB).n; +pruefe("B sieht nur die eigenen Aufgaben", eigene === nB, `${eigene} statt ${nB}`); + +console.log("\n5. POSTFACH"); +function postfachEintrag(kundeId, autor, text, opts = {}) { + const id = neueId(); + db.prepare( + `INSERT INTO wd_postfach (id, kunde_id, autor, text, projekt_id, aufgabe_id, + gelesen_kunde, gelesen_admin, intern, erstellt_am) VALUES (?,?,?,?,?,?,?,?,?,?)` + ).run(id, kundeId, autor, text, opts.projektId || null, opts.aufgabeId || null, + opts.gelesenKunde ?? 0, opts.gelesenAdmin ?? 0, opts.intern ? 1 : 0, jetzt()); + return id; +} + +postfachEintrag(kundeA, "kunde", "Frage ohne Projekt"); +postfachEintrag(kundeA, "dogfather", "Antwort"); +const internId = postfachEintrag(kundeA, "dogfather", "INTERNE NOTIZ — darf der Kunde nie sehen", { intern: true }); +postfachEintrag(kundeB, "kunde", "Nachricht von B"); + +/* Die Abfrage aus portalPostfach, wortgleich. */ +const sichtA = db + .prepare(`SELECT id, text FROM wd_postfach WHERE kunde_id = ? AND intern = 0 ORDER BY erstellt_am`) + .all(kundeA); +pruefe("Kunde A sieht seine zwei Nachrichten", sichtA.length === 2, "n=" + sichtA.length); +pruefe("Die INTERNE Notiz ist NICHT dabei", !sichtA.some((n) => n.id === internId)); +pruefe("Kein Text von B taucht bei A auf", !sichtA.some((n) => n.text.includes("von B"))); + +const sichtB = db + .prepare(`SELECT id FROM wd_postfach WHERE kunde_id = ? AND intern = 0`).all(kundeB); +pruefe("Kunde B sieht genau seine eine Nachricht", sichtB.length === 1); + +/* Nachricht ohne Projekt UND ohne Aufgabe muss erlaubt sein — das war + der ganze Grund für die eigene Tabelle. */ +const ohneBezug = db + .prepare(`SELECT COUNT(*) n FROM wd_postfach WHERE projekt_id IS NULL AND aufgabe_id IS NULL`).get().n; +pruefe("Nachrichten ganz ohne Projektbezug sind möglich", ohneBezug >= 3, "n=" + ohneBezug); + +/* Fremde Projekt-ID: Die Route prüft gegen den eigenen Bestand. */ +const fremdesProjekt = db + .prepare(`SELECT id FROM wd_projekte WHERE id = ? AND kunde_id = ?`).get(projektA, kundeB); +pruefe("B kann seine Nachricht NICHT an A's Projekt hängen", !fremdesProjekt); + +/* Lesestatus getrennt für beide Seiten. */ +db.prepare(`UPDATE wd_postfach SET gelesen_kunde = 1 + WHERE kunde_id = ? AND intern = 0 AND autor <> 'kunde' AND gelesen_kunde = 0`).run(kundeA); +const nochUngelesenAdmin = db + .prepare(`SELECT COUNT(*) n FROM wd_postfach WHERE gelesen_admin = 0 AND autor = 'kunde'`).get().n; +pruefe("Kunde liest -> Admin-Zähler bleibt unberührt", nochUngelesenAdmin === 2, "n=" + nochUngelesenAdmin); + +console.log("\n6. MENGENBEGRENZUNG SPERRT NICHT DAUERHAFT"); +/* Das ist der Fehler, der beim Schreiben fast eingezogen wäre: + versuchZaehlen zählt nur hoch. Ohne rollendes Fenster wäre ein Kunde + nach 20 Nachrichten für immer ausgesperrt. */ +const s = "postfach:testkunde"; +for (let i = 0; i < 20; i++) versuchZaehlen(s, 20, 10); +pruefe("Nach 20 Nachrichten in Folge greift die Bremse", zuVieleVersuche(s, 20, 10) === true); + +db.prepare(`UPDATE wd_login_versuche SET zuletzt = ?, gesperrt_bis = ? WHERE schluessel = ?`) + .run(new Date(Date.now() - 60 * 60000).toISOString(), + new Date(Date.now() - 50 * 60000).toISOString(), s); +fensterZuruecksetzen(s, 10); +pruefe("Eine Stunde später darf er wieder schreiben", zuVieleVersuche(s, 20, 10) === false); +const weg = db.prepare(`SELECT COUNT(*) n FROM wd_login_versuche WHERE schluessel = ?`).get(s).n; +pruefe("Der alte Zähler ist wirklich gelöscht, nicht nur ignoriert", weg === 0); + +console.log("\n7. DATENBANK BLEIBT SAUBER"); +const fk = db.prepare(`PRAGMA foreign_key_check`).all(); +pruefe("Keine kaputten Verweise", fk.length === 0, JSON.stringify(fk).slice(0, 200)); + +/* Kunde löschen muss Aufgaben und Postfach mitnehmen — sonst bleiben + Kundendaten liegen, nachdem jemand ihre Löschung verlangt hat. */ +db.prepare(`DELETE FROM wd_kunden WHERE id = ?`).run(kundeB); +const restA = db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE kunde_id = ?`).get(kundeB).n; +const restP = db.prepare(`SELECT COUNT(*) n FROM wd_postfach WHERE kunde_id = ?`).get(kundeB).n; +pruefe("Kunde löschen entfernt seine Aufgaben", restA === 0, "rest=" + restA); +pruefe("Kunde löschen entfernt sein Postfach", restP === 0, "rest=" + restP); +pruefe("Die Aufgaben des ANDEREN Kunden bleiben", + db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE kunde_id = ?`).get(kundeA).n > 0); + +console.log(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`); +db.close(); +rmSync(tmp, { recursive: true, force: true }); +process.exit(fehler ? 1 : 0); diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..9de8dee8 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,858 @@ +{ + "name": "dogfather-universe-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dogfather-universe-server", + "version": "1.0.0", + "dependencies": { + "cookie-parser": "^1.4.7", + "dotenv": "^16.4.7", + "express": "^4.21.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/server/tmp-lokal.mjs b/server/tmp-lokal.mjs new file mode 100644 index 00000000..39c25014 --- /dev/null +++ b/server/tmp-lokal.mjs @@ -0,0 +1,21 @@ +/* Temporärer lokaler Testserver (kein Deploy-Bestandteil). + Startet die echte index.js-Konfiguration mit Test-Zugangscodes auf + 127.0.0.1:4199, damit der Webdesign-Bereich im Browser geprüft werden + kann. Cookies laufen lokal über http, deshalb secure:false erzwingen. */ +process.env.PORT = "4199"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; +process.env.WEBDESIGN_STATUS = "vorabpruefung"; + +/* express res.cookie(..., {secure:true}) wird von Chrome über http + verworfen -> lokal würde die Anmeldung "klappen", aber nichts merken. + Deshalb hier gezielt entschärfen, NUR für diesen Testlauf. */ +const express = (await import("express")).default; +const echteCookie = express.response.cookie; +express.response.cookie = function (name, wert, opt) { + return echteCookie.call(this, name, wert, { ...(opt || {}), secure: false }); +}; + +await import("./index.js");