Verwaltung: nur noch EINMAL den Code eingeben
Rueckmeldung 22.08.2026: "ich hab auf der verwaltungs seite 2 mal dass ich
den zugangscode eingeben muss und vanvan auch, ich will es nur einmal
eingeben muessen." Berechtigt.
Die Zugangswand hat serverseitig laengst geprueft, WER da ist -- Dogfather
oder VanVan, laut Masterplan S.4 beide gleichberechtigte
Volladministratoren. Ein zweites Passwort danach bringt keinen
zusaetzlichen Schutz, es kostet nur jedes Mal Zeit.
Warum es nicht einfach "Cookie mitschicken" ist: Die Verwaltungsdaten
liegen hinter postfach.dogfather-universe.com, einem ANDEREN Rechnernamen.
Das Sitzungs-Cookie gilt dort nicht -- so sind Cookies gebaut, und das ist
gut so.
Loesung: Die Zugangswand stellt unter /webdesign/api-ausweis einen
kurzlebigen, signierten Ausweis aus, den die interne API anerkennt.
- 15 Minuten gueltig, die Seite holt bei Bedarf still einen neuen
- traegt bereich="wd-admin", damit ein Sitzungs-Token der Zugangswand
hier NICHT durchgeht und umgekehrt
- nur mit gueltiger Zugangssitzung zu bekommen
- gilt AUSSCHLIESSLICH fuer die /webdesign-Endpunkte. Postfach,
Bewerbungen, Supporter und Teamverwaltung bleiben unberuehrt
Fehlt WEBDESIGN_API_SECRET auf einer der beiden Seiten, gilt kein Ausweis
und die Verwaltung fragt wie bisher nach dem Team-Code. Ein fehlender
Konfigurationswert darf niemals eine Tuer oeffnen -- nur eine schliessen.
Genau das prueft der letzte Testfall.
Bei einem 401 wird EINMAL still ein neuer Ausweis geholt und die Anfrage
wiederholt, statt jemanden mitten im Arbeiten rauszuwerfen.
10/10 Tests, darunter: gefaelschte Signatur, fremdes Geheimnis,
abgelaufen, falscher Bereich, erfundene Rolle.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -85,3 +85,11 @@ PAYPAL_WD_PLAN_PREMIUM=
|
||||
# Zufallszeichenkette. Wird sie geaendert, passen alte Hashes nicht mehr --
|
||||
# das ist unkritisch, sie dienen nur der Missbrauchserkennung.
|
||||
WD_IP_SALT=
|
||||
|
||||
# Gegenstueck zu WEBDESIGN_API_SECRET in server/.env (22.08.2026).
|
||||
# MUSS exakt derselbe Wert sein. Damit erkennt die interne API den Ausweis
|
||||
# der Zugangswand an und Dogfather/VanVan muessen sich in der
|
||||
# Webdesign-Verwaltung nicht ein zweites Mal anmelden.
|
||||
# Der Ausweis gilt AUSSCHLIESSLICH fuer die /webdesign-Endpunkte -- Postfach,
|
||||
# Bewerbungen, Supporter und Teamverwaltung bleiben unberuehrt.
|
||||
WEBDESIGN_API_SECRET=
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* =====================================================================
|
||||
webdesign-ausweis.js — Ausweis der Zugangswand anerkennen
|
||||
|
||||
Hintergrund (Rückmeldung 22.08.2026: "ich will es nur einmal eingeben
|
||||
müssen"): Die Zugangswand vor /webdesign hat serverseitig bereits
|
||||
geprüft, wer da ist. Dogfather und VanVan sind laut Masterplan S.4
|
||||
gleichberechtigte Volladministratoren. Ein zweites Passwort für die
|
||||
Verwaltung bringt danach keinen zusätzlichen Schutz — es kostet nur
|
||||
jedes Mal Zeit.
|
||||
|
||||
Der Ausweis wird von server/webdesign-gate.js ausgestellt und hier
|
||||
geprüft. Beide Seiten teilen sich dafür WEBDESIGN_API_SECRET.
|
||||
|
||||
⚠️ WAS DIESER AUSWEIS NICHT IST
|
||||
Er ist KEIN Team-Login. Er berechtigt ausschließlich zu den
|
||||
Webdesign-Endpunkten und verschafft keinerlei Zugriff auf Postfach,
|
||||
Bewerbungen, Supporter oder Teamverwaltung. Wer den Webdesign-Bereich
|
||||
betreten darf, darf damit nicht automatisch alles andere auch.
|
||||
|
||||
⚠️ FEHLT DAS GEHEIMNIS, GILT KEIN AUSWEIS
|
||||
Dann bleibt es bei der Team-Anmeldung wie bisher. Ein fehlender
|
||||
Konfigurationswert darf niemals eine Tür öffnen — nur eine schließen.
|
||||
===================================================================== */
|
||||
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
|
||||
function b64urlZurueck(str) {
|
||||
return Buffer.from(String(str).replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft den Ausweis aus dem Authorization-Kopf.
|
||||
* Liefert { rolle } bei Gültigkeit, sonst null.
|
||||
*/
|
||||
export function ausweisPruefen(req) {
|
||||
const geheim = process.env.WEBDESIGN_API_SECRET;
|
||||
if (!geheim) return null; // nicht eingerichtet -> kein Ausweis
|
||||
|
||||
const kopf = req.headers["authorization"] || "";
|
||||
const token = kopf.startsWith("Bearer ") ? kopf.slice(7) : null;
|
||||
if (!token || !token.includes(".")) return null;
|
||||
|
||||
const [nutzlast, sigB64] = token.split(".");
|
||||
try {
|
||||
const erwartet = createHmac("sha256", geheim).update(nutzlast).digest();
|
||||
const geliefert = b64urlZurueck(sigB64);
|
||||
/* Länge zuerst vergleichen: timingSafeEqual wirft bei
|
||||
unterschiedlich langen Puffern, statt false zu liefern. */
|
||||
if (geliefert.length !== erwartet.length) return null;
|
||||
if (!timingSafeEqual(geliefert, erwartet)) return null;
|
||||
|
||||
const daten = JSON.parse(b64urlZurueck(nutzlast).toString("utf8"));
|
||||
/* bereich prüfen: Ohne diesen Vergleich würde ein Sitzungs-Token der
|
||||
öffentlichen Zugangswand hier ebenfalls durchgehen, obwohl es für
|
||||
einen ganz anderen Zweck ausgestellt wurde. */
|
||||
if (daten.bereich !== "wd-admin") return null;
|
||||
if (!daten.exp || daten.exp < Date.now()) return null;
|
||||
if (daten.rolle !== "dogfather" && daten.rolle !== "vanvan") return null;
|
||||
|
||||
return { rolle: daten.rolle };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einheitliche Rechteprüfung für die Webdesign-Endpunkte.
|
||||
*
|
||||
* Zwei Wege führen hinein:
|
||||
* 1. Der Ausweis der Zugangswand (Dogfather oder VanVan) — beide sind
|
||||
* Volladministratoren und dürfen im Webdesign-Bereich alles.
|
||||
* 2. Eine normale Team-Anmeldung mit dem passenden Einzelrecht — für
|
||||
* den Fall, dass später jemand aus dem Team nur Anfragen ansehen,
|
||||
* aber keine Kundendaten bearbeiten soll.
|
||||
*
|
||||
* `pruefeTeam` wird als Funktion hereingereicht, damit diese Datei nichts
|
||||
* von auth.js und permissions.js wissen muss.
|
||||
*/
|
||||
export async function darfWebdesign(req, recht, pruefeTeam) {
|
||||
const ausweis = ausweisPruefen(req);
|
||||
if (ausweis) return { erlaubt: true, actor: ausweis.rolle, ueberAusweis: true };
|
||||
return pruefeTeam();
|
||||
}
|
||||
@@ -16,6 +16,7 @@ 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 {
|
||||
jetzt, neueId, naechsteNummer, text, istEmail, emailNormal,
|
||||
ipHash, besucherIp, spracheOderDe, verlauf,
|
||||
@@ -27,6 +28,23 @@ import {
|
||||
restlichen internen Bereich.
|
||||
--------------------------------------------------------------------- */
|
||||
async function darf(req, recht) {
|
||||
/* Zwei Wege fuehren hinein.
|
||||
|
||||
1. Der Ausweis der Zugangswand. Wer dort mit dem persoenlichen Code
|
||||
durchgekommen ist, ist Dogfather oder VanVan -- laut Masterplan S.4
|
||||
beide gleichberechtigte Volladministratoren. Ein zweites Passwort
|
||||
danach bringt keinen zusaetzlichen Schutz und kostet nur Zeit
|
||||
(Rueckmeldung 22.08.2026: "ich will es nur einmal eingeben muessen").
|
||||
|
||||
2. Eine normale Team-Anmeldung mit dem passenden Einzelrecht -- fuer
|
||||
den Fall, dass spaeter jemand aus dem Team nur Anfragen ansehen,
|
||||
aber keine Kundendaten bearbeiten koennen soll.
|
||||
|
||||
Der Ausweis gilt AUSSCHLIESSLICH hier. Postfach, Bewerbungen,
|
||||
Supporter und Teamverwaltung bleiben davon unberuehrt. */
|
||||
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 };
|
||||
|
||||
@@ -17,12 +17,30 @@ 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 {
|
||||
jetzt, neueId, naechsteNummer, text, istEmail, emailNormal,
|
||||
tokenHash, verlauf, spracheOderDe, centZuText, PAKETE, PROJEKT_STATUS,
|
||||
} from "../lib/webdesign-helfer.js";
|
||||
|
||||
async function darf(req, recht) {
|
||||
/* Zwei Wege fuehren hinein.
|
||||
|
||||
1. Der Ausweis der Zugangswand. Wer dort mit dem persoenlichen Code
|
||||
durchgekommen ist, ist Dogfather oder VanVan -- laut Masterplan S.4
|
||||
beide gleichberechtigte Volladministratoren. Ein zweites Passwort
|
||||
danach bringt keinen zusaetzlichen Schutz und kostet nur Zeit
|
||||
(Rueckmeldung 22.08.2026: "ich will es nur einmal eingeben muessen").
|
||||
|
||||
2. Eine normale Team-Anmeldung mit dem passenden Einzelrecht -- fuer
|
||||
den Fall, dass spaeter jemand aus dem Team nur Anfragen ansehen,
|
||||
aber keine Kundendaten bearbeiten koennen soll.
|
||||
|
||||
Der Ausweis gilt AUSSCHLIESSLICH hier. Postfach, Bewerbungen,
|
||||
Supporter und Teamverwaltung bleiben davon unberuehrt. */
|
||||
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 };
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* =====================================================================
|
||||
test-webdesign-ausweis.mjs — Ausweis der Zugangswand
|
||||
|
||||
Der Ausweis erlaubt Dogfather und VanVan, die Webdesign-Verwaltung ohne
|
||||
zweite Anmeldung zu benutzen (Rückmeldung 22.08.2026: "ich will es nur
|
||||
einmal eingeben müssen"). Genau deshalb muss hier besonders sauber
|
||||
geprüft werden, was NICHT durchgeht.
|
||||
|
||||
Aufruf: node test-webdesign-ausweis.mjs
|
||||
===================================================================== */
|
||||
import { createHmac } from "node:crypto";
|
||||
process.env.WEBDESIGN_API_SECRET = "test-geheimnis";
|
||||
const { ausweisPruefen } = await import("./lib/webdesign-ausweis.js");
|
||||
|
||||
const b64 = (b) => Buffer.from(b).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
function bau(inhalt, geheim = "test-geheimnis") {
|
||||
const n = b64(Buffer.from(JSON.stringify(inhalt), "utf8"));
|
||||
return n + "." + b64(createHmac("sha256", geheim).update(n).digest());
|
||||
}
|
||||
const req = (t) => ({ headers: t ? { authorization: "Bearer " + t } : {} });
|
||||
|
||||
let ok = 0, bad = 0;
|
||||
const p = (n, b, z = "") => { if (b) { ok++; console.log(" ok " + n); } else { bad++; console.log(" FEHLT " + n + " " + z); } };
|
||||
|
||||
console.log("\n=== Ausweis der Zugangswand ===\n");
|
||||
const gut = bau({ bereich: "wd-admin", rolle: "dogfather", exp: Date.now() + 900000 });
|
||||
p("gueltiger Ausweis wird anerkannt", ausweisPruefen(req(gut))?.rolle === "dogfather");
|
||||
p("VanVan ebenso", ausweisPruefen(req(bau({ bereich: "wd-admin", rolle: "vanvan", exp: Date.now() + 900000 })))?.rolle === "vanvan");
|
||||
p("ohne Ausweis -> null", ausweisPruefen(req(null)) === null);
|
||||
p("Unsinn -> null", ausweisPruefen(req("kein.ausweis")) === null);
|
||||
p("mit fremdem Geheimnis signiert -> null",
|
||||
ausweisPruefen(req(bau({ bereich: "wd-admin", rolle: "dogfather", exp: Date.now() + 900000 }, "anderes"))) === null);
|
||||
p("abgelaufen -> null",
|
||||
ausweisPruefen(req(bau({ bereich: "wd-admin", rolle: "dogfather", exp: Date.now() - 1000 }))) === null);
|
||||
p("Sitzungs-Token der Zugangswand gilt hier NICHT",
|
||||
ausweisPruefen(req(bau({ bereich: "webdesign", rolle: "dogfather", exp: Date.now() + 900000 }))) === null,
|
||||
"-> anderer Zweck, anderer Bereich");
|
||||
p("erfundene Rolle -> null",
|
||||
ausweisPruefen(req(bau({ bereich: "wd-admin", rolle: "hacker", exp: Date.now() + 900000 }))) === null);
|
||||
p("verfaelschte Signatur -> null", ausweisPruefen(req(gut.split(".")[0] + ".AAAA")) === null);
|
||||
|
||||
delete process.env.WEBDESIGN_API_SECRET;
|
||||
const { ausweisPruefen: ohne } = await import("./lib/webdesign-ausweis.js?neu=1");
|
||||
p("ohne eingerichtetes Geheimnis gilt KEIN Ausweis", ohne(req(gut)) === null,
|
||||
"-> ein fehlender Wert schliesst, er oeffnet nicht");
|
||||
|
||||
console.log(`\n=== ${ok} bestanden, ${bad} gescheitert ===\n`);
|
||||
process.exit(bad === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user