192 lines
8.2 KiB
JavaScript
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;
|
|
}
|