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:
@@ -0,0 +1,60 @@
|
||||
/* =====================================================================
|
||||
lib/ticketanizer.js — Bewerbungen als Ticket bei Ticketanizer eröffnen.
|
||||
1:1 portiert aus cloudflare-worker/src/lib/ticketanizer.js.
|
||||
===================================================================== */
|
||||
|
||||
const TICKETANIZER_BASIS_URL = "https://api.ticketanizer.com";
|
||||
|
||||
const PANEL_IDS = { modi: 4, kooperation: 5, scout: 6, creator: 7 };
|
||||
|
||||
const FIELD_LABELS = {
|
||||
name: "Name", email: "E-Mail", kontakt: "Kontakt (Discord/TikTok)", erfahrung: "Erfahrung",
|
||||
verfuegbarkeit: "Verfügbarkeit", warum: "Warum möchtest du dabei sein?", platform: "Plattform",
|
||||
content: "Content-Art", firma: "Firma / Projekt", idee: "Idee / Anliegen",
|
||||
};
|
||||
|
||||
function ersteNachrichtAufbauen(type, data) {
|
||||
const zeilen = Object.entries(data || {})
|
||||
.filter(([, v]) => String(v ?? "").trim() !== "")
|
||||
.map(([key, value]) => `**${FIELD_LABELS[key] || key}:** ${String(value).trim()}`);
|
||||
if (!zeilen.length) zeilen.push("_Keine weiteren Angaben übermittelt._");
|
||||
const text = zeilen.join("\n");
|
||||
return text.length > 3500 ? text.slice(0, 3500) + "…" : text;
|
||||
}
|
||||
|
||||
const warten = (ms) => new Promise((fertig) => setTimeout(fertig, ms));
|
||||
const MAX_VERSUCHE = 3;
|
||||
const STANDARD_WARTEZEITEN_MS = [2000, 5000];
|
||||
|
||||
async function ticketAnfrageSenden(apiKey, panelId, visitorName, firstMessage) {
|
||||
for (let versuch = 1; versuch <= MAX_VERSUCHE; versuch++) {
|
||||
const antwort = await fetch(`${TICKETANIZER_BASIS_URL}/v1/tickets`, {
|
||||
method: "POST",
|
||||
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ panel_id: panelId, visitor_name: visitorName, first_message: firstMessage }),
|
||||
});
|
||||
if (antwort.status !== 429 || versuch === MAX_VERSUCHE) return antwort;
|
||||
const retryAfterHeader = Number(antwort.headers.get("Retry-After"));
|
||||
const wartezeit = Number.isFinite(retryAfterHeader) && retryAfterHeader > 0 ? retryAfterHeader * 1000 : STANDARD_WARTEZEITEN_MS[versuch - 1] || 5000;
|
||||
await warten(wartezeit);
|
||||
}
|
||||
}
|
||||
|
||||
export async function bewerbungAlsTicketAnlegen(type, data) {
|
||||
const apiKey = process.env.TICKETANIZER_API_KEY;
|
||||
const panelId = PANEL_IDS[type];
|
||||
if (!apiKey || !panelId) return;
|
||||
|
||||
const visitorName = String(data?.name || data?.firma || "Unbekannt").trim().slice(0, 80) || "Unbekannt";
|
||||
const firstMessage = ersteNachrichtAufbauen(type, data);
|
||||
|
||||
try {
|
||||
const antwort = await ticketAnfrageSenden(apiKey, panelId, visitorName, firstMessage);
|
||||
if (!antwort.ok) {
|
||||
const antwortText = await antwort.text().catch(() => "");
|
||||
console.error(`[ticketanizer] Fehler beim Ticket-Anlegen (${type}): HTTP ${antwort.status} — ${antwortText}`);
|
||||
}
|
||||
} catch (fehler) {
|
||||
console.error(`[ticketanizer] Ausnahme beim Ticket-Anlegen (${type}):`, fehler && fehler.message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user