WIP: PayPal-Einstellungen ueber die Verwaltung (Test folgt auf dem Server)
This commit is contained in:
@@ -124,7 +124,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
zahlungAnsehen, zahlungStarten, zahlungAbschliessen,
|
zahlungAnsehen, zahlungStarten, zahlungAbschliessen,
|
||||||
paypalWebhook, zahlungAnlegen, zahlungenListe,
|
paypalWebhook, zahlungAnlegen, zahlungenListe,
|
||||||
|
einstellungenLesen, einstellungenSchreiben, einstellungenPruefen,
|
||||||
} from "./routes/webdesign-zahlungen.js";
|
} 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";
|
import { vorlagenSicherstellen } from "./lib/webdesign-vorlagen.js";
|
||||||
|
|
||||||
initDb();
|
initDb();
|
||||||
@@ -135,6 +141,14 @@ initDb();
|
|||||||
Neustart weg -- und niemand kaeme auf die Idee, dass der Neustart
|
Neustart weg -- und niemand kaeme auf die Idee, dass der Neustart
|
||||||
daran schuld war. */
|
daran schuld war. */
|
||||||
vorlagenSicherstellen();
|
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 PORT = Number(process.env.PORT || 4200);
|
||||||
const app = express();
|
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.post("/webdesign/portal/zahlung/:id/abschluss", wrap(zahlungAbschliessen));
|
||||||
app.get("/webdesign/admin/zahlungen", wrap(zahlungenListe));
|
app.get("/webdesign/admin/zahlungen", wrap(zahlungenListe));
|
||||||
app.post("/webdesign/admin/zahlungen", wrap(zahlungAnlegen));
|
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", wrap(adminPostfachListe));
|
||||||
app.get("/webdesign/admin/postfach-zaehler", wrap(adminPostfachZaehler));
|
app.get("/webdesign/admin/postfach-zaehler", wrap(adminPostfachZaehler));
|
||||||
app.get("/webdesign/admin/postfach/:kundeId", wrap(adminPostfachVerlauf));
|
app.get("/webdesign/admin/postfach/:kundeId", wrap(adminPostfachVerlauf));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
===================================================================== */
|
===================================================================== */
|
||||||
|
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { einstellung } from "./webdesign-geheimnisse.js";
|
||||||
|
|
||||||
export class PayPalNichtEingerichtet extends Error {
|
export class PayPalNichtEingerichtet extends Error {
|
||||||
constructor(fehlend) {
|
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
|
live) würde echtes Geld bewegt. Die ungefährliche Richtung gehört in die
|
||||||
Voreinstellung. */
|
Voreinstellung. */
|
||||||
function basis() {
|
function basis() {
|
||||||
return process.env.PAYPAL_ENV === "live"
|
return einstellung("PAYPAL_ENV") === "live"
|
||||||
? "https://api-m.paypal.com"
|
? "https://api-m.paypal.com"
|
||||||
: "https://api-m.sandbox.paypal.com";
|
: "https://api-m.sandbox.paypal.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function istLive() {
|
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:
|
/* Prüft, ob alles Nötige da ist. `fuer` unterscheidet die beiden Welten:
|
||||||
@@ -62,9 +63,9 @@ export function istLive() {
|
|||||||
Betreuungsplan angelegt wurde. */
|
Betreuungsplan angelegt wurde. */
|
||||||
export function fehlendeEinstellungen(fuer = "einmal") {
|
export function fehlendeEinstellungen(fuer = "einmal") {
|
||||||
const fehlt = [];
|
const fehlt = [];
|
||||||
if (!process.env.PAYPAL_CLIENT_ID) fehlt.push("PAYPAL_CLIENT_ID");
|
if (!einstellung("PAYPAL_CLIENT_ID")) fehlt.push("PAYPAL_CLIENT_ID");
|
||||||
if (!process.env.PAYPAL_CLIENT_SECRET) fehlt.push("PAYPAL_CLIENT_SECRET");
|
if (!einstellung("PAYPAL_CLIENT_SECRET")) fehlt.push("PAYPAL_CLIENT_SECRET");
|
||||||
if (fuer === "abo" && !process.env.PAYPAL_WD_PLAN_BASIS) fehlt.push("PAYPAL_WD_PLAN_BASIS");
|
if (fuer === "abo" && !einstellung("PAYPAL_WD_PLAN_BASIS")) fehlt.push("PAYPAL_WD_PLAN_BASIS");
|
||||||
return fehlt;
|
return fehlt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ async function zugangstoken() {
|
|||||||
if (token && token.laeuftAb > Date.now() + 60_000) return token.wert;
|
if (token && token.laeuftAb > Date.now() + 60_000) return token.wert;
|
||||||
|
|
||||||
const anmeldung = Buffer.from(
|
const anmeldung = Buffer.from(
|
||||||
`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
|
`${einstellung("PAYPAL_CLIENT_ID")}:${einstellung("PAYPAL_CLIENT_SECRET")}`
|
||||||
).toString("base64");
|
).toString("base64");
|
||||||
|
|
||||||
const antwort = await fetch(`${basis()}/v1/oauth2/token`, {
|
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
|
/* Für Tests: Zwischenspeicher leeren, damit ein Wechsel der Zugangsdaten
|
||||||
sofort wirkt statt erst nach einer Stunde. */
|
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() {
|
export function tokenVergessen() {
|
||||||
token = null;
|
token = null;
|
||||||
}
|
}
|
||||||
@@ -158,6 +167,17 @@ export function paypalZuCent(wert) {
|
|||||||
return parseInt(ganz, 10) * 100 + parseInt((rest + "00").slice(0, 2), 10);
|
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
|
1) EINMALZAHLUNG — Bestellung anlegen
|
||||||
Der Kunde wird danach zum "approve"-Link geschickt und bestätigt dort
|
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. */
|
PayPal den Betrag am Plan festmacht — nicht am einzelnen Abo. */
|
||||||
export function planFuerPaket(paket) {
|
export function planFuerPaket(paket) {
|
||||||
const karte = {
|
const karte = {
|
||||||
basis: process.env.PAYPAL_WD_PLAN_BASIS,
|
basis: einstellung("PAYPAL_WD_PLAN_BASIS"),
|
||||||
plus: process.env.PAYPAL_WD_PLAN_PLUS,
|
plus: einstellung("PAYPAL_WD_PLAN_PLUS"),
|
||||||
premium: process.env.PAYPAL_WD_PLAN_PREMIUM,
|
premium: einstellung("PAYPAL_WD_PLAN_PREMIUM"),
|
||||||
};
|
};
|
||||||
return karte[paket] || karte.basis || null;
|
return karte[paket] || karte.basis || null;
|
||||||
}
|
}
|
||||||
@@ -339,7 +359,7 @@ export async function aboKuendigen(subscriptionId, grund) {
|
|||||||
--------------------------------------------------------------------- */
|
--------------------------------------------------------------------- */
|
||||||
export async function webhookEchtheitPruefen(kopfzeilen, rohkoerper) {
|
export async function webhookEchtheitPruefen(kopfzeilen, rohkoerper) {
|
||||||
pruefeEingerichtet("einmal");
|
pruefeEingerichtet("einmal");
|
||||||
if (!process.env.PAYPAL_WD_WEBHOOK_ID) {
|
if (!einstellung("PAYPAL_WD_WEBHOOK_ID")) {
|
||||||
throw new PayPalNichtEingerichtet(["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_id: kopfzeilen["paypal-transmission-id"],
|
||||||
transmission_sig: kopfzeilen["paypal-transmission-sig"],
|
transmission_sig: kopfzeilen["paypal-transmission-sig"],
|
||||||
transmission_time: kopfzeilen["paypal-transmission-time"],
|
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),
|
webhook_event: JSON.parse(rohkoerper),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { ausweisPruefen } from "../lib/webdesign-ausweis.js";
|
|||||||
import { brauchtKunde } from "./webdesign-portal.js";
|
import { brauchtKunde } from "./webdesign-portal.js";
|
||||||
import { zustimmungSpeichern } from "./webdesign-widerruf.js";
|
import { zustimmungSpeichern } from "./webdesign-widerruf.js";
|
||||||
import * as paypal from "../lib/webdesign-paypal.js";
|
import * as paypal from "../lib/webdesign-paypal.js";
|
||||||
|
import * as geheim from "../lib/webdesign-geheimnisse.js";
|
||||||
import {
|
import {
|
||||||
jetzt, neueId, naechsteNummer, text, verlauf, centZuText,
|
jetzt, neueId, naechsteNummer, text, verlauf, centZuText,
|
||||||
besucherIp, spracheOderDe,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -638,6 +638,7 @@
|
|||||||
<button type="button" class="vw-filter-knopf vw-an" data-tab="anfragen">Anfragen</button>
|
<button type="button" class="vw-filter-knopf vw-an" data-tab="anfragen">Anfragen</button>
|
||||||
<button type="button" class="vw-filter-knopf" data-tab="projekte">Projekte</button>
|
<button type="button" class="vw-filter-knopf" data-tab="projekte">Projekte</button>
|
||||||
<button type="button" class="vw-filter-knopf" data-tab="kunden">Kunden</button>
|
<button type="button" class="vw-filter-knopf" data-tab="kunden">Kunden</button>
|
||||||
|
<button type="button" class="vw-filter-knopf" data-tab="zahlungen">Zahlungen</button>
|
||||||
<button type="button" class="vw-filter-knopf" data-tab="postfach">Postfach<span
|
<button type="button" class="vw-filter-knopf" data-tab="postfach">Postfach<span
|
||||||
class="vw-abzeichen" id="vw-pf-zaehler" hidden>0</span></button>
|
class="vw-abzeichen" id="vw-pf-zaehler" hidden>0</span></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -725,6 +726,13 @@
|
|||||||
<div id="vw-projekte-liste"><div class="vw-laedt">Wird geladen…</div></div>
|
<div id="vw-projekte-liste"><div class="vw-laedt">Wird geladen…</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ ZAHLUNGEN ============ -->
|
||||||
|
<div id="vw-zahlungen-bereich" hidden>
|
||||||
|
<div id="vw-pp-stand"><div class="vw-laedt">Wird geladen…</div></div>
|
||||||
|
<h2 style="font-size:1.15rem;margin-top:1.6rem">Zahlungen</h2>
|
||||||
|
<div id="vw-zahlungen-liste"><div class="vw-laedt">Wird geladen…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ============ POSTFACH ============ -->
|
<!-- ============ POSTFACH ============ -->
|
||||||
<div id="vw-postfach-bereich" hidden>
|
<div id="vw-postfach-bereich" hidden>
|
||||||
<p class="wd-klein wd-leise" style="margin-bottom:1rem">
|
<p class="wd-klein wd-leise" style="margin-bottom:1rem">
|
||||||
@@ -1423,6 +1431,7 @@
|
|||||||
anfragen: { titel: "Projektanfragen", kasten: null },
|
anfragen: { titel: "Projektanfragen", kasten: null },
|
||||||
projekte: { titel: "Projekte", kasten: "vw-projekte-bereich" },
|
projekte: { titel: "Projekte", kasten: "vw-projekte-bereich" },
|
||||||
kunden: { titel: "Kunden", kasten: "vw-kunden-bereich" },
|
kunden: { titel: "Kunden", kasten: "vw-kunden-bereich" },
|
||||||
|
zahlungen:{ titel: "Zahlungen", kasten: "vw-zahlungen-bereich" },
|
||||||
postfach: { titel: "Postfach", kasten: "vw-postfach-bereich" },
|
postfach: { titel: "Postfach", kasten: "vw-postfach-bereich" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1443,6 +1452,7 @@
|
|||||||
|
|
||||||
if (aktiverTab === "kunden") ladeKunden();
|
if (aktiverTab === "kunden") ladeKunden();
|
||||||
if (aktiverTab === "projekte") ladeProjekte();
|
if (aktiverTab === "projekte") ladeProjekte();
|
||||||
|
if (aktiverTab === "zahlungen") ladeZahlungen();
|
||||||
if (aktiverTab === "postfach") ladePostfach();
|
if (aktiverTab === "postfach") ladePostfach();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1956,6 +1966,197 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =================================================================
|
||||||
|
ZAHLUNGEN UND PAYPAL-EINRICHTUNG
|
||||||
|
|
||||||
|
Filipe soll die Zugangsdaten HIER eintragen koennen und nicht per
|
||||||
|
SSH in einer Datei. Die Werte gehen verschluesselt in die Datenbank
|
||||||
|
und wirken sofort -- ohne Dienstneustart.
|
||||||
|
|
||||||
|
Was hier NIE passiert: einen hinterlegten Wert wieder anzeigen. Der
|
||||||
|
Server gibt ihn gar nicht erst heraus. Sichtbar ist nur, ob etwas da
|
||||||
|
ist, woher es stammt und wie lang es ist.
|
||||||
|
================================================================= */
|
||||||
|
var PP_FELDER = [
|
||||||
|
{ k: "PAYPAL_ENV", titel: "Betriebsart", art: "wahl",
|
||||||
|
hilfe: "live = echtes Geld. sandbox = Testbetrieb, es fliesst nichts." },
|
||||||
|
{ k: "PAYPAL_CLIENT_ID", titel: "Client ID", art: "text",
|
||||||
|
hilfe: "developer.paypal.com → Apps & Credentials → deine App" },
|
||||||
|
{ k: "PAYPAL_CLIENT_SECRET", titel: "Secret", art: "geheim",
|
||||||
|
hilfe: "Dort daneben auf „Show“ klicken. Wird nach dem Speichern nie wieder angezeigt." },
|
||||||
|
{ k: "PAYPAL_WD_WEBHOOK_ID", titel: "Webhook-Kennung", art: "text",
|
||||||
|
hilfe: "Beginnt mit WH-. Entsteht, wenn du den Webhook anlegst." },
|
||||||
|
{ k: "PAYPAL_WD_PLAN_BASIS", titel: "Plan für die Betreuung", art: "text",
|
||||||
|
hilfe: "Beginnt mit P-. Nur nötig für monatliche Betreuungs-Abos." },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function ladeZahlungen() {
|
||||||
|
var ziel = document.getElementById("vw-pp-stand");
|
||||||
|
ziel.innerHTML = '<div class="vw-laedt">Wird geladen…</div>';
|
||||||
|
try {
|
||||||
|
var d = await api("/webdesign/admin/einstellungen");
|
||||||
|
zeichnePayPal(d);
|
||||||
|
} catch (fehler) {
|
||||||
|
if (fehler.status === 401) { token = null; tokenSchreiben(null); zeigeAnmeldung(); return; }
|
||||||
|
ziel.innerHTML = '<div class="vw-leer-hinweis">' + s(fehler.message) + "</div>";
|
||||||
|
}
|
||||||
|
ladeZahlungsliste();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeichnePayPal(d) {
|
||||||
|
var e = d.einrichtung || {};
|
||||||
|
var st = d.stand || {};
|
||||||
|
|
||||||
|
var kopf = e.einmal
|
||||||
|
? (e.live
|
||||||
|
? '<div class="wd-hinweis" style="margin-bottom:1rem;border-left-color:var(--wd-ok)">' +
|
||||||
|
'<p class="wd-hinweis-titel" style="color:var(--wd-ok)">PayPal ist eingerichtet — Echtbetrieb</p>' +
|
||||||
|
'<p class="wd-klein">Zahlungen gehen direkt auf dein Konto.</p></div>'
|
||||||
|
: '<div class="wd-hinweis" style="margin-bottom:1rem"><p class="wd-hinweis-titel">' +
|
||||||
|
'Eingerichtet, aber im TESTMODUS</p><p class="wd-klein">Es fließt kein echtes Geld. ' +
|
||||||
|
'Stell die Betriebsart auf <strong>Echtbetrieb</strong>, sobald du bereit bist.</p></div>')
|
||||||
|
: '<div class="wd-hinweis" style="margin-bottom:1rem"><p class="wd-hinweis-titel">' +
|
||||||
|
'Noch nicht eingerichtet</p><p class="wd-klein">Es fehlt: <strong>' +
|
||||||
|
s((e.fehlt || []).join(", ") || "—") + '</strong>. ' +
|
||||||
|
'Solange sagt das Kundenportal freundlich Bescheid und nennt den Weg über das Postfach.</p></div>';
|
||||||
|
|
||||||
|
var felder = PP_FELDER.map(function (f) {
|
||||||
|
var z = st[f.k] || {};
|
||||||
|
var lage = z.gesetzt
|
||||||
|
? '<span class="wd-tag wd-tag--ok">hinterlegt' +
|
||||||
|
(z.quelle === "server" ? " (auf dem Server)" : "") +
|
||||||
|
(z.laenge ? " · " + z.laenge + " Zeichen" : "") + "</span>"
|
||||||
|
: '<span class="wd-tag wd-tag--gold">fehlt</span>';
|
||||||
|
|
||||||
|
if (f.art === "wahl") {
|
||||||
|
var jetztWert = z.wert || "sandbox";
|
||||||
|
return '<div class="wd-feld" style="margin-bottom:1rem">' +
|
||||||
|
"<label>" + s(f.titel) + " " + lage + "</label>" +
|
||||||
|
'<div class="vw-status-wahl" style="margin:.3rem 0">' +
|
||||||
|
'<button type="button" class="vw-filter-knopf' + (jetztWert === "sandbox" ? " vw-an" : "") +
|
||||||
|
'" data-pp-env="sandbox">Testmodus</button>' +
|
||||||
|
'<button type="button" class="vw-filter-knopf' + (jetztWert === "live" ? " vw-an" : "") +
|
||||||
|
'" data-pp-env="live">Echtbetrieb</button>' +
|
||||||
|
"</div>" +
|
||||||
|
'<span class="wd-feld-hilfe">' + s(f.hilfe) + "</span></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der Platzhalter sagt ausdruecklich, dass Leerlassen NICHTS
|
||||||
|
aendert. Ohne diesen Hinweis loescht man beim Speichern eines
|
||||||
|
einzelnen Feldes versehentlich die anderen -- oder glaubt es
|
||||||
|
zumindest und traut sich deshalb nicht. */
|
||||||
|
return '<div class="wd-feld" style="margin-bottom:1rem">' +
|
||||||
|
'<label for="pp-' + f.k + '">' + s(f.titel) + " " + lage + "</label>" +
|
||||||
|
'<input type="' + (f.art === "geheim" ? "password" : "text") + '" id="pp-' + f.k +
|
||||||
|
'" autocomplete="off" spellcheck="false" placeholder="' +
|
||||||
|
(z.gesetzt ? "leer lassen → unverändert" : "hier einfügen") + '" />' +
|
||||||
|
'<span class="wd-feld-hilfe">' + s(f.hilfe) + "</span></div>";
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
document.getElementById("vw-pp-stand").innerHTML =
|
||||||
|
kopf +
|
||||||
|
'<div class="wd-karte">' +
|
||||||
|
'<h2 style="font-size:1.15rem;margin-top:0">PayPal einrichten</h2>' +
|
||||||
|
'<p class="wd-klein wd-leise">Die Werte werden verschlüsselt gespeichert und wirken sofort — ' +
|
||||||
|
"kein Neustart nötig. Ein gespeicherter Wert wird nie wieder angezeigt; " +
|
||||||
|
"wer ihn verliert, erzeugt bei PayPal einen neuen.</p>" +
|
||||||
|
'<div class="wd-hinweis" style="margin:.9rem 0">' +
|
||||||
|
'<p class="wd-hinweis-titel">Diese Adresse trägst du bei PayPal als Webhook ein</p>' +
|
||||||
|
'<code style="display:block;word-break:break-all;font-size:.86rem;margin-top:.3rem">' +
|
||||||
|
s(d.webhookUrl || "") + "</code></div>" +
|
||||||
|
felder +
|
||||||
|
'<div class="wd-btn-reihe">' +
|
||||||
|
'<button type="button" class="wd-btn wd-btn--haupt" id="pp-speichern">Speichern</button>' +
|
||||||
|
'<button type="button" class="wd-btn wd-btn--zweit" id="pp-pruefen">Verbindung testen</button>' +
|
||||||
|
"</div>" +
|
||||||
|
'<p class="wd-klein wd-leise" style="margin:.7rem 0 0">' +
|
||||||
|
"„Verbindung testen“ meldet sich probeweise bei PayPal an. " +
|
||||||
|
"Es fließt dabei kein Geld — aber erst das beweist, dass die Werte stimmen.</p>" +
|
||||||
|
"</div>";
|
||||||
|
|
||||||
|
var gewaehlteArt = null;
|
||||||
|
document.querySelectorAll("[data-pp-env]").forEach(function (b) {
|
||||||
|
b.addEventListener("click", function () {
|
||||||
|
gewaehlteArt = b.dataset.ppEnv;
|
||||||
|
document.querySelectorAll("[data-pp-env]").forEach(function (x) {
|
||||||
|
x.classList.toggle("vw-an", x === b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("pp-speichern").addEventListener("click", async function () {
|
||||||
|
var knopf = this;
|
||||||
|
var werte = {};
|
||||||
|
PP_FELDER.forEach(function (f) {
|
||||||
|
if (f.art === "wahl") { if (gewaehlteArt) werte[f.k] = gewaehlteArt; return; }
|
||||||
|
var el = document.getElementById("pp-" + f.k);
|
||||||
|
/* Leeres Feld = NICHT anfassen. Nur so kann man ein einzelnes
|
||||||
|
Feld nachtragen, ohne die anderen zu verlieren. */
|
||||||
|
if (el && el.value.trim()) werte[f.k] = el.value.trim();
|
||||||
|
});
|
||||||
|
if (!Object.keys(werte).length) {
|
||||||
|
window.WD.meldung(meldung, "Nichts eingetragen — es wurde nichts geändert.", "fehler");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.WD.knopfSperren(knopf, true, "…");
|
||||||
|
try {
|
||||||
|
var r = await api("/webdesign/admin/einstellungen", { method: "POST", body: { werte: werte } });
|
||||||
|
/* Felder sofort leeren: Ein Secret soll nicht im Formular stehen
|
||||||
|
bleiben, wenn jemand anders auf den Bildschirm schaut. */
|
||||||
|
PP_FELDER.forEach(function (f) {
|
||||||
|
var el = document.getElementById("pp-" + f.k);
|
||||||
|
if (el) el.value = "";
|
||||||
|
});
|
||||||
|
window.WD.meldung(meldung, "Gespeichert: " + (r.geaendert || []).join(", "), "ok");
|
||||||
|
ladeZahlungen();
|
||||||
|
} catch (f2) {
|
||||||
|
window.WD.knopfSperren(knopf, false);
|
||||||
|
window.WD.meldung(meldung, f2.message, "fehler");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("pp-pruefen").addEventListener("click", async function () {
|
||||||
|
var knopf = this;
|
||||||
|
window.WD.knopfSperren(knopf, true, "Wird getestet…");
|
||||||
|
try {
|
||||||
|
var r = await api("/webdesign/admin/einstellungen/pruefen", { method: "POST", body: {} });
|
||||||
|
window.WD.meldung(meldung, r.meldung, r.live ? "ok" : "fehler");
|
||||||
|
} catch (f2) {
|
||||||
|
window.WD.meldung(meldung, f2.message, "fehler");
|
||||||
|
} finally { window.WD.knopfSperren(knopf, false); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ladeZahlungsliste() {
|
||||||
|
var ziel = document.getElementById("vw-zahlungen-liste");
|
||||||
|
try {
|
||||||
|
var d = await api("/webdesign/admin/zahlungen");
|
||||||
|
if (!d.zahlungen.length) {
|
||||||
|
ziel.innerHTML = '<div class="vw-leer-hinweis" style="padding:2rem 1rem">' +
|
||||||
|
"Noch keine Zahlungen. Sie entstehen im Projekt.</div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ziel.innerHTML = d.zahlungen.map(function (z) {
|
||||||
|
var marke = z.status === "bezahlt" ? "angenommen"
|
||||||
|
: z.status === "offen" ? "neu"
|
||||||
|
: z.status === "fehlge" ? "abgelehnt" : "pruefung";
|
||||||
|
return '<div class="vw-karte" style="cursor:default">' +
|
||||||
|
'<span class="vw-karte-nr">' + s(z.nummer) + "</span>" +
|
||||||
|
'<span class="vw-karte-mitte">' +
|
||||||
|
'<span class="vw-karte-name">' + s(z.betragText) + " · " + s(z.zweck_text || "") + "</span>" +
|
||||||
|
'<span class="vw-karte-info">' + s(z.kunde_name || "") +
|
||||||
|
(z.projekt_titel ? " · " + s(z.projekt_titel) : "") + "</span>" +
|
||||||
|
"</span>" +
|
||||||
|
'<span class="vw-karte-rechts">' +
|
||||||
|
'<span class="vw-status vw-status--' + marke + '">' + s(z.status) + "</span>" +
|
||||||
|
'<span class="vw-karte-zeit">' + window.WD.datum(z.erstellt_am) + "</span>" +
|
||||||
|
"</span></div>";
|
||||||
|
}).join("");
|
||||||
|
} catch (fehler) {
|
||||||
|
ziel.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* =================================================================
|
/* =================================================================
|
||||||
POSTFACH
|
POSTFACH
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user