Alle ausstehenden Aenderungen (Bilder, Texte) fuer den Server-Umzug uebernommen
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/* =====================================================================
|
||||
lib/supporter-auth.js — Registrierung, E-Mail-Bestätigung, Magic-Link-
|
||||
Login und Sessions für den ÖFFENTLICHEN Supporter-Bereich.
|
||||
|
||||
Bewusst komplett getrennt von auth.js (das ist für das interne
|
||||
Team-Zugangssystem mit Rollen/Rechten). Supporter sind Fans/Kund:innen
|
||||
ohne jegliche Berechtigungen im internen Bereich.
|
||||
|
||||
Login-Prinzip (Abschnitt 5/116 im Auftrag: "bevorzugt … sicherer
|
||||
E-Mail-Code oder Magic Link"): kein Passwort nötig. Ein 6-stelliger Code
|
||||
wird an die hinterlegte E-Mail-Adresse geschickt, 15 Minuten gültig,
|
||||
einmal verwendbar, nur der Hash liegt in der Datenbank.
|
||||
===================================================================== */
|
||||
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; // 30 Tage — Supporter sollen nicht ständig neu einloggen müssen
|
||||
const INACTIVITY_MS = 14 * 24 * 60 * 60 * 1000; // 14 Tage ohne Aktivität → Session verfällt
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/** Erzeugt+speichert einen neuen 6-stelligen Code für E-Mail-Bestätigung oder Login. */
|
||||
export async function issueAuthCode(env, supporterId, purpose) {
|
||||
const code = generateNumericCode();
|
||||
const codeHash = await sha256Hex(code);
|
||||
await env.DB.prepare(
|
||||
`INSERT INTO supporter_auth_codes (id, supporter_id, purpose, code_hash, expires_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.bind(generateId(), supporterId, purpose, codeHash, new Date(Date.now() + CODE_TTL_MS).toISOString(), nowIso())
|
||||
.run();
|
||||
return code;
|
||||
}
|
||||
|
||||
/** Prüft einen eingegebenen Code; markiert ihn bei Erfolg als verbraucht (einmal gültig). */
|
||||
export async function verifyAuthCode(env, supporterId, purpose, code) {
|
||||
const codeHash = await sha256Hex(String(code || "").trim());
|
||||
const row = await env.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`
|
||||
)
|
||||
.bind(supporterId, purpose, codeHash)
|
||||
.first();
|
||||
if (!row) return false;
|
||||
if (row.used_at) return false;
|
||||
if (new Date(row.expires_at).getTime() < Date.now()) return false;
|
||||
await env.DB.prepare(`UPDATE supporter_auth_codes SET used_at = ? WHERE id = ?`).bind(nowIso(), row.id).run();
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function createSupporterSession(env, supporterId) {
|
||||
const token = generateSessionToken();
|
||||
const tokenHash = await sha256Hex(token);
|
||||
const now = new Date();
|
||||
await env.DB.prepare(
|
||||
`INSERT INTO supporter_sessions (token_hash, supporter_id, created_at, last_active_at, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
)
|
||||
.bind(tokenHash, supporterId, now.toISOString(), now.toISOString(), new Date(now.getTime() + SESSION_MAX_AGE_MS).toISOString())
|
||||
.run();
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function resolveSupporterSession(env, token) {
|
||||
if (!token) return null;
|
||||
const tokenHash = await sha256Hex(token);
|
||||
const session = await env.DB.prepare(`SELECT * FROM supporter_sessions WHERE token_hash = ?`).bind(tokenHash).first();
|
||||
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) {
|
||||
await env.DB.prepare(`DELETE FROM supporter_sessions WHERE token_hash = ?`).bind(tokenHash).run();
|
||||
return null;
|
||||
}
|
||||
await env.DB.prepare(`UPDATE supporter_sessions SET last_active_at = ? WHERE token_hash = ?`).bind(nowIso(), tokenHash).run();
|
||||
|
||||
const supporter = await env.DB.prepare(
|
||||
`SELECT * FROM supporter_users WHERE id = ? AND status = 'active'`
|
||||
)
|
||||
.bind(session.supporter_id)
|
||||
.first();
|
||||
if (!supporter) return null;
|
||||
return supporter;
|
||||
}
|
||||
|
||||
export async function endSupporterSession(env, token) {
|
||||
if (!token) return;
|
||||
await env.DB.prepare(`DELETE FROM supporter_sessions WHERE token_hash = ?`).bind(await sha256Hex(token)).run();
|
||||
}
|
||||
|
||||
export async function requireSupporterSession(request, env) {
|
||||
const auth = request.headers.get("Authorization") || "";
|
||||
const token = auth.startsWith("Bearer ") ? auth.slice(7) : null;
|
||||
return resolveSupporterSession(env, token);
|
||||
}
|
||||
Reference in New Issue
Block a user