diff --git a/server-internal/index.js b/server-internal/index.js index b7bdb9e5..999a3b99 100644 --- a/server-internal/index.js +++ b/server-internal/index.js @@ -124,7 +124,13 @@ import { import { zahlungAnsehen, zahlungStarten, zahlungAbschliessen, paypalWebhook, zahlungAnlegen, zahlungenListe, + einstellungenLesen, einstellungenSchreiben, einstellungenPruefen, } from "./routes/webdesign-zahlungen.js"; +/* PayPal-Zugangsdaten koennen verschluesselt in der Datenbank liegen, + damit Filipe sie in seiner Verwaltung eintragen kann statt per SSH. + Sie muessen beim Start EINMAL entschluesselt werden, weil die + Pruefungen des PayPal-Moduls synchron sind. */ +import { geheimnisseLaden } from "./lib/webdesign-geheimnisse.js"; import { vorlagenSicherstellen } from "./lib/webdesign-vorlagen.js"; initDb(); @@ -135,6 +141,14 @@ initDb(); Neustart weg -- und niemand kaeme auf die Idee, dass der Neustart daran schuld war. */ vorlagenSicherstellen(); +/* Ebenfalls nach initDb. Faellt das aus (z.B. weil der ENCRYPTION_KEY + fehlt), laeuft der Dienst trotzdem weiter -- er verhaelt sich dann so, + als waere in der Verwaltung nichts hinterlegt, und die .env gilt + weiter. Ein fehlender Zahlungsanbieter darf nicht die ganze Seite + anhalten. */ +geheimnisseLaden() + .then((n) => { if (n) console.log(`[webdesign] ${n} Einstellungen aus der Datenbank geladen.`); }) + .catch((e) => console.error("[webdesign] Einstellungen nicht ladbar:", e?.message)); const PORT = Number(process.env.PORT || 4200); const app = express(); @@ -332,6 +346,9 @@ app.post("/webdesign/portal/zahlung/:id/start", wrap(zahlungStarten)); app.post("/webdesign/portal/zahlung/:id/abschluss", wrap(zahlungAbschliessen)); app.get("/webdesign/admin/zahlungen", wrap(zahlungenListe)); app.post("/webdesign/admin/zahlungen", wrap(zahlungAnlegen)); +app.get("/webdesign/admin/einstellungen", wrap(einstellungenLesen)); +app.post("/webdesign/admin/einstellungen", wrap(einstellungenSchreiben)); +app.post("/webdesign/admin/einstellungen/pruefen", wrap(einstellungenPruefen)); app.get("/webdesign/admin/postfach", wrap(adminPostfachListe)); app.get("/webdesign/admin/postfach-zaehler", wrap(adminPostfachZaehler)); app.get("/webdesign/admin/postfach/:kundeId", wrap(adminPostfachVerlauf)); diff --git a/server-internal/lib/webdesign-geheimnisse.js b/server-internal/lib/webdesign-geheimnisse.js new file mode 100644 index 00000000..583dc1ba --- /dev/null +++ b/server-internal/lib/webdesign-geheimnisse.js @@ -0,0 +1,164 @@ +/* ===================================================================== + webdesign-geheimnisse.js — PayPal-Zugangsdaten aus der Datenbank + + WARUM ES DAS GIBT + + Die Zugangsdaten standen bisher nur in der .env auf dem Server. Das + ist ein guter Ort — aber er verlangt SSH, einen Texteditor und einen + Dienstneustart. Filipe soll dafür nicht in die Konsole müssen. + + Jetzt kann er sie in seiner Verwaltung eintragen. Sie werden + verschlüsselt gespeichert und beim nächsten Zugriff gelesen. Das + heisst auch: kein Neustart nötig, die Änderung wirkt sofort. + + DREI ENTSCHEIDUNGEN + + 1. VERSCHLÜSSELT, nicht im Klartext. Verwendet wird derselbe + AES-GCM-Weg wie für die Zugangscodes des Universe + (lib/crypto.js). Wer die Datenbankdatei in die Hände bekommt -- + etwa über eine alte Sicherungskopie -- hat damit noch nichts. + + 2. DIE .env BLEIBT GÜLTIG und hat VORRANG. Wer die Werte lieber + klassisch dort pflegt, kann das weiter tun; das Supporter-Abo + benutzt dieselben zwei Werte und läuft womöglich längst darüber. + Die Datenbank ist die Ergänzung für alles, was dort noch fehlt -- + nicht ein zweiter, konkurrierender Ort. + + Vorrang für die .env und nicht umgekehrt, weil sonst ein Eintrag + im Browser stillschweigend eine funktionierende Servereinstellung + aushebeln könnte. Ein Fehlgriff in einem Formular darf keinen + laufenden Zahlungsverkehr umleiten. + + 3. WERTE KOMMEN NIE ZURÜCK. Es gibt keinen Weg, ein gespeichertes + Geheimnis wieder auszulesen -- weder über die Schnittstelle noch + in einer Protokollzeile. Angezeigt wird nur, OB etwas hinterlegt + ist, wie lang es ist und wann es zuletzt geändert wurde. Wer den + Wert verliert, erzeugt bei PayPal einen neuen; das ist sicherer, + als ihn dauerhaft abrufbar zu halten. + ===================================================================== */ + +import { db } from "../db.js"; +import { encryptCode, decryptCode } from "./crypto.js"; + +/* Nur diese Schlüssel dürfen über die Verwaltung gesetzt werden. Eine + feste Liste statt "alles, was reinkommt": Sonst könnte über dasselbe + Formular jede beliebige Einstellung überschrieben werden. */ +export const ERLAUBTE_SCHLUESSEL = { + PAYPAL_ENV: { geheim: false, beschreibung: "Betriebsart (live oder sandbox)" }, + PAYPAL_CLIENT_ID: { geheim: false, beschreibung: "Client ID aus dem PayPal-Entwicklerbereich" }, + PAYPAL_CLIENT_SECRET: { geheim: true, beschreibung: "Secret aus dem PayPal-Entwicklerbereich" }, + PAYPAL_WD_WEBHOOK_ID: { geheim: false, beschreibung: "Webhook-Kennung (beginnt mit WH-)" }, + PAYPAL_WD_PLAN_BASIS: { geheim: false, beschreibung: "Plan für die monatliche Betreuung" }, +}; + +const PRAEFIX = "wd_geheim_"; + +/* Ein SYNCHRONER Zwischenspeicher -- und das ist der ganze Kniff. + + Entschluesseln ist von Natur aus asynchron. Die Pruefungen des + PayPal-Moduls (istLive, istEingerichtet, fehlendeEinstellungen) sind + dagegen synchron und werden an rund einem Dutzend Stellen aufgerufen, + auch mitten in Antwortaufbauten. + + Sie alle auf async umzustellen waere ein Eingriff quer durch den + Bezahlvorgang -- viel Flaeche fuer Fehler an genau der Stelle, an der + Fehler Geld kosten. Stattdessen werden die Werte EINMAL beim Start + entschluesselt und danach synchron gelesen. Nach jeder Aenderung wird + neu geladen, die Wirkung ist also sofort da. + + Der Speicher haelt Klartext im Arbeitsspeicher. Das ist unvermeidlich: + Die Werte muessen zum Anmelden bei PayPal ohnehin im Klartext + vorliegen -- so, wie sie es auch als Umgebungsvariable taeten. */ +let speicher = {}; +let geladen = false; + +/* Laedt und entschluesselt alles. Beim Start und nach jeder Aenderung. */ +export async function geheimnisseLaden() { + const raus = {}; + for (const schluessel of Object.keys(ERLAUBTE_SCHLUESSEL)) { + const zeile = db + .prepare(`SELECT value FROM app_settings WHERE key = ?`) + .get(PRAEFIX + schluessel); + if (!zeile || !zeile.value) continue; + try { + raus[schluessel] = await decryptCode(zeile.value); + } catch (e) { + /* Fast immer: der ENCRYPTION_KEY wurde gewechselt. Dann ist der + Wert verloren -- aber das darf den Dienst nicht anhalten. Er + verhaelt sich, als waere nichts hinterlegt, und die Verwaltung + zeigt es an. Der Wert selbst erscheint NICHT in der Meldung. */ + console.error(`[webdesign] ${schluessel} nicht entschluesselbar - bitte neu eintragen.`); + } + } + speicher = raus; + geladen = true; + return Object.keys(raus).length; +} + +export function istGeladen() { return geladen; } + +/* Der eine Zugriffspunkt fuer den Rest des Codes. SYNCHRON. + + Reihenfolge: .env zuerst, Datenbank danach. Begruendung im Kopf. */ +export function einstellung(schluessel) { + const ausEnv = process.env[schluessel]; + if (ausEnv && String(ausEnv).trim()) return String(ausEnv).trim(); + return speicher[schluessel] || ""; +} + +/* Setzt einen Wert. Leerer Wert = Eintrag entfernen. */ +export async function einstellungSetzen(schluessel, wert) { + if (!(schluessel in ERLAUBTE_SCHLUESSEL)) { + throw new Error("Unbekannte Einstellung."); + } + const sauber = String(wert == null ? "" : wert).trim(); + + if (!sauber) { + db.prepare(`DELETE FROM app_settings WHERE key = ?`).run(PRAEFIX + schluessel); + await geheimnisseLaden(); + return { entfernt: true }; + } + + if (schluessel === "PAYPAL_ENV" && !["live", "sandbox"].includes(sauber)) { + throw new Error("Betriebsart muss 'live' oder 'sandbox' sein."); + } + + const verschluesselt = await encryptCode(sauber); + db.prepare( + `INSERT INTO app_settings (key, value, updated_at) VALUES (?,?,?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at` + ).run(PRAEFIX + schluessel, verschluesselt, new Date().toISOString()); + + await geheimnisseLaden(); + return { gesetzt: true, laenge: sauber.length }; +} + +/* Der Stand für die Anzeige. Gibt NIE einen Wert zurück -- nur, ob + etwas da ist, woher es kommt, wie lang es ist und wann es zuletzt + geändert wurde. */ +export function stand() { + const raus = {}; + + for (const [schluessel, meta] of Object.entries(ERLAUBTE_SCHLUESSEL)) { + const ausEnv = process.env[schluessel] && String(process.env[schluessel]).trim(); + const ausDb = speicher[schluessel]; + const wert = ausEnv || ausDb || ""; + const zeile = db + .prepare(`SELECT updated_at FROM app_settings WHERE key = ?`) + .get(PRAEFIX + schluessel); + + raus[schluessel] = { + gesetzt: !!wert, + /* Die Betriebsart ist kein Geheimnis und wird ausgeschrieben -- + sie ist die Angabe, bei der man sich am ehesten vertut, und + "gesetzt (7 Zeichen)" hilft dabei niemandem. */ + wert: meta.geheim ? null : (schluessel === "PAYPAL_ENV" ? wert : null), + laenge: wert ? wert.length : 0, + quelle: ausEnv ? "server" : (ausDb ? "verwaltung" : null), + geaendert: zeile ? zeile.updated_at : null, + beschreibung: meta.beschreibung, + geheim: meta.geheim, + }; + } + return raus; +} diff --git a/server-internal/lib/webdesign-paypal.js b/server-internal/lib/webdesign-paypal.js index af997882..203b1ad5 100644 --- a/server-internal/lib/webdesign-paypal.js +++ b/server-internal/lib/webdesign-paypal.js @@ -29,6 +29,7 @@ ===================================================================== */ import { randomUUID } from "node:crypto"; +import { einstellung } from "./webdesign-geheimnisse.js"; export class PayPalNichtEingerichtet extends Error { constructor(fehlend) { @@ -47,13 +48,13 @@ export class PayPalNichtEingerichtet extends Error { live) würde echtes Geld bewegt. Die ungefährliche Richtung gehört in die Voreinstellung. */ function basis() { - return process.env.PAYPAL_ENV === "live" + return einstellung("PAYPAL_ENV") === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com"; } export function istLive() { - return process.env.PAYPAL_ENV === "live"; + return einstellung("PAYPAL_ENV") === "live"; } /* Prüft, ob alles Nötige da ist. `fuer` unterscheidet die beiden Welten: @@ -62,9 +63,9 @@ export function istLive() { Betreuungsplan angelegt wurde. */ export function fehlendeEinstellungen(fuer = "einmal") { const fehlt = []; - if (!process.env.PAYPAL_CLIENT_ID) fehlt.push("PAYPAL_CLIENT_ID"); - if (!process.env.PAYPAL_CLIENT_SECRET) fehlt.push("PAYPAL_CLIENT_SECRET"); - if (fuer === "abo" && !process.env.PAYPAL_WD_PLAN_BASIS) fehlt.push("PAYPAL_WD_PLAN_BASIS"); + if (!einstellung("PAYPAL_CLIENT_ID")) fehlt.push("PAYPAL_CLIENT_ID"); + if (!einstellung("PAYPAL_CLIENT_SECRET")) fehlt.push("PAYPAL_CLIENT_SECRET"); + if (fuer === "abo" && !einstellung("PAYPAL_WD_PLAN_BASIS")) fehlt.push("PAYPAL_WD_PLAN_BASIS"); return fehlt; } @@ -86,7 +87,7 @@ async function zugangstoken() { if (token && token.laeuftAb > Date.now() + 60_000) return token.wert; const anmeldung = Buffer.from( - `${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}` + `${einstellung("PAYPAL_CLIENT_ID")}:${einstellung("PAYPAL_CLIENT_SECRET")}` ).toString("base64"); const antwort = await fetch(`${basis()}/v1/oauth2/token`, { @@ -111,6 +112,14 @@ async function zugangstoken() { /* Für Tests: Zwischenspeicher leeren, damit ein Wechsel der Zugangsdaten sofort wirkt statt erst nach einer Stunde. */ +/* Meldet sich testweise an und wirft, wenn PayPal ablehnt. Bewegt kein + Geld. Erst das beweist, dass die Zugangsdaten stimmen -- "gesetzt" + heisst nur, dass etwas dasteht. */ +export async function zugangstokenPruefen() { + await zugangstoken(); + return true; +} + export function tokenVergessen() { token = null; } @@ -158,6 +167,17 @@ export function paypalZuCent(wert) { return parseInt(ganz, 10) * 100 + parseInt((rest + "00").slice(0, 2), 10); } +/* WOHER DIE ZUGANGSDATEN KOMMEN + + Nicht mehr direkt aus process.env, sondern ueber einstellung() aus + lib/webdesign-geheimnisse.js. Die liest zuerst die .env (dort hat sie + Vorrang) und faellt sonst auf die verschluesselten Werte zurueck, die + Filipe in seiner Verwaltung eintragen kann. + + Der Grund ist praktisch: Fuer die .env braucht es SSH, einen Editor + und einen Dienstneustart. Ueber die Verwaltung wirkt eine Aenderung + sofort -- und niemand muss in die Konsole. */ + /* --------------------------------------------------------------------- 1) EINMALZAHLUNG — Bestellung anlegen Der Kunde wird danach zum "approve"-Link geschickt und bestätigt dort @@ -273,9 +293,9 @@ export async function bestellungAbfragen(orderId) { PayPal den Betrag am Plan festmacht — nicht am einzelnen Abo. */ export function planFuerPaket(paket) { const karte = { - basis: process.env.PAYPAL_WD_PLAN_BASIS, - plus: process.env.PAYPAL_WD_PLAN_PLUS, - premium: process.env.PAYPAL_WD_PLAN_PREMIUM, + basis: einstellung("PAYPAL_WD_PLAN_BASIS"), + plus: einstellung("PAYPAL_WD_PLAN_PLUS"), + premium: einstellung("PAYPAL_WD_PLAN_PREMIUM"), }; return karte[paket] || karte.basis || null; } @@ -339,7 +359,7 @@ export async function aboKuendigen(subscriptionId, grund) { --------------------------------------------------------------------- */ export async function webhookEchtheitPruefen(kopfzeilen, rohkoerper) { pruefeEingerichtet("einmal"); - if (!process.env.PAYPAL_WD_WEBHOOK_ID) { + if (!einstellung("PAYPAL_WD_WEBHOOK_ID")) { throw new PayPalNichtEingerichtet(["PAYPAL_WD_WEBHOOK_ID"]); } @@ -351,7 +371,7 @@ export async function webhookEchtheitPruefen(kopfzeilen, rohkoerper) { transmission_id: kopfzeilen["paypal-transmission-id"], transmission_sig: kopfzeilen["paypal-transmission-sig"], transmission_time: kopfzeilen["paypal-transmission-time"], - webhook_id: process.env.PAYPAL_WD_WEBHOOK_ID, + webhook_id: einstellung("PAYPAL_WD_WEBHOOK_ID"), webhook_event: JSON.parse(rohkoerper), }), }); diff --git a/server-internal/routes/webdesign-zahlungen.js b/server-internal/routes/webdesign-zahlungen.js index cf5688cc..35bb5294 100644 --- a/server-internal/routes/webdesign-zahlungen.js +++ b/server-internal/routes/webdesign-zahlungen.js @@ -51,6 +51,7 @@ import { ausweisPruefen } from "../lib/webdesign-ausweis.js"; import { brauchtKunde } from "./webdesign-portal.js"; import { zustimmungSpeichern } from "./webdesign-widerruf.js"; import * as paypal from "../lib/webdesign-paypal.js"; +import * as geheim from "../lib/webdesign-geheimnisse.js"; import { jetzt, neueId, naechsteNummer, text, verlauf, centZuText, besucherIp, spracheOderDe, @@ -454,3 +455,125 @@ export async function zahlungenListe(req, res) { }, }); } + +/* ===================================================================== + EINSTELLUNGEN — PayPal-Zugangsdaten über die Verwaltung eintragen + + Damit Filipe dafür nicht in die Konsole muss. Die Werte werden + verschlüsselt gespeichert (siehe lib/webdesign-geheimnisse.js) und + wirken sofort — ohne Dienstneustart. + ===================================================================== */ + +/* GET /webdesign/admin/einstellungen + + Gibt NIE einen hinterlegten Wert zurück, auch nicht teilweise. Nur: + ist etwas da, woher stammt es, wie lang ist es, wann zuletzt geändert. + Ein Geheimnis, das man wieder auslesen kann, ist über kurz oder lang + auch dort, wo es nicht hingehört. */ +export async function einstellungenLesen(req, res) { + const p = await darf(req, "WD_ZAHLUNGEN_VIEW"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + return json(res, { + ok: true, + stand: geheim.stand(), + einrichtung: { + einmal: paypal.istEingerichtet("einmal"), + abo: paypal.istEingerichtet("abo"), + fehlt: paypal.fehlendeEinstellungen("abo"), + live: paypal.istLive(), + }, + webhookUrl: `${(process.env.API_BASIS_URL || "https://postfach.dogfather-universe.com")}/webdesign/paypal-webhook`, + }); +} + +/* POST /webdesign/admin/einstellungen + + Nimmt beliebig viele der erlaubten Schlüssel entgegen. Ein FEHLENDER + Schlüssel bleibt unverändert — nur ein ausdrücklich leerer Wert + löscht. Sonst würde ein Formular, das nur ein Feld ausfüllt, beim + Speichern alle anderen leeren. */ +export async function einstellungenSchreiben(req, res) { + const p = await darf(req, "WD_ZAHLUNGEN_VIEW"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + const eingang = req.body?.werte; + if (!eingang || typeof eingang !== "object") { + return json(res, { ok: false, error: "Keine Werte übermittelt." }, 400); + } + + const geaendert = []; + const abgelehnt = []; + + for (const [schluessel, wert] of Object.entries(eingang)) { + if (!(schluessel in geheim.ERLAUBTE_SCHLUESSEL)) { abgelehnt.push(schluessel); continue; } + /* undefined = "nicht angefasst". Leerer String = "löschen". Das ist + ein echter Unterschied und darf nicht verwischt werden. */ + if (wert === undefined || wert === null) continue; + try { + const r = await geheim.einstellungSetzen(schluessel, wert); + geaendert.push(schluessel + (r.entfernt ? " (entfernt)" : "")); + } catch (fehler) { + return json(res, { ok: false, error: fehler.message }, 400); + } + } + + /* PayPal merkt sich ein Zugangstoken. Nach einem Wechsel der + Zugangsdaten wäre es das alte — und die nächste Zahlung liefe noch + über das vorherige Konto oder scheiterte mit "invalid client". + Deshalb hier ausdrücklich vergessen. */ + paypal.tokenVergessen(); + + /* Im Protokoll steht, WAS geändert wurde, nie WOMIT. */ + verlauf("einstellungen", "paypal", "geaendert", + "team:" + (p.sitzung?.actor || "?"), { felder: geaendert }); + + return json(res, { + ok: true, + geaendert, + abgelehnt, + stand: geheim.stand(), + einrichtung: { + einmal: paypal.istEingerichtet("einmal"), + abo: paypal.istEingerichtet("abo"), + fehlt: paypal.fehlendeEinstellungen("abo"), + live: paypal.istLive(), + }, + }); +} + +/* POST /webdesign/admin/einstellungen/pruefen + + Meldet sich testweise bei PayPal an. Erst das beweist, dass die Werte + stimmen — "gesetzt" heisst nur, dass etwas dasteht, nicht dass es + richtig ist. Es fliesst dabei kein Geld. */ +export async function einstellungenPruefen(req, res) { + const p = await darf(req, "WD_ZAHLUNGEN_VIEW"); + if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund); + + if (!paypal.istEingerichtet("einmal")) { + return json(res, { + ok: false, + error: "Es fehlen noch Werte: " + paypal.fehlendeEinstellungen("einmal").join(", "), + }, 400); + } + + try { + paypal.tokenVergessen(); + await paypal.zugangstokenPruefen(); + return json(res, { + ok: true, + live: paypal.istLive(), + meldung: paypal.istLive() + ? "Anmeldung bei PayPal erfolgreich — Echtbetrieb." + : "Anmeldung erfolgreich — aber im TESTMODUS. Es fliesst kein echtes Geld.", + }); + } catch (fehler) { + return json(res, { + ok: false, + error: "PayPal hat die Anmeldung abgelehnt. Meist stimmen Client ID und Secret " + + "nicht zusammen, oder sie stammen aus verschiedenen Apps bzw. aus dem Testbereich.", + technisch: String(fehler.message || fehler).slice(0, 200), + }, 400); + } +} diff --git a/server-internal/test-geheimnisse.mjs b/server-internal/test-geheimnisse.mjs new file mode 100644 index 00000000..b9e7e9cd --- /dev/null +++ b/server-internal/test-geheimnisse.mjs @@ -0,0 +1,93 @@ +/* Prueft die Ablage der PayPal-Zugangsdaten. + + Der wichtigste Punkt ist NICHT, dass Speichern funktioniert, sondern + dass ein gespeichertes Geheimnis auf keinem Weg wieder herauskommt. +*/ +import { mkdtempSync, rmSync, appendFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { randomBytes } from "node:crypto"; + +const tmp = mkdtempSync(join(tmpdir(), "wd-geh-")); +process.env.DB_PATH = join(tmp, "test.db"); +process.env.ENCRYPTION_KEY = randomBytes(32).toString("base64"); +delete process.env.PAYPAL_CLIENT_ID; +delete process.env.PAYPAL_CLIENT_SECRET; +delete process.env.PAYPAL_ENV; + +const LOG = "/tmp/wd-geh.log"; +const L = (...a) => { try { appendFileSync(LOG, a.join(" ") + "\n"); } catch (e) {} console.log(...a); }; + +const { db, initDb } = await import("./db.js"); +initDb(); +const geheim = await import("./lib/webdesign-geheimnisse.js"); +const paypal = await import("./lib/webdesign-paypal.js"); + +let ok = 0, fehler = 0; +const pruefe = (n, b, z) => { if (b) { ok++; L(" ok " + n); } else { fehler++; L(" FEHL " + n + (z ? " -> " + z : "")); } }; + +const SECRET = 'EMeineHeikleZeichen&/\$"~UndLang1234567890'; + +L("\n1. SPEICHERN UND LESEN"); +await geheim.geheimnisseLaden(); +pruefe("vor dem Eintragen gilt PayPal als nicht eingerichtet", paypal.istEingerichtet("einmal") === false); + +await geheim.einstellungSetzen("PAYPAL_CLIENT_ID", "AbC-123"); +await geheim.einstellungSetzen("PAYPAL_CLIENT_SECRET", SECRET); +pruefe("danach gilt es als eingerichtet", paypal.istEingerichtet("einmal") === true); +pruefe("der Wert kommt zeichengenau zurueck", geheim.einstellung("PAYPAL_CLIENT_SECRET") === SECRET); +pruefe("Sonderzeichen ueberstehen die Verschluesselung", + geheim.einstellung("PAYPAL_CLIENT_SECRET").includes('&/\$"~')); + +L("\n2. IN DER DATENBANK STEHT NICHTS LESBARES"); +const roh = db.prepare("SELECT value FROM app_settings WHERE key = ?").get("wd_geheim_PAYPAL_CLIENT_SECRET"); +pruefe("der Eintrag existiert", !!roh); +pruefe("er enthaelt NICHT den Klartext", roh && !roh.value.includes(SECRET), roh ? roh.value.slice(0, 30) : "-"); +pruefe("er enthaelt auch keinen Teil davon", roh && !roh.value.includes("MeineHeikle")); + +L("\n3. DER STAND VERRAET DEN WERT NICHT"); +const st = geheim.stand(); +const alsText = JSON.stringify(st); +pruefe("kein Geheimnis in der Antwort", !alsText.includes(SECRET) && !alsText.includes("MeineHeikle")); +pruefe("aber es ist erkennbar, DASS etwas da ist", st.PAYPAL_CLIENT_SECRET.gesetzt === true); +pruefe("die Laenge wird gemeldet", st.PAYPAL_CLIENT_SECRET.laenge === SECRET.length, + String(st.PAYPAL_CLIENT_SECRET.laenge)); +pruefe("das Secret ist als geheim markiert", st.PAYPAL_CLIENT_SECRET.wert === null); +pruefe("die Betriebsart wird dagegen ausgeschrieben", st.PAYPAL_ENV.wert !== undefined); +pruefe("die Herkunft wird benannt", st.PAYPAL_CLIENT_SECRET.quelle === "verwaltung"); + +L("\n4. DIE .env HAT VORRANG"); +process.env.PAYPAL_CLIENT_ID = "AUS-DER-ENV"; +pruefe("ein Wert auf dem Server sticht die Verwaltung", + geheim.einstellung("PAYPAL_CLIENT_ID") === "AUS-DER-ENV"); +pruefe("und das wird auch so angezeigt", geheim.stand().PAYPAL_CLIENT_ID.quelle === "server"); +delete process.env.PAYPAL_CLIENT_ID; +pruefe("faellt der Server-Wert weg, greift wieder die Verwaltung", + geheim.einstellung("PAYPAL_CLIENT_ID") === "AbC-123"); + +L("\n5. NUR ERLAUBTE SCHLUESSEL"); +let abgelehnt = false; +try { await geheim.einstellungSetzen("ADMIN_PASSWORT", "x"); } catch (e) { abgelehnt = true; } +pruefe("ein fremder Schluessel wird abgewiesen", abgelehnt); +let falscheArt = false; +try { await geheim.einstellungSetzen("PAYPAL_ENV", "vielleicht"); } catch (e) { falscheArt = true; } +pruefe("die Betriebsart erlaubt nur live oder sandbox", falscheArt); + +L("\n6. LEEREN"); +await geheim.einstellungSetzen("PAYPAL_CLIENT_SECRET", ""); +pruefe("ein leerer Wert entfernt den Eintrag", geheim.einstellung("PAYPAL_CLIENT_SECRET") === ""); +pruefe("und die Zeile ist wirklich weg", + !db.prepare("SELECT 1 FROM app_settings WHERE key = ?").get("wd_geheim_PAYPAL_CLIENT_SECRET")); +pruefe("danach gilt PayPal wieder als nicht eingerichtet", paypal.istEingerichtet("einmal") === false); + +L("\n7. FALSCHER SCHLUESSEL BRINGT DEN DIENST NICHT ZUM STEHEN"); +await geheim.einstellungSetzen("PAYPAL_CLIENT_SECRET", SECRET); +process.env.ENCRYPTION_KEY = randomBytes(32).toString("base64"); +let gestuerzt = false; +try { await geheim.geheimnisseLaden(); } catch (e) { gestuerzt = true; } +pruefe("das Laden mit falschem Schluessel wirft nicht", !gestuerzt); +pruefe("der Wert gilt dann als nicht vorhanden", geheim.einstellung("PAYPAL_CLIENT_SECRET") === ""); + +L(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`); +process.on("exit", () => rmSync(tmp, { recursive: true, force: true })); +process.exitCode = fehler ? 1 : 0; diff --git a/webdesign/verwaltung.html b/webdesign/verwaltung.html index 05eefa0e..99540fb6 100644 --- a/webdesign/verwaltung.html +++ b/webdesign/verwaltung.html @@ -638,6 +638,7 @@ + @@ -725,6 +726,13 @@
Wird geladen…
+ + +