Files
karim 6a2393301d feat(admin): Betreiber-Panel (/api/admin) mit is_admin-Flag
- 0003_admin.sql: accounts.is_admin
- auth.js: ensureAdminFlag (Konto = ADMIN_EMAIL wird auto-promoted),
  is_admin im JWT, requireAdmin-Middleware (prüft DB autoritativ)
- routes/admin.js: GET /stats (Kunden/Abos/Instanzen/MRR), GET /accounts,
  GET /accounts/:id/instances, POST /instances/:id/{suspend,reactivate}
- register/login + /account/me liefern is_admin
- ADMIN_EMAIL in .env.example

E2E: Admin-Promotion, Kunde→403, Stats (2 Kunden/MRR 49), Kundenliste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 00:04:19 +02:00

56 lines
2.5 KiB
JavaScript

// RAPPORT-HOST Backend-Entry.
import express from "express";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { env } from "./env.js";
import { authRouter } from "./routes/auth.js";
import { billingRouter } from "./routes/billing.js";
import { accountRouter } from "./routes/account.js";
import { adminRouter } from "./routes/admin.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
// WICHTIG: Der Stripe-Webhook braucht den ROHEN Body für die Signaturprüfung.
// express.raw greift nur für diese Route und setzt req._body, sodass das danach
// registrierte express.json() den Webhook-Body NICHT erneut parst.
app.use("/api/billing/webhook", express.raw({ type: "application/json" }));
app.use(express.json());
app.get("/api/health", (_req, res) => res.json({ ok: true, service: "rapport-host" }));
app.use("/api/auth", authRouter);
app.use("/api/billing", billingRouter);
app.use("/api/account", accountRouter);
app.use("/api/admin", adminRouter);
// ── Statische Auslieferung der RAPPORT-WEBSITE (Hugo-Build) ──────────────────
// Die komplette Frontend-Oberfläche (Marketing + Hosting + Login/Konto als
// Vanilla-JS-Seiten) kommt aus dem AGPL-Repo RAPPORT-WEBSITE. Dieses Backend
// liefert dessen gebautes public/ aus und stellt darunter /api bereit.
// Saubere Trennung: die Website enthält keinen Backend-Code, nur fetch('/api').
// Pfad per WEBSITE_PUBLIC_DIR überschreibbar (Default: Schwester-Repo).
const websitePublic = env.websitePublicDir;
app.use(express.static(websitePublic));
app.get("*", (req, res, next) => {
if (req.path.startsWith("/api/")) return next();
// Hugo erzeugt pro Seite eine eigene index.html → Clean-URLs funktionieren
// direkt über express.static. Unbekannte Pfade → 404-Seite.
res.status(404).sendFile(path.join(websitePublic, "404.html"), (err) => err && next());
});
// Express-Fehlerhandler: fängt synchron geworfene Fehler aus Routen → 500
// statt stiller Empty-Reply.
app.use((err, _req, res, _next) => {
console.error("Unbehandelter Routen-Fehler:", err);
if (!res.headersSent) res.status(500).json({ error: "Interner Fehler." });
});
// Letzte Verteidigungslinie: ein einzelner async-Fehler darf den Prozess nicht
// killen (im Dev-Test sonst "connection refused" für alle Folge-Requests).
process.on("unhandledRejection", (reason) => console.error("unhandledRejection:", reason));
app.listen(env.port, () => {
console.log(`RAPPORT-HOST API läuft auf :${env.port} (Base: ${env.publicBaseUrl})`);
});