/* ===================================================================== routes/auth.js — Login / Logout ===================================================================== */ import { verifyCode, createSession, endSession, isLockedOut, recordFailedAttempt, clearAttempts, clientKey } from "../lib/auth.js"; import { logAction } from "../lib/audit.js"; import { nowIso, sha256Hex } from "../lib/crypto.js"; import { json } from "../lib/http.js"; /** * Der Hauptadministrator ändert seinen eigenen Zugangscode. * Nur mit gültiger Owner-Session UND erneuter Eingabe des aktuellen Codes. * Gespeichert wird ausschließlich der SHA-256-Hash — nie der Klartext. */ export async function setOwnerCode(request, env, session) { if (!session || !session.isOwner) { return json({ ok: false, error: "Das darf nur der Hauptadministrator." }, 403); } const body = await request.json().catch(() => ({})); const currentCode = String(body.currentCode || ""); const newCode = String(body.newCode || ""); const check = await verifyCode(env, currentCode); if (!check.ok || !check.isOwner) { await logAction(env, "owner", "owner.code_change_failed", null, null); return json({ ok: false, error: "Der aktuelle Code stimmt nicht." }, 403); } if (newCode.length < 10) { return json({ ok: false, error: "Der neue Code muss mindestens 10 Zeichen haben." }, 400); } if (newCode === currentCode) { return json({ ok: false, error: "Der neue Code muss sich vom alten unterscheiden." }, 400); } const hash = await sha256Hex(newCode); await env.DB.prepare( `INSERT INTO app_settings (key, value, updated_at) VALUES ('owner_code_hash', ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at` ) .bind(hash, nowIso()) .run(); await logAction(env, "owner", "owner.code_changed", null, null); return json({ ok: true }); } export async function login(request, env) { const body = await request.json().catch(() => ({})); const code = String(body.code || ""); const key = `login:${clientKey(request)}`; if (await isLockedOut(env, key)) { return json({ ok: false, error: "Zu viele Fehlversuche. Bitte in ein paar Minuten erneut versuchen." }, 429); } const result = await verifyCode(env, code); if (!result.ok) { const { count } = await recordFailedAttempt(env, key); await logAction(env, "anonym", "login.failed", null, { attempt: count }); if (result.reason === "locked") return json({ ok: false, error: "Dieser Zugang ist gesperrt." }, 403); if (result.reason === "expired") return json({ ok: false, error: "Dieser Zugang ist abgelaufen." }, 403); return json({ ok: false, error: "Falscher Code." }, 401); } await clearAttempts(env, key); if (result.isOwner) { const token = await createSession(env, { isOwner: true }); await logAction(env, "owner", "login.success", null, null); return json({ ok: true, token, isOwner: true, name: "Dogi", roleName: "Hauptadministrator" }); } const token = await createSession(env, { isOwner: false, userId: result.user.id }); await env.DB.prepare(`UPDATE users SET last_login_at = ? WHERE id = ?`).bind(nowIso(), result.user.id).run(); const roleRow = result.user.role_id ? await env.DB.prepare(`SELECT name, permissions FROM roles WHERE id = ?`).bind(result.user.role_id).first() : null; const overridesRows = await env.DB.prepare( `SELECT permission, allowed FROM user_permission_overrides WHERE user_id = ?` ) .bind(result.user.id) .all(); const overrides = {}; for (const r of overridesRows.results || []) overrides[r.permission] = !!r.allowed; const permissions = roleRow ? JSON.parse(roleRow.permissions || "[]") : []; await logAction(env, result.user.username, "login.success", null, null); return json({ ok: true, token, isOwner: false, name: result.user.name, username: result.user.username, roleName: roleRow?.name || "Ohne Rolle", permissions, overrides, }); } export async function logout(request, env) { const auth = request.headers.get("Authorization") || ""; const token = auth.startsWith("Bearer ") ? auth.slice(7) : null; await endSession(env, token); return json({ ok: true }); }