Vollständiger 1:1-Port des cloudflare-worker/ (dogfather-universe-postfach) auf server-internal/: Bewerbungs-Postfach mit Rollen/Rechten, Team- Zugangsverwaltung (verschlüsselte Owner-only-Codes), DogiCrew-Supporter-Abo (PayPal Subscriptions, Google-Login, Resend-E-Mail, 30-Monats-Prämienzyklus) und automatische TikTok-Live-Erkennung. D1 -> better-sqlite3, Cloudflare- Cron -> node-cron. Läuft ohne gesetzte Secrets weiter (PayPal/Google/Resend/ Ticketanizer/Discord bleiben "nicht konfiguriert" statt kaputt), bis die echten Zugangsdaten eingetragen werden.
194 lines
10 KiB
JavaScript
194 lines
10 KiB
JavaScript
/* =====================================================================
|
|
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}`);
|
|
});
|