/* ===================================================================== DOGFATHER UNIVERSE — Interner Bereich (Postfach, Team-Verwaltung, DogiCrew-Supporter-Abo). Node.js/Express-Server als 1:1-Ersatz für den Cloudflare Worker "dogfather-universe-postfach" (siehe ../cloudflare-worker/src/worker.js für das Original). Architektur (unverändert zum Original): db.js SQLite-Datenbank (better-sqlite3), Migrationen lib/crypto.js Hashing, AES-GCM (Code-Verschlüsselung), IDs lib/auth.js Login-Prüfung, Sessions, Rate-Limiting (Team) lib/supporter-auth.js Login/Sessions für den öffentlichen Bereich lib/permissions.js Vollständiger Permission-Katalog lib/audit.js Aktivitätsprotokoll schreiben lib/http.js JSON-Response-Helfer lib/live-status.js TikTok-Live-Erkennung (ersetzt Cloudflare-Cron) routes/*.js Endpoint-Handler pro Bereich Unterschiede zum Original (rein technisch, gleiches Verhalten): - D1 (async) → better-sqlite3 (synchron) - KV BEWERBUNGEN wurde nie für mehr als den Live-Status-Schalter genutzt, der jetzt komplett in app_settings (SQLite) lebt. - ctx.waitUntil() → einfaches "nicht abwarten" (Node-Prozess läuft durchgehend weiter, kein Cold-Start-Risiko wie bei Workers). - scheduled()-Cron → node-cron, gleicher Rhythmus ("* * * * *"). ===================================================================== */ import "dotenv/config"; import express from "express"; import cors from "cors"; import cron from "node-cron"; import { initDb } from "./db.js"; import { json } from "./lib/http.js"; import { requireSession } from "./lib/auth.js"; import { requireSupporterSession } from "./lib/supporter-auth.js"; import { handleLiveStatusGet, handleLiveStatusSet, refreshLiveFromTikTok } from "./lib/live-status.js"; import { login, logout, setOwnerCode } from "./routes/auth.js"; import { listRoles, createRole, updateRole, deleteRole } from "./routes/roles.js"; import { listUsers, prepareUser, finalizeUser, setUserCode, revealUserCode, lockUser, unlockUser, deleteUser, setUserRole, setUserPermissions, endUserSessions, updateUserMeta, } from "./routes/users.js"; import { submitApplication, listApplications, changeApplicationStatus, takeOverApplication, assignApplication, archiveApplication, restoreApplication, deleteApplication, markAnswered, listNotes, addNote, deleteNote, listDrafts, saveDraft, } from "./routes/applications.js"; import { listAuditLog } from "./routes/audit.js"; import { registerSupporter, verifySupporterEmail, requestSupporterLogin, verifySupporterLogin, logoutSupporter, googleLoginSupporter, getPublicConfig, getSupporterMe, updateSupporterProfile, requestSupporterEmailChange, confirmSupporterEmailChange, redeemPremium, deleteSupporterAccount, } from "./routes/supporter.js"; import { startPaypalSubscription, confirmPaypalSubscription, cancelPaypalSubscription, handlePaypalWebhook, } from "./routes/supporter-paypal.js"; import { listSupporters, getSupporterDetail, updatePremium, listCollections, upsertCollection, getAdminOverview, } from "./routes/supporter-admin.js"; initDb(); const PORT = Number(process.env.PORT || 4200); const app = express(); app.set("trust proxy", 1); app.use(cors()); app.use( express.json({ limit: "2mb", // Roh-Body wird zusätzlich als String aufbewahrt — die PayPal-Webhook- // Signaturprüfung braucht den unveränderten Original-Text (siehe // routes/supporter-paypal.js), nicht das nachträglich neu serialisierte // req.body. verify: (req, res, buf) => { req.rawBody = buf.toString("utf8"); }, }) ); /* ---------- Middlewares: interne Team-Session bzw. Supporter-Session ---------- */ async function requireTeamSession(req, res, next) { const session = await requireSession(req); if (!session) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut anmelden." }, 401); req.session = session; next(); } async function requireSupporterAuth(req, res, next) { const supporter = await requireSupporterSession(req); if (!supporter) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut einloggen." }, 401); req.supporter = supporter; next(); } /** Reicht die injizierte session/supporter als drittes Argument an den Handler weiter (wie im Cloudflare-Original). */ const withSession = (handler) => (req, res) => handler(req, res, req.session); const withSupporter = (handler) => (req, res) => handler(req, res, req.supporter); /* ---------- Öffentliche Routen (keine Session) ---------- */ app.get("/health", (req, res) => json(res, { ok: true, service: "dogfather-universe-interner-bereich" })); app.post("/submit", submitApplication); app.get("/live-status", handleLiveStatusGet); app.post("/auth/login", login); app.post("/supporter/register", registerSupporter); app.post("/supporter/verify-email", verifySupporterEmail); app.post("/supporter/login-request", requestSupporterLogin); app.post("/supporter/verify-login", verifySupporterLogin); app.post("/supporter/google-login", googleLoginSupporter); app.get("/supporter/public-config", getPublicConfig); // PayPal ruft diese Adresse direkt auf — eigene Signaturprüfung statt Session (siehe lib/paypal.js). app.post("/supporter/paypal-webhook", handlePaypalWebhook); /* ---------- Team-Bereich (interne Session, requireTeamSession) ---------- */ app.post("/auth/logout", requireTeamSession, logout); app.post("/admin/owner/set-code", requireTeamSession, withSession(setOwnerCode)); app.post("/live-status/set", requireTeamSession, withSession(handleLiveStatusSet)); app.post("/admin/applications/list", requireTeamSession, withSession(listApplications)); app.post("/admin/applications/status", requireTeamSession, withSession(changeApplicationStatus)); app.post("/admin/applications/take-over", requireTeamSession, withSession(takeOverApplication)); app.post("/admin/applications/assign", requireTeamSession, withSession(assignApplication)); app.post("/admin/applications/archive", requireTeamSession, withSession(archiveApplication)); app.post("/admin/applications/restore", requireTeamSession, withSession(restoreApplication)); app.post("/admin/applications/delete", requireTeamSession, withSession(deleteApplication)); app.post("/admin/applications/mark-answered", requireTeamSession, withSession(markAnswered)); app.post("/admin/notes/list", requireTeamSession, withSession(listNotes)); app.post("/admin/notes/add", requireTeamSession, withSession(addNote)); app.post("/admin/notes/delete", requireTeamSession, withSession(deleteNote)); app.post("/admin/drafts/list", requireTeamSession, withSession(listDrafts)); app.post("/admin/drafts/save", requireTeamSession, withSession(saveDraft)); app.post("/admin/roles/list", requireTeamSession, withSession(listRoles)); app.post("/admin/roles/create", requireTeamSession, withSession(createRole)); app.post("/admin/roles/update", requireTeamSession, withSession(updateRole)); app.post("/admin/roles/delete", requireTeamSession, withSession(deleteRole)); app.post("/admin/users/list", requireTeamSession, withSession(listUsers)); app.post("/admin/users/prepare", requireTeamSession, withSession(prepareUser)); app.post("/admin/users/finalize", requireTeamSession, withSession(finalizeUser)); app.post("/admin/users/set-code", requireTeamSession, withSession(setUserCode)); app.post("/admin/users/reveal-code", requireTeamSession, withSession(revealUserCode)); app.post("/admin/users/lock", requireTeamSession, withSession(lockUser)); app.post("/admin/users/unlock", requireTeamSession, withSession(unlockUser)); app.post("/admin/users/delete", requireTeamSession, withSession(deleteUser)); app.post("/admin/users/set-role", requireTeamSession, withSession(setUserRole)); app.post("/admin/users/set-permissions", requireTeamSession, withSession(setUserPermissions)); app.post("/admin/users/end-sessions", requireTeamSession, withSession(endUserSessions)); app.post("/admin/users/update-meta", requireTeamSession, withSession(updateUserMeta)); app.post("/admin/audit/list", requireTeamSession, withSession(listAuditLog)); // Supporter-Abo — Administrationsbereich (interne Team-Session + eigene // SUPPORTER_*-Rechte, siehe lib/permissions.js). app.post("/admin/overview", requireTeamSession, withSession(getAdminOverview)); app.post("/admin/supporter/list", requireTeamSession, withSession(listSupporters)); app.post("/admin/supporter/detail", requireTeamSession, withSession(getSupporterDetail)); app.post("/admin/supporter/premium/update", requireTeamSession, withSession(updatePremium)); app.post("/admin/supporter/collections/list", requireTeamSession, withSession(listCollections)); app.post("/admin/supporter/collections/upsert", requireTeamSession, withSession(upsertCollection)); /* ---------- Öffentlicher Supporter-Bereich (eigene Session, requireSupporterAuth) ---------- */ app.post("/supporter/logout", requireSupporterAuth, logoutSupporter); app.post("/supporter/me", requireSupporterAuth, withSupporter(getSupporterMe)); app.post("/supporter/update-profile", requireSupporterAuth, withSupporter(updateSupporterProfile)); app.post("/supporter/request-email-change", requireSupporterAuth, withSupporter(requestSupporterEmailChange)); app.post("/supporter/confirm-email-change", requireSupporterAuth, withSupporter(confirmSupporterEmailChange)); app.post("/supporter/redeem-premium", requireSupporterAuth, withSupporter(redeemPremium)); app.post("/supporter/delete-account", requireSupporterAuth, withSupporter(deleteSupporterAccount)); app.post("/supporter/paypal/start", requireSupporterAuth, withSupporter(startPaypalSubscription)); app.post("/supporter/paypal/confirm", requireSupporterAuth, withSupporter(confirmPaypalSubscription)); app.post("/supporter/paypal/cancel", requireSupporterAuth, withSupporter(cancelPaypalSubscription)); app.use((req, res) => json(res, { ok: false, error: "Not found." }, 404)); /* ---------- Cron: läuft jede Minute, hält den Live-Punkt aktuell (ersetzt Cloudflares scheduled()) ---------- */ cron.schedule("* * * * *", () => { refreshLiveFromTikTok().catch((err) => console.error("[live-status] Fehler beim Aktualisieren:", err?.message || err)); }); app.listen(PORT, "127.0.0.1", () => { console.log(`DOGFATHER UNIVERSE — Interner Bereich läuft auf http://127.0.0.1:${PORT}`); });