/* ===================================================================== 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. WAS IN DER VERWALTUNG STEHT, GILT. Die .env ist der Rückfall. ⚠️ Das war zuerst andersherum, und die Umkehr hat einen konkreten Anlass (23.08.2026). Ursprünglich hatte die .env Vorrang -- mit dem Gedanken, dass ein Fehlgriff im Formular keine funktionierende Servereinstellung aushebeln soll. Das klingt vorsichtig, war aber falsch: In der .env stand PAYPAL_ENV=sandbox. Filipe stellte im Formular auf "Echtbetrieb" -- und nichts geschah, weil der Serverwert gewann. Seine echten Zugangsdaten wurden gegen den TESTSERVER von PayPal geprüft, der sie zwangsläufig ablehnte. Die Meldung lautete "PayPal hat die Anmeldung abgelehnt" und zeigte damit auf die Zugangsdaten statt auf die Betriebsart. Ein Formular mit Schaltern, die nichts bewirken, ist schlimmer als gar kein Formular: Es behauptet eine Wirkung, die es nicht hat, und schickt einen bei der Fehlersuche in die falsche Richtung. Der Sinn dieser Ablage ist gerade, dass Filipe die Werte OHNE SSH setzen kann. Dann muss das, was er dort einträgt, auch gelten. Gefährlich ist das nicht: Diese Werte werden ausschliesslich vom Webdesign-Bereich gelesen. Das DogiCrew-Supporter-Abo hat sein eigenes Modul (lib/paypal.js) und liest weiterhin direkt aus der Umgebung -- es kann hierüber nicht beeinflusst werden. Welche Quelle gerade greift, zeigt die Verwaltung bei jedem Wert an. 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: Verwaltung zuerst, .env als Rueckfall. Begruendung im Kopf der Datei -- kurz: Wer den Wert im Formular setzt, erwartet, dass er gilt. */ export function einstellung(schluessel) { const ausDb = speicher[schluessel]; if (ausDb && String(ausDb).trim()) return String(ausDb).trim(); const ausEnv = process.env[schluessel]; return ausEnv ? String(ausEnv).trim() : ""; } /* 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]; /* Dieselbe Reihenfolge wie in einstellung() -- sonst zeigt die Anzeige eine andere Quelle an, als tatsaechlich benutzt wird. */ const wert = ausDb || ausEnv || ""; 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: ausDb ? "verwaltung" : (ausEnv ? "server" : null), /* Liegt in BEIDEN etwas, ist das erwaehnenswert: Der Serverwert wird dann nicht benutzt, und das soll man sehen. */ auchAufServer: !!(ausDb && ausEnv), geaendert: zeile ? zeile.updated_at : null, beschreibung: meta.beschreibung, geheim: meta.geheim, }; } return raus; }