/* ===================================================================== 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); }