Postfach/Team-Verwaltung/Supporter-Abo als Node.js/Express-Server portiert

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.
This commit is contained in:
2026-08-05 18:57:19 +02:00
parent e331965bf1
commit e87aca3019
26 changed files with 2962 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
/* =====================================================================
lib/live-status.js — Automatische TikTok-Live-Erkennung für den grünen
Punkt auf der Startseite. 1:1 portiert aus dem Live-Status-Teil von
cloudflare-worker/src/worker.js, D1 → better-sqlite3.
Ein Cron-Auftrag (node-cron in index.js, ersetzt Cloudflares
`scheduled()`) fragt jede Minute bei TikTok nach, ob @dogfather0804
gerade sendet, und schreibt das Ergebnis in die Tabelle app_settings.
Zwei Schlüssel:
live_auto letzter ERFOLGREICH ermittelter Stand (wird bei einem
fehlgeschlagenen Abruf bewusst NICHT überschrieben,
damit ein TikTok-Aussetzer den Punkt nicht fälschlich
auf offline zieht)
live_override manueller Vorrang aus dem Postfach, mit Ablaufzeit.
Solange er gilt, schlägt er die Automatik.
===================================================================== */
import { db } from "../db.js";
import { json } from "./http.js";
import { logAction } from "./audit.js";
import { checkTikTokLive } from "./tiktok-live.js";
const DEFAULT_OVERRIDE_MINUTES = 180;
function readSetting(key) {
const row = db.prepare(`SELECT value FROM app_settings WHERE key = ?`).get(key);
if (!row || !row.value) return null;
try {
return JSON.parse(row.value);
} catch {
return null;
}
}
function writeSetting(key, value) {
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(key, JSON.stringify(value), new Date().toISOString());
}
/** Ermittelt den Stand, der nach außen gilt (Vorrang: manuell vor Automatik). */
function resolveLiveStatus() {
const auto = readSetting("live_auto") || {};
const override = readSetting("live_override");
const overrideActive = override && override.until && new Date(override.until).getTime() > Date.now();
if (overrideActive) {
return {
live: !!override.live,
source: "manuell",
updatedAt: override.setAt || null,
overrideUntil: override.until,
autoLive: auto.live ?? null,
autoCheckedAt: auto.checkedAt || null,
autoHealthy: auto.ok !== false,
};
}
return {
live: !!auto.live,
source: "automatisch",
updatedAt: auto.changedAt || auto.checkedAt || null,
title: auto.title || null,
autoLive: auto.live ?? null,
autoCheckedAt: auto.checkedAt || null,
autoHealthy: auto.ok !== false,
};
}
export async function handleLiveStatusGet(req, res) {
return json(res, { ok: true, ...resolveLiveStatus() });
}
/**
* Manueller Eingriff aus dem Postfach.
* { live: true|false, minutes?: number } → Vorrang setzen
* { auto: true } → Vorrang aufheben, Automatik übernimmt
*/
export async function handleLiveStatusSet(req, res, session) {
if (!session?.isOwner) return json(res, { ok: false, error: "Nur Dogi darf den Live-Status ändern." }, 403);
const body = req.body || {};
if (body.auto === true) {
db.prepare(`DELETE FROM app_settings WHERE key = 'live_override'`).run();
logAction("owner", "live.override_cleared", null, null);
return json(res, { ok: true, ...resolveLiveStatus() });
}
const minutes = Number.isFinite(body.minutes) ? Math.min(Math.max(body.minutes, 5), 1440) : DEFAULT_OVERRIDE_MINUTES;
const now = new Date();
writeSetting("live_override", {
live: !!body.live,
setAt: now.toISOString(),
until: new Date(now.getTime() + minutes * 60_000).toISOString(),
});
logAction("owner", "live.override_set", null, { live: !!body.live, minutes });
return json(res, { ok: true, ...resolveLiveStatus() });
}
/** Wird vom Cron-Auftrag aufgerufen (index.js) — fragt TikTok ab und schreibt das Ergebnis. */
export async function refreshLiveFromTikTok() {
const username = process.env.TIKTOK_USERNAME || "dogfather0804";
const result = await checkTikTokLive(username);
const now = new Date().toISOString();
if (!result.ok) {
// Fehlgeschlagen: letzten guten Stand behalten, nur den Fehler vermerken.
const previous = readSetting("live_auto") || {};
writeSetting("live_auto", { ...previous, ok: false, lastError: result.reason, checkedAt: now });
return { ok: false, reason: result.reason };
}
const previous = readSetting("live_auto") || {};
const changed = previous.live !== result.live;
writeSetting("live_auto", {
ok: true,
live: result.live,
title: result.title,
roomId: result.roomId,
rawStatus: result.rawStatus,
checkedAt: now,
changedAt: changed ? now : previous.changedAt || now,
lastError: null,
});
if (changed) {
logAction("system", result.live ? "live.started" : "live.ended", null, { title: result.title });
}
return { ok: true, live: result.live, changed };
}