/* ===================================================================== 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; }