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.
87 lines
3.5 KiB
JavaScript
87 lines
3.5 KiB
JavaScript
/* =====================================================================
|
|
lib/supporter-auth.js — Registrierung, E-Mail-Code-Login, Sessions für
|
|
den öffentlichen Supporter-Bereich. 1:1 portiert, D1 → better-sqlite3.
|
|
===================================================================== */
|
|
import { db } from "../db.js";
|
|
import { sha256Hex, generateSessionToken, generateId, nowIso } from "./crypto.js";
|
|
|
|
const CODE_TTL_MS = 15 * 60 * 1000;
|
|
const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
const INACTIVITY_MS = 14 * 24 * 60 * 60 * 1000;
|
|
|
|
export function normalizeTikTokUsername(input) {
|
|
if (!input) return null;
|
|
return String(input).trim().replace(/^@+/, "").toLowerCase() || null;
|
|
}
|
|
|
|
export function normalizeEmail(input) {
|
|
return String(input || "").trim().toLowerCase();
|
|
}
|
|
|
|
function generateNumericCode() {
|
|
const n = crypto.getRandomValues(new Uint32Array(1))[0] % 1_000_000;
|
|
return String(n).padStart(6, "0");
|
|
}
|
|
|
|
export async function issueAuthCode(supporterId, purpose) {
|
|
const code = generateNumericCode();
|
|
const codeHash = await sha256Hex(code);
|
|
db.prepare(
|
|
`INSERT INTO supporter_auth_codes (id, supporter_id, purpose, code_hash, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?)`
|
|
).run(generateId(), supporterId, purpose, codeHash, new Date(Date.now() + CODE_TTL_MS).toISOString(), nowIso());
|
|
return code;
|
|
}
|
|
|
|
export async function verifyAuthCode(supporterId, purpose, code) {
|
|
const codeHash = await sha256Hex(String(code || "").trim());
|
|
const row = db
|
|
.prepare(
|
|
`SELECT id, expires_at, used_at FROM supporter_auth_codes
|
|
WHERE supporter_id = ? AND purpose = ? AND code_hash = ? ORDER BY created_at DESC LIMIT 1`
|
|
)
|
|
.get(supporterId, purpose, codeHash);
|
|
if (!row) return false;
|
|
if (row.used_at) return false;
|
|
if (new Date(row.expires_at).getTime() < Date.now()) return false;
|
|
db.prepare(`UPDATE supporter_auth_codes SET used_at = ? WHERE id = ?`).run(nowIso(), row.id);
|
|
return true;
|
|
}
|
|
|
|
export async function createSupporterSession(supporterId) {
|
|
const token = generateSessionToken();
|
|
const tokenHash = await sha256Hex(token);
|
|
const now = new Date();
|
|
db.prepare(
|
|
`INSERT INTO supporter_sessions (token_hash, supporter_id, created_at, last_active_at, expires_at) VALUES (?, ?, ?, ?, ?)`
|
|
).run(tokenHash, supporterId, now.toISOString(), now.toISOString(), new Date(now.getTime() + SESSION_MAX_AGE_MS).toISOString());
|
|
return token;
|
|
}
|
|
|
|
export async function resolveSupporterSession(token) {
|
|
if (!token) return null;
|
|
const tokenHash = await sha256Hex(token);
|
|
const session = db.prepare(`SELECT * FROM supporter_sessions WHERE token_hash = ?`).get(tokenHash);
|
|
if (!session) return null;
|
|
|
|
const now = Date.now();
|
|
if (new Date(session.expires_at).getTime() < now || now - new Date(session.last_active_at).getTime() > INACTIVITY_MS) {
|
|
db.prepare(`DELETE FROM supporter_sessions WHERE token_hash = ?`).run(tokenHash);
|
|
return null;
|
|
}
|
|
db.prepare(`UPDATE supporter_sessions SET last_active_at = ? WHERE token_hash = ?`).run(nowIso(), tokenHash);
|
|
|
|
const supporter = db.prepare(`SELECT * FROM supporter_users WHERE id = ? AND status = 'active'`).get(session.supporter_id);
|
|
return supporter || null;
|
|
}
|
|
|
|
export async function endSupporterSession(token) {
|
|
if (!token) return;
|
|
db.prepare(`DELETE FROM supporter_sessions WHERE token_hash = ?`).run(await sha256Hex(token));
|
|
}
|
|
|
|
export async function requireSupporterSession(req) {
|
|
const auth = req.headers["authorization"] || "";
|
|
const token = auth.startsWith("Bearer ") ? auth.slice(7) : null;
|
|
return resolveSupporterSession(token);
|
|
}
|