/* ===================================================================== routes/users.js — Zugänge und Teammitglieder. 1:1 portiert aus cloudflare-worker/src/routes/users.js. Zentrale Regel aus dem Anforderungsdokument, überall durchgesetzt: AUSSCHLIESSLICH der Owner darf Codes sehen, erstellen, ändern, kopieren oder zurücksetzen. Keine Rolle und keine individuelle Berechtigung kann das jemals freischalten — die entsprechenden Funktionen prüfen `session.isOwner` direkt, nie eine Permission. ===================================================================== */ import { db } from "../db.js"; import { generateId, generateFriendlyCode, sha256Hex, encryptCode, decryptCode, nowIso } from "../lib/crypto.js"; import { hasPermission, isValidPermissionKey } from "../lib/permissions.js"; import { endAllSessionsForUser, verifyCode } from "../lib/auth.js"; import { logAction } from "../lib/audit.js"; import { json } from "../lib/http.js"; function can(session, key) { if (!session) return false; if (session.isOwner) return true; return hasPermission(session.permissions, session.overrides, key); } function loadUser(id) { return db .prepare(`SELECT u.*, r.name as role_name FROM users u LEFT JOIN roles r ON r.id = u.role_id WHERE u.id = ?`) .get(id); } function publicUser(u) { return { id: u.id, name: u.name, username: u.username, email: u.email, roleId: u.role_id, roleName: u.role_name, status: u.status, note: u.note, expiresAt: u.expires_at, createdAt: u.created_at, createdBy: u.created_by, lastLoginAt: u.last_login_at, // Codes erscheinen hier bewusst NIE — auch nicht verschlüsselt/gehasht. }; } export async function listUsers(req, res, session) { if (!can(session, "TEAM_VIEW") && !session?.isOwner) { return json(res, { ok: false, error: "Keine Berechtigung." }, 403); } const rows = db .prepare(`SELECT u.*, r.name as role_name FROM users u LEFT JOIN roles r ON r.id = u.role_id ORDER BY u.created_at DESC`) .all(); return json(res, { ok: true, users: rows.map(publicUser) }); } /** Nicht-Owner mit TEAM_PREPARE_USER können einen Entwurf anlegen — noch ohne Code, inaktiv. */ export async function prepareUser(req, res, session) { if (!can(session, "TEAM_PREPARE_USER")) return json(res, { ok: false, error: "Keine Berechtigung." }, 403); const body = req.body || {}; const name = String(body.name || "").trim().slice(0, 80); const username = String(body.username || "").trim().toLowerCase().slice(0, 40); if (!name || !username) return json(res, { ok: false, error: "Name und Benutzername sind Pflicht." }, 400); const id = generateId(); try { db.prepare( `INSERT INTO users (id, name, username, email, role_id, status, note, created_at, created_by) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?)` ).run(id, name, username, String(body.email || "").trim() || null, body.roleId || null, String(body.note || "").slice(0, 500) || null, nowIso(), session.actor); } catch { return json(res, { ok: false, error: "Benutzername bereits vergeben." }, 400); } logAction(session.actor, "user.prepare", id, { username }); return json(res, { ok: true, id }); } /** NUR Owner: legt einen Zugang final an (neu oder aus einem vorbereiteten Entwurf) inkl. Code. */ export async function finalizeUser(req, res, session) { if (!session?.isOwner) return json(res, { ok: false, error: "Nur der Hauptadministrator darf Zugänge anlegen/aktivieren." }, 403); const body = req.body || {}; let id = body.pendingUserId ? String(body.pendingUserId) : null; const name = String(body.name || "").trim().slice(0, 80); const username = String(body.username || "").trim().toLowerCase().slice(0, 40); const email = String(body.email || "").trim() || null; const roleId = body.roleId || null; const note = String(body.note || "").slice(0, 500) || null; if (!id && (!name || !username)) { return json(res, { ok: false, error: "Name und Benutzername sind Pflicht." }, 400); } const code = body.code ? String(body.code).trim().slice(0, 40) : generateFriendlyCode(); const codeHash = await sha256Hex(code); const codeEnc = await encryptCode(code); let expiresAt = null; if (body.expiresAt) expiresAt = new Date(body.expiresAt).toISOString(); else if (body.expiresInDays) expiresAt = new Date(Date.now() + Number(body.expiresInDays) * 86400000).toISOString(); if (id) { const existing = loadUser(id); if (!existing) return json(res, { ok: false, error: "Vorbereiteter Benutzer nicht gefunden." }, 404); db.prepare( `UPDATE users SET name = ?, username = ?, email = ?, role_id = ?, code_hash = ?, code_enc = ?, status = 'active', note = ?, expires_at = ? WHERE id = ?` ).run(name || existing.name, username || existing.username, email, roleId, codeHash, codeEnc, note, expiresAt, id); } else { id = generateId(); try { db.prepare( `INSERT INTO users (id, name, username, email, role_id, code_hash, code_enc, status, note, expires_at, created_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)` ).run(id, name, username, email, roleId, codeHash, codeEnc, note, expiresAt, nowIso(), session.actor); } catch { return json(res, { ok: false, error: "Benutzername bereits vergeben." }, 400); } } logAction(session.actor, "user.finalize", id, { username: username || undefined }); // Der Klartext-Code wird JETZT einmalig zurückgegeben — danach nie wieder abrufbar, // außer über den geschützten "Code anzeigen"-Reveal-Flow (siehe revealUserCode()). return json(res, { ok: true, id, code }); } /** NUR Owner: neuen Code für bestehenden Benutzer setzen (manuell oder automatisch). */ export async function setUserCode(req, res, session) { if (!session?.isOwner) return json(res, { ok: false, error: "Nur der Hauptadministrator darf Codes ändern." }, 403); const body = req.body || {}; const id = String(body.id || ""); const user = loadUser(id); if (!user) return json(res, { ok: false, error: "Benutzer nicht gefunden." }, 404); const code = body.code ? String(body.code).trim().slice(0, 40) : generateFriendlyCode(); const codeHash = await sha256Hex(code); const codeEnc = await encryptCode(code); db.prepare(`UPDATE users SET code_hash = ?, code_enc = ? WHERE id = ?`).run(codeHash, codeEnc, id); endAllSessionsForUser(id); // alter Code darf auf keinem Gerät mehr gültig bleiben logAction(session.actor, "user.code_reset", id, null); return json(res, { ok: true, code }); } /** NUR Owner, mit erneuter Re-Auth (ownerCode): zeigt einen bestehenden Code im Klartext. */ export async function revealUserCode(req, res, session) { if (!session?.isOwner) return json(res, { ok: false, error: "Nur der Hauptadministrator darf Codes ansehen." }, 403); const body = req.body || {}; const reAuth = await verifyCode(String(body.reAuthCode || "")); if (!reAuth.ok || !reAuth.isOwner) { return json(res, { ok: false, error: "Re-Authentifizierung fehlgeschlagen." }, 401); } const id = String(body.id || ""); const user = loadUser(id); if (!user || !user.code_enc) return json(res, { ok: false, error: "Kein Code hinterlegt." }, 404); const code = await decryptCode(user.code_enc); logAction(session.actor, "user.code_reveal", id, null); return json(res, { ok: true, code }); } export async function lockUser(req, res, session) { if (!can(session, "TEAM_LOCK_USER")) return json(res, { ok: false, error: "Keine Berechtigung." }, 403); const body = req.body || {}; const id = String(body.id || ""); db.prepare(`UPDATE users SET status = 'locked' WHERE id = ?`).run(id); endAllSessionsForUser(id); logAction(session.actor, "user.lock", id, null); return json(res, { ok: true }); } export async function unlockUser(req, res, session) { if (!can(session, "TEAM_LOCK_USER")) return json(res, { ok: false, error: "Keine Berechtigung." }, 403); const body = req.body || {}; const id = String(body.id || ""); const user = loadUser(id); if (!user) return json(res, { ok: false, error: "Benutzer nicht gefunden." }, 404); if (user.expires_at && new Date(user.expires_at) < new Date()) { return json(res, { ok: false, error: "Zugang ist abgelaufen — bitte zuerst Gültigkeit verlängern." }, 400); } db.prepare(`UPDATE users SET status = 'active' WHERE id = ?`).run(id); logAction(session.actor, "user.unlock", id, null); return json(res, { ok: true }); } /** NUR Owner: Zugang vollständig löschen. */ export async function deleteUser(req, res, session) { if (!session?.isOwner) return json(res, { ok: false, error: "Nur der Hauptadministrator darf Zugänge löschen." }, 403); const body = req.body || {}; const id = String(body.id || ""); db.prepare(`DELETE FROM user_permission_overrides WHERE user_id = ?`).run(id); db.prepare(`DELETE FROM sessions WHERE user_id = ?`).run(id); db.prepare(`DELETE FROM users WHERE id = ?`).run(id); logAction(session.actor, "user.delete", id, null); return json(res, { ok: true }); } export async function setUserRole(req, res, session) { if (!can(session, "TEAM_ASSIGN_ROLE")) return json(res, { ok: false, error: "Keine Berechtigung." }, 403); const body = req.body || {}; const id = String(body.id || ""); const roleId = body.roleId || null; db.prepare(`UPDATE users SET role_id = ? WHERE id = ?`).run(roleId, id); logAction(session.actor, "user.set_role", id, { roleId }); return json(res, { ok: true }); } export async function setUserPermissions(req, res, session) { if (!can(session, "TEAM_EDIT_PERMISSIONS")) return json(res, { ok: false, error: "Keine Berechtigung." }, 403); const body = req.body || {}; const id = String(body.id || ""); const overrides = body.overrides && typeof body.overrides === "object" ? body.overrides : {}; db.prepare(`DELETE FROM user_permission_overrides WHERE user_id = ?`).run(id); for (const [key, allowed] of Object.entries(overrides)) { if (!isValidPermissionKey(key)) continue; // Owner-only-Aktionen können hier nie landen db.prepare(`INSERT INTO user_permission_overrides (user_id, permission, allowed) VALUES (?, ?, ?)`).run(id, key, allowed ? 1 : 0); } logAction(session.actor, "user.set_permissions", id, { overrides }); return json(res, { ok: true }); } /** NUR Owner: alle Sitzungen einer Person sofort beenden ("alle Geräte abmelden"). */ export async function endUserSessions(req, res, session) { if (!session?.isOwner) return json(res, { ok: false, error: "Nur der Hauptadministrator darf Sitzungen beenden." }, 403); const body = req.body || {}; const id = String(body.id || ""); endAllSessionsForUser(id); logAction(session.actor, "user.end_sessions", id, null); return json(res, { ok: true }); } export async function updateUserMeta(req, res, session) { if (!session?.isOwner) return json(res, { ok: false, error: "Nur der Hauptadministrator darf diese Angaben ändern." }, 403); const body = req.body || {}; const id = String(body.id || ""); const user = loadUser(id); if (!user) return json(res, { ok: false, error: "Benutzer nicht gefunden." }, 404); const name = body.name !== undefined ? String(body.name).trim().slice(0, 80) || user.name : user.name; const email = body.email !== undefined ? String(body.email).trim() || null : user.email; const note = body.note !== undefined ? String(body.note).slice(0, 500) || null : user.note; let expiresAt = user.expires_at; if (body.expiresAt !== undefined) expiresAt = body.expiresAt ? new Date(body.expiresAt).toISOString() : null; if (body.expiresInDays) expiresAt = new Date(Date.now() + Number(body.expiresInDays) * 86400000).toISOString(); db.prepare(`UPDATE users SET name = ?, email = ?, note = ?, expires_at = ? WHERE id = ?`).run(name, email, note, expiresAt, id); logAction(session.actor, "user.update_meta", id, null); return json(res, { ok: true }); }