Files
dogfather-universe/server-internal/lib/webdesign-geheimnisse.js
T

192 lines
8.2 KiB
JavaScript

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