Die beiden Dateien, in denen Geld und Lagerbestand zusammenlaufen, hatten bisher keinen einzigen Test. Dazu meldete die Go-Live-Pruefung gruene Haken fuer Werte, die gar nicht gesetzt waren. Tests fuer api/checkout/route.ts (10 Faelle) Torwaechter (fremder Origin, Turnstile, Rate-Limit), deaktiviertes Produkt, Preis 0, ausverkaufte Ware, Sitzungslebensdauer — und die beiden Regressionen: scheitert die Stripe-Sitzung, wird die Bestellung aufgeloest und OHNE orderNumber geantwortet; laesst sich die Sitzungs-ID nicht speichern, wird die Sitzung geschlossen. Per Mutationstest geprueft, dass der zweite Fall ohne den Fix rot wird. Tests fuer api/checkout/webhook/route.ts (12 Faelle) Signaturpruefung, verzoegerte Zahlung (SEPA), Auslieferung samt Rechnung und Anhang, mehrfach zugestelltes Ereignis (Gutschein und Rechnungsnummer duerfen sich nicht wiederholen), Zahlung auf eine stornierte Bestellung, Betragsabweichung, abgelaufene Sitzung, fehlgeschlagener Einzug, volle und teilweise Erstattung, Rueckbuchung. Datenbank und Stripe sind in beiden Dateien vollstaendig ersetzt. Go-Live-Pruefung: falsch-positive Haken beseitigt Regeln, die einen Wert voraussetzen, bestanden bei fehlendem Wert einfach — eine leere Zeichenkette enthaelt nun einmal kein "example.com". Der Bericht meldete deshalb "NEXT_PUBLIC_SITE_URL ist die echte Domain" als bestanden, direkt neben dem Blocker "NEXT_PUBLIC_SITE_URL gesetzt". Ohne jeden Stripe-Schluessel galt sogar der Live-Modus als bestaetigt. Das ist die gefaehrlichste Fehlmeldung, die eine solche Pruefung machen kann: sie beruhigt ueber genau den Punkt, der offen ist. mustWhenSet/shouldWhenSet lassen Folgeregeln bei fehlendem Wert entfallen. Ausserdem sagt der Bericht jetzt ausdruecklich, dass er die Umgebung prueft, in der er LAEUFT. Lokal meldet er Blocker, die in der ConfigMap des Clusters laengst erledigt sind; wer das ein paarmal erlebt, liest ueber den Bericht hinweg. Das README nennt den Aufruf im Pod. tsc: 0 Fehler. vitest: 316/316 gruen (zuvor 294). eslint: 0 Warnungen. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
369 lines
17 KiB
TypeScript
369 lines
17 KiB
TypeScript
/**
|
|
* Go-Live-Pruefung: faellt laut durch, statt den Shop still mit
|
|
* Platzhaltern oder Test-Zugangsdaten oeffentlich zu stellen.
|
|
*
|
|
* Aufruf: `pnpm preflight` (liest .env) — gedacht als letzter Schritt vor
|
|
* dem ersten oeffentlichen Deploy und als Schritt in der Deploy-Pipeline.
|
|
* Endet mit Exit-Code 1, sobald ein Blocker offen ist.
|
|
*
|
|
* Die Inhaltspruefungen fragen bewusst die DATENBANK und nicht den
|
|
* Quelltext: Impressum, Datenschutz, AGB, Widerrufsbelehrung und die
|
|
* Telefonnummer sind im Admin-Bereich unter "Inhalte" pflegbar (siehe
|
|
* lib/content-registry.ts). Die Texte in i18n/dictionaries/ sind nur der
|
|
* Rueckfallwert, solange dort nichts gepflegt ist. Eine Pruefung, die nur
|
|
* in den Quelltext schaut, meldet deshalb einen Blocker, den die
|
|
* Betreiberin im Admin laengst geschlossen hat — und schweigt umgekehrt,
|
|
* wenn jemand einen Platzhaltertext erst im Admin eintraegt.
|
|
*
|
|
* Ist die Datenbank nicht erreichbar (Aufruf von einer Maschine ohne
|
|
* Zugang zur Produktionsdatenbank), werden diese Pruefungen ausdruecklich
|
|
* als ungeprueft gemeldet statt geraten.
|
|
*/
|
|
import "dotenv/config";
|
|
import path from "node:path";
|
|
import { prisma } from "../src/lib/prisma";
|
|
import { getContentValue } from "../src/lib/content";
|
|
import { locales } from "../src/i18n/config";
|
|
import { getStripeClient } from "../src/lib/stripe";
|
|
|
|
const blockers: string[] = [];
|
|
const warnings: string[] = [];
|
|
const passed: string[] = [];
|
|
|
|
function must(label: string, ok: boolean, hint: string): void {
|
|
if (ok) passed.push(label);
|
|
else blockers.push(`${label}\n ${hint}`);
|
|
}
|
|
|
|
function should(label: string, ok: boolean, hint: string): void {
|
|
if (ok) passed.push(label);
|
|
else warnings.push(`${label}\n ${hint}`);
|
|
}
|
|
|
|
const value = (name: string): string => (process.env[name] ?? "").trim();
|
|
const isSet = (name: string): boolean => value(name).length > 0;
|
|
|
|
/**
|
|
* Eine Regel, die einen GESETZTEN Wert voraussetzt.
|
|
*
|
|
* Ohne diese Weiche meldete der Bericht gruene Haken fuer Variablen, die gar
|
|
* nicht existieren: "NEXT_PUBLIC_SITE_URL ist die echte Domain, kein
|
|
* Platzhalter" stand als bestanden da, weil eine leere Zeichenkette nun einmal
|
|
* kein "example.com" enthaelt — direkt neben dem Blocker
|
|
* "NEXT_PUBLIC_SITE_URL gesetzt". Dasselbe galt fuer den Test auf einen
|
|
* sk_test-Schluessel: ohne jeden Stripe-Schluessel galt der Live-Modus als
|
|
* bestaetigt.
|
|
*
|
|
* Das ist die gefaehrlichste Art von Fehlmeldung, die eine Go-Live-Pruefung
|
|
* machen kann — sie beruhigt ueber genau den Punkt, der offen ist. Fehlt der
|
|
* Wert, entfaellt die Folgeregel hier stillschweigend; gemeldet wird er
|
|
* ohnehin schon von der Regel, die seine blosse Existenz prueft.
|
|
*/
|
|
function mustWhenSet(name: string, label: string, ok: (raw: string) => boolean, hint: string): void {
|
|
if (!isSet(name)) return;
|
|
must(label, ok(value(name)), hint);
|
|
}
|
|
|
|
/** Wie mustWhenSet, nur als Warnung statt als Blocker. */
|
|
function shouldWhenSet(name: string, label: string, ok: (raw: string) => boolean, hint: string): void {
|
|
if (!isSet(name)) return;
|
|
should(label, ok(value(name)), hint);
|
|
}
|
|
|
|
// --- Sitzungs-/Zugangsgeheimnisse -----------------------------------------
|
|
const sessionSecret = value("SESSION_SECRET");
|
|
must(
|
|
"SESSION_SECRET gesetzt und mindestens 32 Zeichen lang",
|
|
sessionSecret.length >= 32,
|
|
"Signiert die Login-Cookies. Erzeugen mit: openssl rand -base64 32"
|
|
);
|
|
mustWhenSet(
|
|
"SESSION_SECRET",
|
|
"SESSION_SECRET ist nicht mehr der Beispielwert",
|
|
(raw) => !raw.includes("change-me"),
|
|
"Der Wert aus .env.example ist oeffentlich bekannt — jede Sitzung waere faelschbar."
|
|
);
|
|
|
|
const adminEmail = value("ADMIN_EMAIL");
|
|
must(
|
|
"ADMIN_EMAIL gesetzt und kein Beispielwert",
|
|
adminEmail.length > 0 && !adminEmail.includes("change-me") && !adminEmail.endsWith("@example.com"),
|
|
'Legt das erste Admin-Konto an (prisma db seed). Eine unvorhersehbare Adresse waehlen, nicht "admin@...".'
|
|
);
|
|
must(
|
|
"ADMIN_PASSWORD gesetzt, kein Beispielwert, mindestens 12 Zeichen",
|
|
value("ADMIN_PASSWORD").length >= 12 && !value("ADMIN_PASSWORD").includes("change-me"),
|
|
"Dieses Konto darf Rueckerstattungen ausloesen und alle Kundendaten einsehen."
|
|
);
|
|
|
|
// --- Oeffentliche Adresse der Seite ---------------------------------------
|
|
const siteUrl = value("NEXT_PUBLIC_SITE_URL");
|
|
must(
|
|
"NEXT_PUBLIC_SITE_URL gesetzt",
|
|
siteUrl.length > 0,
|
|
"Einzige Quelle fuer Sitemap, Canonical-Tags, JSON-LD und alle Links in E-Mails (src/lib/url.ts)."
|
|
);
|
|
mustWhenSet(
|
|
"NEXT_PUBLIC_SITE_URL",
|
|
"NEXT_PUBLIC_SITE_URL nutzt HTTPS und nicht localhost",
|
|
(raw) => raw.startsWith("https://") && !raw.includes("localhost"),
|
|
"Bei HTTP oder localhost landen genau diese Adressen in Bestellbestaetigungen und in der Sitemap."
|
|
);
|
|
mustWhenSet(
|
|
"NEXT_PUBLIC_SITE_URL",
|
|
"NEXT_PUBLIC_SITE_URL ist die echte Domain, kein Platzhalter",
|
|
(raw) => !raw.includes("example.com") && !raw.includes("example.org"),
|
|
'Die echte Domain steht in k8s/01-configmap.yaml (NEXT_PUBLIC_SITE_URL) und k8s/06-ingress.yaml (host/tls.hosts) und muss an beiden Stellen dieselbe sein. Steht hier eine Beispiel-Domain, zeigen Sitemap, Canonical-Tags und jeder Link in jeder Bestellbestaetigung auf eine fremde Domain.'
|
|
);
|
|
|
|
// --- Zahlung ---------------------------------------------------------------
|
|
must(
|
|
"STRIPE_SECRET_KEY gesetzt",
|
|
isSet("STRIPE_SECRET_KEY"),
|
|
"Ohne Stripe-Schluessel schlaegt jeder Checkout fehl — der Shop nimmt keine Bestellung an."
|
|
);
|
|
must(
|
|
"STRIPE_WEBHOOK_SECRET gesetzt",
|
|
isSet("STRIPE_WEBHOOK_SECRET"),
|
|
'Ohne Webhook-Secret wird keine Zahlung bestaetigt: Bestellungen bleiben auf "NEU" stehen, es gibt keine Rechnung und keine Bestaetigungsmail.'
|
|
);
|
|
mustWhenSet(
|
|
"STRIPE_SECRET_KEY",
|
|
"Stripe laeuft im Live-Modus (kein sk_test-Schluessel)",
|
|
(raw) => !raw.startsWith("sk_test"),
|
|
"Mit einem Test-Schluessel zahlt niemand echtes Geld — Bestellungen sehen erfolgreich aus, ohne dass Umsatz entsteht."
|
|
);
|
|
|
|
// --- E-Mail ----------------------------------------------------------------
|
|
must(
|
|
"SMTP_HOST, SMTP_USER und SMTP_PASSWORD gesetzt",
|
|
isSet("SMTP_HOST") && isSet("SMTP_USER") && isSet("SMTP_PASSWORD"),
|
|
"Ohne SMTP werden E-Mails nur ins Server-Log geschrieben (siehe src/lib/mail.ts): keine Bestellbestaetigung, keine Versandmeldung, keine Passwort-Zuruecksetzung."
|
|
);
|
|
// Ohne SMTP_FROM greift der Standardabsender aus lib/mail.ts — deshalb nur
|
|
// pruefen, wenn tatsaechlich ein eigener Wert hinterlegt ist.
|
|
shouldWhenSet(
|
|
"SMTP_FROM",
|
|
"SMTP_FROM zeigt auf eine eigene Domain",
|
|
(raw) => !raw.includes("example.com"),
|
|
"Absenderadressen fremder Domains landen regelmaessig im Spam-Ordner."
|
|
);
|
|
|
|
// --- Umsatzsteuer ----------------------------------------------------------
|
|
// Ein falscher Steuersatz faellt im Betrieb kaum auf: die Rechnung sieht
|
|
// plausibel aus, nur die ausgewiesene Steuer stimmt nicht. Deshalb hier
|
|
// ausdruecklich pruefen, statt sich auf den Rueckfallwert zu verlassen.
|
|
const vatRate = value("VAT_RATE_PERCENT");
|
|
const vatRateNumber = Number(vatRate);
|
|
must(
|
|
"VAT_RATE_PERCENT gesetzt und ein ganzzahliger Prozentwert",
|
|
vatRate.length > 0 && Number.isInteger(vatRateNumber) && vatRateNumber >= 0 && vatRateNumber <= 100,
|
|
"Wird auf jeder Rechnung ausgewiesen. 7 = ermaessigt (Honig), 19 = voll, 0 = Kleinunternehmerregelung nach § 19 UStG. Ohne gueltigen Wert rechnet die Anwendung mit 7% weiter — fuer Kerzen zu niedrig, fuer Kleinunternehmer schlicht falsch."
|
|
);
|
|
shouldWhenSet(
|
|
"VAT_RATE_PERCENT",
|
|
"Umsatzsteuersatz ist ein in Deutschland ueblicher Wert",
|
|
(raw) => ["0", "7", "19"].includes(raw),
|
|
`Eingetragen ist "${vatRate}". Ueblich sind 0 (§ 19 UStG), 7 (ermaessigt) oder 19 (voll) — ein anderer Wert ist moeglich, aber pruefenswert.`
|
|
);
|
|
|
|
// --- Rechnungsablage -------------------------------------------------------
|
|
// Das Upload-Verzeichnis wird oeffentlich und ohne Anmeldung ausgeliefert
|
|
// (app/uploads/[...path]). Liegt die Rechnungsablage darin, sind fremde
|
|
// Rechnungen samt Anschrift und Bestellinhalt fuer jeden abrufbar, der den
|
|
// Pfad erraet — und im Betrieb faellt das nicht auf, weil weiterhin alles
|
|
// funktioniert.
|
|
const invoiceDir = path.resolve(value("INVOICE_DIR") || "./invoices");
|
|
const uploadDir = path.resolve(value("UPLOAD_DIR") || "./uploads");
|
|
must(
|
|
"INVOICE_DIR liegt ausserhalb von UPLOAD_DIR",
|
|
invoiceDir !== uploadDir && !invoiceDir.startsWith(uploadDir + path.sep),
|
|
`Rechnungen (${invoiceDir}) duerfen nicht unterhalb des oeffentlich ausgelieferten Upload-Verzeichnisses (${uploadDir}) liegen. Eigenes Verzeichnis waehlen, siehe .env.example.`
|
|
);
|
|
|
|
// --- Datenbank -------------------------------------------------------------
|
|
must("DATABASE_URL gesetzt", isSet("DATABASE_URL"), "Ohne Datenbank startet die Anwendung nicht.");
|
|
|
|
// --- Missbrauchsschutz -----------------------------------------------------
|
|
must(
|
|
"Cloudflare Turnstile konfiguriert",
|
|
isSet("TURNSTILE_SECRET_KEY") && isSet("NEXT_PUBLIC_TURNSTILE_SITE_KEY"),
|
|
"Blocker, weil die Pruefung bei fehlendem Schluessel nicht etwa fehlschlaegt, sondern STILL UEBERSPRUNGEN wird (src/lib/turnstile.ts) — alle oeffentlichen Formulare stuenden dann ungeschuetzt offen, ohne dass es auffiele: Login und Zwei-Faktor-Eingabe (Ausprobieren von Zugangsdaten), Passwort-Reset (Mailversand an fremde Adressen), Registrierung, Checkout (Bestandsblockade, Kartentests), Newsletter, Firmenanfrage und Verfuegbarkeits-Benachrichtigung. Beide Werte gehoeren zusammen: der Site-Key ist oeffentlich, der Secret-Key gehoert ins Secret."
|
|
);
|
|
|
|
// --- Inhalte, die rechtlich verpflichtend sind -----------------------------
|
|
// Beide Markierungen pruefen: die deutsche Fassung nutzt "[PLATZHALTER",
|
|
// die englische "[PLACEHOLDER". Eine Pruefung auf nur eine von beiden liesse
|
|
// die jeweils andere Sprachfassung mit Luecken online gehen.
|
|
/**
|
|
* Erste aussagekraeftige Zeile einer Fehlermeldung. Prisma-Verbindungsfehler
|
|
* beginnen mit Leerzeilen — ein blosses split("\n")[0] ergaebe eine leere
|
|
* Klammer statt der Ursache.
|
|
*/
|
|
function firstMeaningfulLine(error: unknown): string {
|
|
if (!(error instanceof Error)) return "unbekannter Fehler";
|
|
const line = error.message.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
return line ?? error.name;
|
|
}
|
|
|
|
const PLACEHOLDER_MARKERS = ["[PLATZHALTER", "[PLACEHOLDER"];
|
|
const hasPlaceholder = (text: string): boolean =>
|
|
PLACEHOLDER_MARKERS.some((marker) => text.includes(marker));
|
|
|
|
const LEGAL_KEYS = [
|
|
{ key: "legal.impressum", label: "Impressum" },
|
|
{ key: "legal.datenschutz", label: "Datenschutz" },
|
|
{ key: "legal.agb", label: "AGB" },
|
|
{ key: "legal.widerruf", label: "Widerrufsbelehrung" },
|
|
] as const;
|
|
|
|
// Kein Top-Level-await: das Projekt hat kein "type": "module" in der
|
|
// package.json, tsx uebersetzt dieses Skript daher nach CommonJS.
|
|
async function checkDatabaseContent(): Promise<void> {
|
|
try {
|
|
// Verbindung zuerst pruefen, damit ein DB-Ausfall unten nicht faelschlich
|
|
// als "Inhalt in Ordnung" durchgeht.
|
|
await prisma.$queryRaw`SELECT 1`;
|
|
|
|
const withPlaceholder: string[] = [];
|
|
for (const { key, label } of LEGAL_KEYS) {
|
|
for (const locale of locales) {
|
|
if (hasPlaceholder(await getContentValue(key, locale))) {
|
|
withPlaceholder.push(`${label} (${locale})`);
|
|
}
|
|
}
|
|
}
|
|
must(
|
|
"Rechtstexte enthalten keine Platzhalter-Markierungen mehr",
|
|
withPlaceholder.length === 0,
|
|
`Noch offen: ${withPlaceholder.join(", ")}. Im Admin-Bereich unter "Inhalte" pflegbar — kein Deploy noetig. Ein unvollstaendiges Impressum nach § 5 TMG ist in Deutschland abmahnfaehig: Name, Rechtsform und Umsatzsteuer-ID bzw. der Hinweis auf § 19 UStG muessen eingetragen sein.`
|
|
);
|
|
|
|
should(
|
|
"Telefonnummer ist keine Platzhalternummer",
|
|
!(await getContentValue("company.phone", "de")).includes("000 000000"),
|
|
'Sichtbar im Footer, auf der Kontaktseite und im Impressum. Im Admin-Bereich unter "Inhalte" pflegbar.'
|
|
);
|
|
} catch (error: unknown) {
|
|
// Bewusst KEIN Blocker und KEIN stilles Bestehen: ohne Datenbank laesst
|
|
// sich der tatsaechlich ausgelieferte Text nicht feststellen.
|
|
warnings.push(
|
|
`Rechtstexte und Telefonnummer konnten NICHT geprueft werden\n Die Datenbank ist von hier aus nicht erreichbar (${firstMeaningfulLine(
|
|
error
|
|
)}). Diese Inhalte liegen in der Datenbank, nicht im Quelltext — die Pruefung mit Zugriff auf die Produktionsdatenbank wiederholen oder /impressum im Browser aufrufen.`
|
|
);
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prueft, ob der Stripe-Webhook-Endpunkt die vier Ereignisse abonniert hat,
|
|
* auf die der Shop angewiesen ist.
|
|
*
|
|
* Das ist keine Formalie: fehlt checkout.session.expired, bleibt der beim
|
|
* Checkout reservierte Lagerbestand jedes abgebrochenen Kaufs dauerhaft
|
|
* blockiert. Fehlt checkout.session.completed, wird ueberhaupt keine Zahlung
|
|
* bestaetigt. Beides faellt im Betrieb erst auf, wenn Ware unverkaeuflich im
|
|
* System steht oder Geld eingezogen wurde, ohne dass eine Rechnung entstand —
|
|
* beides zu spaet.
|
|
*/
|
|
const REQUIRED_WEBHOOK_EVENTS = [
|
|
"checkout.session.completed",
|
|
"checkout.session.async_payment_succeeded",
|
|
"checkout.session.async_payment_failed",
|
|
"checkout.session.expired",
|
|
// Geld, das den Shop wieder verlaesst, ohne dass hier jemand etwas
|
|
// angeklickt hat: eine Erstattung von Hand im Stripe-Dashboard und eine
|
|
// Rueckbuchung durch die Bank. Ohne diese beiden Ereignisse steht die
|
|
// Zahlung im Shop weiter als erfolgreich da, und eine fristgebundene
|
|
// Rueckbuchung faellt nur auf, wenn jemand ins Dashboard sieht.
|
|
"charge.refunded",
|
|
"charge.dispute.created",
|
|
] as const;
|
|
|
|
async function checkStripeWebhookEvents(): Promise<void> {
|
|
if (!isSet("STRIPE_SECRET_KEY")) return; // fehlt bereits als Blocker oben
|
|
|
|
try {
|
|
const stripe = getStripeClient();
|
|
const endpoints = await stripe.webhookEndpoints.list({ limit: 100 });
|
|
const active = endpoints.data.filter((endpoint) => endpoint.status === "enabled");
|
|
|
|
// "*" abonniert alle Ereignisse — dann ist alles Noetige dabei.
|
|
const covered = new Set<string>();
|
|
for (const endpoint of active) {
|
|
for (const event of endpoint.enabled_events) covered.add(event);
|
|
}
|
|
|
|
const missing = covered.has("*")
|
|
? []
|
|
: REQUIRED_WEBHOOK_EVENTS.filter((event) => !covered.has(event));
|
|
|
|
if (active.length === 0) {
|
|
warnings.push(
|
|
"Kein aktiver Stripe-Webhook-Endpunkt gefunden\n Ohne ihn wird keine Zahlung bestaetigt und kein abgebrochener Checkout aufgeraeumt. Unter https://dashboard.stripe.com/webhooks einen Endpunkt auf /api/checkout/webhook anlegen. Hinweis: mit einem eingeschraenkten Schluessel (rk_...) kann diese Pruefung die Liste moeglicherweise nicht sehen, obwohl ein Endpunkt existiert."
|
|
);
|
|
return;
|
|
}
|
|
|
|
must(
|
|
`Stripe-Webhook abonniert alle ${REQUIRED_WEBHOOK_EVENTS.length} benoetigten Ereignisse`,
|
|
missing.length === 0,
|
|
`Nicht abonniert: ${missing.join(", ")}. Besonders checkout.session.expired: ohne dieses Ereignis bleibt der Lagerbestand jedes abgebrochenen Checkouts blockiert. Unter https://dashboard.stripe.com/webhooks am Endpunkt nachtragen.`
|
|
);
|
|
} catch (error: unknown) {
|
|
// Weder Blocker noch stilles Bestehen: der Schluessel darf die Liste
|
|
// vielleicht gar nicht lesen (eingeschraenkter Schluessel), und daraus
|
|
// laesst sich nichts ueber die tatsaechliche Konfiguration ableiten.
|
|
warnings.push(
|
|
`Stripe-Webhook-Ereignisse konnten NICHT geprueft werden\n ${firstMeaningfulLine(
|
|
error
|
|
)}. Im Dashboard unter https://dashboard.stripe.com/webhooks von Hand nachsehen, ob ${REQUIRED_WEBHOOK_EVENTS.join(
|
|
", "
|
|
)} abonniert sind.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function report(): void {
|
|
// --- Ausgabe ---------------------------------------------------------------
|
|
const line = "─".repeat(72);
|
|
console.log(`\n${line}\nGo-Live-Pruefung Wabenhain\n${line}`);
|
|
// Ohne diesen Hinweis wird der Bericht regelmaessig falsch gelesen: auf einem
|
|
// Entwicklungsrechner meldet er Blocker fuer Werte, die in der ConfigMap des
|
|
// Clusters laengst richtig stehen (NEXT_PUBLIC_SITE_URL, SMTP_FROM,
|
|
// VAT_RATE_PERCENT). Wer das ein paarmal erlebt, sieht ueber den Bericht
|
|
// hinweg — und damit auch ueber den einen Blocker, der echt ist.
|
|
console.log(
|
|
`
|
|
Geprueft wird die Umgebung, in der dieses Skript LAEUFT. Fuer eine Aussage
|
|
ueber den Live-Betrieb im Pod ausfuehren, nicht auf dem eigenen Rechner —
|
|
siehe README, Abschnitt "Vor dem Live-Gang".`
|
|
);
|
|
console.log(`\n✔ Bestanden: ${passed.length}`);
|
|
for (const item of passed) console.log(` ✔ ${item}`);
|
|
|
|
if (warnings.length > 0) {
|
|
console.log(`\n⚠ Warnungen: ${warnings.length}`);
|
|
for (const item of warnings) console.log(` ⚠ ${item}`);
|
|
}
|
|
|
|
if (blockers.length > 0) {
|
|
console.log(`\n✖ Blocker: ${blockers.length}`);
|
|
for (const item of blockers) console.log(` ✖ ${item}`);
|
|
console.log(`\n${line}`);
|
|
console.log("Nicht bereit fuer den Live-Betrieb. Die Blocker oben zuerst beheben.\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n${line}`);
|
|
console.log(
|
|
warnings.length > 0
|
|
? "Keine Blocker. Warnungen oben pruefen, dann ist der Live-Betrieb moeglich.\n"
|
|
: "Alle Pruefungen bestanden — bereit fuer den Live-Betrieb.\n"
|
|
);
|
|
}
|
|
|
|
void Promise.all([checkDatabaseContent(), checkStripeWebhookEvents()]).then(report);
|