Vollständiges Zugangs-/Rollen-/Rechtesystem gebaut (Anforderungsdokument): D1-Datenbank, AES-verschlüsselte Codes nur für Owner einsehbar, granulare Permissions, Session-Auth mit Lockout, Zugänge-&-Teammitglieder-Verwaltungsseite, Bewerbungen mit Status/Notizen/Entwürfen

This commit is contained in:
2026-07-31 00:37:04 +02:00
parent 96087f460e
commit 6a1c0e355c
18 changed files with 2283 additions and 539 deletions
@@ -0,0 +1,218 @@
/* =====================================================================
routes/applications.js — Bewerbungen (Postfach), jetzt in D1 mit
vollem Rollen-/Rechte-Modell statt dem alten pauschalen Owner/Helfer-
Zugriff. Status, Zuweisung, Notizen und Antwortentwürfe pro Bewerbung.
===================================================================== */
import { generateId, nowIso } from "../lib/crypto.js";
import { hasPermission } from "../lib/permissions.js";
import { logAction } from "../lib/audit.js";
import { json } from "../lib/http.js";
const ALLOWED_TYPES = ["creator", "scout", "kooperation"];
function can(session, key) {
if (!session) return false;
if (session.isOwner) return true;
return hasPermission(session.permissions, session.overrides, key);
}
function viewPermissionForType(type) {
return type === "creator" ? "APPLICATIONS_VIEW_CREATOR" : "APPLICATIONS_VIEW_MANAGER_SCOUT";
}
export async function submitApplication(request, env) {
let body;
try {
body = await request.json();
} catch {
return json({ ok: false, error: "Ungültiges JSON." }, 400);
}
const { type, data } = body || {};
if (!ALLOWED_TYPES.includes(type) || typeof data !== "object" || !data) {
return json({ ok: false, error: "Ungültige Bewerbungsdaten." }, 400);
}
const values = Object.values(data).map((v) => String(v || "").trim());
if (values.every((v) => v === "")) return json({ ok: false, error: "Leere Bewerbung." }, 400);
const id = generateId();
await env.DB.prepare(
`INSERT INTO applications (id, type, data, status, archived, created_at) VALUES (?, ?, ?, 'neu', 0, ?)`
)
.bind(id, type, JSON.stringify(data), nowIso())
.run();
return json({ ok: true });
}
export async function listApplications(request, env, session) {
if (!session) return json({ ok: false, error: "Nicht angemeldet." }, 401);
const body = await request.json().catch(() => ({}));
const includeArchived = !!body.includeArchived;
const allowedTypes = ALLOWED_TYPES.filter((t) => session.isOwner || can(session, viewPermissionForType(t)));
if (allowedTypes.length === 0) return json({ ok: true, applications: [] });
if (includeArchived && !session.isOwner && !can(session, "APPLICATIONS_VIEW_ARCHIVED")) {
return json({ ok: false, error: "Keine Berechtigung für archivierte Bewerbungen." }, 403);
}
const placeholders = allowedTypes.map(() => "?").join(",");
const archivedClause = includeArchived ? "" : "AND archived = 0";
const rows = await env.DB.prepare(
`SELECT * FROM applications WHERE type IN (${placeholders}) ${archivedClause} ORDER BY created_at DESC LIMIT 300`
)
.bind(...allowedTypes)
.all();
const applications = (rows.results || []).map((r) => ({
id: r.id,
type: r.type,
data: session.isOwner || can(session, "APPLICATIONS_OPEN_FULL") ? JSON.parse(r.data) : null,
status: r.status,
assignedTo: r.assigned_to,
archived: !!r.archived,
createdAt: r.created_at,
}));
return json({ ok: true, applications });
}
export async function changeApplicationStatus(request, env, session) {
if (!can(session, "APPLICATIONS_CHANGE_STATUS")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const status = String(body.status || "").slice(0, 40);
await env.DB.prepare(`UPDATE applications SET status = ? WHERE id = ?`).bind(status, id).run();
await logAction(env, session.actor, "application.status", id, { status });
return json({ ok: true });
}
export async function takeOverApplication(request, env, session) {
if (!can(session, "APPLICATIONS_TAKE_OVER")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const who = session.isOwner ? "owner" : session.userId;
await env.DB.prepare(`UPDATE applications SET assigned_to = ? WHERE id = ?`).bind(who, id).run();
await logAction(env, session.actor, "application.take_over", id, null);
return json({ ok: true });
}
export async function assignApplication(request, env, session) {
if (!can(session, "APPLICATIONS_ASSIGN")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const assignedTo = body.assignedTo ? String(body.assignedTo) : null;
await env.DB.prepare(`UPDATE applications SET assigned_to = ? WHERE id = ?`).bind(assignedTo, id).run();
await logAction(env, session.actor, "application.assign", id, { assignedTo });
return json({ ok: true });
}
export async function archiveApplication(request, env, session) {
if (!can(session, "APPLICATIONS_ARCHIVE")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
await env.DB.prepare(`UPDATE applications SET archived = 1 WHERE id = ?`).bind(id).run();
await logAction(env, session.actor, "application.archive", id, null);
return json({ ok: true });
}
export async function restoreApplication(request, env, session) {
if (!can(session, "APPLICATIONS_RESTORE")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
await env.DB.prepare(`UPDATE applications SET archived = 0 WHERE id = ?`).bind(id).run();
await logAction(env, session.actor, "application.restore", id, null);
return json({ ok: true });
}
export async function deleteApplication(request, env, session) {
if (!can(session, "APPLICATIONS_DELETE")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
await env.DB.prepare(`DELETE FROM application_notes WHERE application_id = ?`).bind(id).run();
await env.DB.prepare(`DELETE FROM application_drafts WHERE application_id = ?`).bind(id).run();
await env.DB.prepare(`DELETE FROM applications WHERE id = ?`).bind(id).run();
await logAction(env, session.actor, "application.delete", id, null);
return json({ ok: true });
}
export async function markAnswered(request, env, session) {
if (!can(session, "APPLICATIONS_MARK_ANSWERED")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
await env.DB.prepare(`UPDATE applications SET status = 'beantwortet' WHERE id = ?`).bind(id).run();
await logAction(env, session.actor, "application.mark_answered", id, null);
return json({ ok: true });
}
/* ---- Notizen ---- */
export async function listNotes(request, env, session) {
if (!can(session, "NOTES_READ")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const rows = await env.DB.prepare(
`SELECT * FROM application_notes WHERE application_id = ? ORDER BY created_at ASC`
)
.bind(String(body.applicationId || ""))
.all();
return json({ ok: true, notes: rows.results || [] });
}
export async function addNote(request, env, session) {
if (!can(session, "NOTES_WRITE")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const text = String(body.text || "").slice(0, 2000);
if (!text.trim()) return json({ ok: false, error: "Leere Notiz." }, 400);
await env.DB.prepare(
`INSERT INTO application_notes (application_id, author, text, created_at) VALUES (?, ?, ?, ?)`
)
.bind(String(body.applicationId || ""), session.actor, text, nowIso())
.run();
await logAction(env, session.actor, "note.add", body.applicationId, null);
return json({ ok: true });
}
export async function deleteNote(request, env, session) {
if (!can(session, "NOTES_DELETE")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
await env.DB.prepare(`DELETE FROM application_notes WHERE id = ?`).bind(Number(body.id)).run();
await logAction(env, session.actor, "note.delete", String(body.id), null);
return json({ ok: true });
}
/* ---- Antwortentwürfe ---- */
export async function listDrafts(request, env, session) {
const body = await request.json().catch(() => ({}));
const rows = await env.DB.prepare(
`SELECT * FROM application_drafts WHERE application_id = ? ORDER BY updated_at DESC`
)
.bind(String(body.applicationId || ""))
.all();
return json({ ok: true, drafts: rows.results || [] });
}
export async function saveDraft(request, env, session) {
const body = await request.json().catch(() => ({}));
const content = String(body.content || "").slice(0, 5000);
if (body.id) {
const existing = await env.DB.prepare(`SELECT * FROM application_drafts WHERE id = ?`).bind(Number(body.id)).first();
if (!existing) return json({ ok: false, error: "Entwurf nicht gefunden." }, 404);
const isOwnDraft = existing.author === session.actor;
if (!can(session, isOwnDraft ? "DRAFTS_EDIT_OWN" : "DRAFTS_EDIT_OTHERS")) {
return json({ ok: false, error: "Keine Berechtigung." }, 403);
}
await env.DB.prepare(`UPDATE application_drafts SET content = ?, updated_at = ? WHERE id = ?`)
.bind(content, nowIso(), Number(body.id))
.run();
await logAction(env, session.actor, "draft.update", String(body.id), null);
return json({ ok: true });
}
if (!can(session, "DRAFTS_CREATE")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
await env.DB.prepare(
`INSERT INTO application_drafts (application_id, author, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`
)
.bind(String(body.applicationId || ""), session.actor, content, nowIso(), nowIso())
.run();
await logAction(env, session.actor, "draft.create", body.applicationId, null);
return json({ ok: true });
}
+20
View File
@@ -0,0 +1,20 @@
/* =====================================================================
routes/audit.js — Aktivitätsprotokoll ansehen
===================================================================== */
import { hasPermission } from "../lib/permissions.js";
import { json } from "../lib/http.js";
export async function listAuditLog(request, env, session) {
if (!session) return json({ ok: false, error: "Nicht angemeldet." }, 401);
if (!session.isOwner && !hasPermission(session.permissions, session.overrides, "SETTINGS_VIEW_ACTIVITY_LOG")) {
return json({ ok: false, error: "Keine Berechtigung." }, 403);
}
const body = await request.json().catch(() => ({}));
const limit = Math.min(Number(body.limit) || 100, 500);
const rows = await env.DB.prepare(
`SELECT id, actor, action, target, detail, created_at FROM audit_log ORDER BY created_at DESC LIMIT ?`
)
.bind(limit)
.all();
return json({ ok: true, entries: rows.results || [] });
}
+69
View File
@@ -0,0 +1,69 @@
/* =====================================================================
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 } from "../lib/crypto.js";
import { json } from "../lib/http.js";
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 });
}
+95
View File
@@ -0,0 +1,95 @@
/* =====================================================================
routes/roles.js — Rollen erstellen/umbenennen/verwalten
Die Owner-Rolle existiert absichtlich NICHT in dieser Tabelle — sie ist
fest an POSTFACH_CODE gebunden und kann hier weder erstellt, verändert
noch gelöscht werden (siehe Anforderungsdokument: "Meine persönliche
Hauptadministrator-Rolle bleibt davon ausgeschlossen").
===================================================================== */
import { generateId, nowIso } from "../lib/crypto.js";
import { isValidPermissionKey, hasPermission } from "../lib/permissions.js";
import { logAction } from "../lib/audit.js";
import { json } from "../lib/http.js";
function canManageRoles(session) {
if (!session) return false;
if (session.isOwner) return true;
return (
hasPermission(session.permissions, session.overrides, "TEAM_CREATE_ROLE") ||
hasPermission(session.permissions, session.overrides, "TEAM_EDIT_PERMISSIONS")
);
}
function canViewRoles(session) {
if (!session) return false;
if (session.isOwner) return true;
return (
canManageRoles(session) ||
hasPermission(session.permissions, session.overrides, "TEAM_ASSIGN_ROLE") ||
hasPermission(session.permissions, session.overrides, "TEAM_VIEW")
);
}
function sanitizePermissions(list) {
if (!Array.isArray(list)) return [];
return [...new Set(list.filter((k) => typeof k === "string" && isValidPermissionKey(k)))];
}
export async function listRoles(env, session) {
if (!canViewRoles(session)) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const rows = await env.DB.prepare(`SELECT * FROM roles ORDER BY created_at ASC`).all();
const roles = (rows.results || []).map((r) => ({ ...r, permissions: JSON.parse(r.permissions || "[]") }));
return json({ ok: true, roles });
}
export async function createRole(request, env, session) {
if (!canManageRoles(session)) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const name = String(body.name || "").trim().slice(0, 60);
if (!name) return json({ ok: false, error: "Name fehlt." }, 400);
const permissions = sanitizePermissions(body.permissions);
const id = generateId();
await env.DB.prepare(`INSERT INTO roles (id, name, permissions, created_at) VALUES (?, ?, ?, ?)`)
.bind(id, name, JSON.stringify(permissions), nowIso())
.run();
await logAction(env, session.actor, "role.create", id, { name, permissions });
return json({ ok: true, role: { id, name, permissions } });
}
export async function updateRole(request, env, session) {
if (!canManageRoles(session)) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
if (!id) return json({ ok: false, error: "id fehlt." }, 400);
const existing = await env.DB.prepare(`SELECT * FROM roles WHERE id = ?`).bind(id).first();
if (!existing) return json({ ok: false, error: "Rolle nicht gefunden." }, 404);
const name = body.name !== undefined ? String(body.name).trim().slice(0, 60) || existing.name : existing.name;
const permissions = body.permissions !== undefined ? sanitizePermissions(body.permissions) : JSON.parse(existing.permissions || "[]");
await env.DB.prepare(`UPDATE roles SET name = ?, permissions = ? WHERE id = ?`)
.bind(name, JSON.stringify(permissions), id)
.run();
await logAction(env, session.actor, "role.update", id, { name, permissions });
return json({ ok: true });
}
export async function deleteRole(request, env, session) {
if (!canManageRoles(session)) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
if (!id) return json({ ok: false, error: "id fehlt." }, 400);
const inUse = await env.DB.prepare(`SELECT COUNT(*) as n FROM users WHERE role_id = ?`).bind(id).first();
if (inUse && inUse.n > 0) {
return json({ ok: false, error: "Rolle ist noch Personen zugewiesen — erst umziehen." }, 400);
}
await env.DB.prepare(`DELETE FROM roles WHERE id = ?`).bind(id).run();
await logAction(env, session.actor, "role.delete", id, null);
return json({ ok: true });
}
+269
View File
@@ -0,0 +1,269 @@
/* =====================================================================
routes/users.js — Zugänge und Teammitglieder
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 { 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);
}
async function loadUser(env, id) {
return env.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 = ?`
)
.bind(id)
.first();
}
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(env, session) {
if (!can(session, "TEAM_VIEW") && !session?.isOwner) {
return json({ ok: false, error: "Keine Berechtigung." }, 403);
}
const rows = await env.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({ ok: true, users: (rows.results || []).map(publicUser) });
}
/** Nicht-Owner mit TEAM_PREPARE_USER können einen Entwurf anlegen — noch ohne Code, inaktiv. */
export async function prepareUser(request, env, session) {
if (!can(session, "TEAM_PREPARE_USER")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const name = String(body.name || "").trim().slice(0, 80);
const username = String(body.username || "").trim().toLowerCase().slice(0, 40);
if (!name || !username) return json({ ok: false, error: "Name und Benutzername sind Pflicht." }, 400);
const id = generateId();
try {
await env.DB.prepare(
`INSERT INTO users (id, name, username, email, role_id, status, note, created_at, created_by)
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?)`
)
.bind(id, name, username, String(body.email || "").trim() || null, body.roleId || null, String(body.note || "").slice(0, 500) || null, nowIso(), session.actor)
.run();
} catch {
return json({ ok: false, error: "Benutzername bereits vergeben." }, 400);
}
await logAction(env, session.actor, "user.prepare", id, { username });
return json({ ok: true, id });
}
/** NUR Owner: legt einen Zugang final an (neu oder aus einem vorbereiteten Entwurf) inkl. Code. */
export async function finalizeUser(request, env, session) {
if (!session?.isOwner) return json({ ok: false, error: "Nur der Hauptadministrator darf Zugänge anlegen/aktivieren." }, 403);
const body = await request.json().catch(() => ({}));
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({ 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(env, 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 = await loadUser(env, id);
if (!existing) return json({ ok: false, error: "Vorbereiteter Benutzer nicht gefunden." }, 404);
await env.DB.prepare(
`UPDATE users SET name = ?, username = ?, email = ?, role_id = ?, code_hash = ?, code_enc = ?,
status = 'active', note = ?, expires_at = ? WHERE id = ?`
)
.bind(name || existing.name, username || existing.username, email, roleId, codeHash, codeEnc, note, expiresAt, id)
.run();
} else {
id = generateId();
try {
await env.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', ?, ?, ?, ?)`
)
.bind(id, name, username, email, roleId, codeHash, codeEnc, note, expiresAt, nowIso(), session.actor)
.run();
} catch {
return json({ ok: false, error: "Benutzername bereits vergeben." }, 400);
}
}
await logAction(env, 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 revealCode()).
return json({ ok: true, id, code });
}
/** NUR Owner: neuen Code für bestehenden Benutzer setzen (manuell oder automatisch). */
export async function setUserCode(request, env, session) {
if (!session?.isOwner) return json({ ok: false, error: "Nur der Hauptadministrator darf Codes ändern." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const user = await loadUser(env, id);
if (!user) return json({ 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(env, code);
await env.DB.prepare(`UPDATE users SET code_hash = ?, code_enc = ? WHERE id = ?`)
.bind(codeHash, codeEnc, id)
.run();
await endAllSessionsForUser(env, id); // alter Code darf auf keinem Gerät mehr gültig bleiben
await logAction(env, session.actor, "user.code_reset", id, null);
return json({ ok: true, code });
}
/** NUR Owner, mit erneuter Re-Auth (ownerCode): zeigt einen bestehenden Code im Klartext. */
export async function revealUserCode(request, env, session) {
if (!session?.isOwner) return json({ ok: false, error: "Nur der Hauptadministrator darf Codes ansehen." }, 403);
const body = await request.json().catch(() => ({}));
const reAuth = await verifyCode(env, String(body.reAuthCode || ""));
if (!reAuth.ok || !reAuth.isOwner) {
return json({ ok: false, error: "Re-Authentifizierung fehlgeschlagen." }, 401);
}
const id = String(body.id || "");
const user = await loadUser(env, id);
if (!user || !user.code_enc) return json({ ok: false, error: "Kein Code hinterlegt." }, 404);
const code = await decryptCode(env, user.code_enc);
await logAction(env, session.actor, "user.code_reveal", id, null);
return json({ ok: true, code });
}
export async function lockUser(request, env, session) {
if (!can(session, "TEAM_LOCK_USER")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
await env.DB.prepare(`UPDATE users SET status = 'locked' WHERE id = ?`).bind(id).run();
await endAllSessionsForUser(env, id);
await logAction(env, session.actor, "user.lock", id, null);
return json({ ok: true });
}
export async function unlockUser(request, env, session) {
if (!can(session, "TEAM_LOCK_USER")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const user = await loadUser(env, id);
if (!user) return json({ ok: false, error: "Benutzer nicht gefunden." }, 404);
if (user.expires_at && new Date(user.expires_at) < new Date()) {
return json({ ok: false, error: "Zugang ist abgelaufen — bitte zuerst Gültigkeit verlängern." }, 400);
}
await env.DB.prepare(`UPDATE users SET status = 'active' WHERE id = ?`).bind(id).run();
await logAction(env, session.actor, "user.unlock", id, null);
return json({ ok: true });
}
/** NUR Owner: Zugang vollständig löschen. */
export async function deleteUser(request, env, session) {
if (!session?.isOwner) return json({ ok: false, error: "Nur der Hauptadministrator darf Zugänge löschen." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
await env.DB.prepare(`DELETE FROM user_permission_overrides WHERE user_id = ?`).bind(id).run();
await env.DB.prepare(`DELETE FROM sessions WHERE user_id = ?`).bind(id).run();
await env.DB.prepare(`DELETE FROM users WHERE id = ?`).bind(id).run();
await logAction(env, session.actor, "user.delete", id, null);
return json({ ok: true });
}
export async function setUserRole(request, env, session) {
if (!can(session, "TEAM_ASSIGN_ROLE")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const roleId = body.roleId || null;
await env.DB.prepare(`UPDATE users SET role_id = ? WHERE id = ?`).bind(roleId, id).run();
await logAction(env, session.actor, "user.set_role", id, { roleId });
return json({ ok: true });
}
export async function setUserPermissions(request, env, session) {
if (!can(session, "TEAM_EDIT_PERMISSIONS")) return json({ ok: false, error: "Keine Berechtigung." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const overrides = body.overrides && typeof body.overrides === "object" ? body.overrides : {};
await env.DB.prepare(`DELETE FROM user_permission_overrides WHERE user_id = ?`).bind(id).run();
for (const [key, allowed] of Object.entries(overrides)) {
if (!isValidPermissionKey(key)) continue; // Owner-only-Aktionen können hier nie landen
await env.DB.prepare(
`INSERT INTO user_permission_overrides (user_id, permission, allowed) VALUES (?, ?, ?)`
)
.bind(id, key, allowed ? 1 : 0)
.run();
}
await logAction(env, session.actor, "user.set_permissions", id, { overrides });
return json({ ok: true });
}
/** NUR Owner: alle Sitzungen einer Person sofort beenden ("alle Geräte abmelden"). */
export async function endUserSessions(request, env, session) {
if (!session?.isOwner) return json({ ok: false, error: "Nur der Hauptadministrator darf Sitzungen beenden." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
await endAllSessionsForUser(env, id);
await logAction(env, session.actor, "user.end_sessions", id, null);
return json({ ok: true });
}
export async function updateUserMeta(request, env, session) {
if (!session?.isOwner) return json({ ok: false, error: "Nur der Hauptadministrator darf diese Angaben ändern." }, 403);
const body = await request.json().catch(() => ({}));
const id = String(body.id || "");
const user = await loadUser(env, id);
if (!user) return json({ 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();
await env.DB.prepare(`UPDATE users SET name = ?, email = ?, note = ?, expires_at = ? WHERE id = ?`)
.bind(name, email, note, expiresAt, id)
.run();
await logAction(env, session.actor, "user.update_meta", id, null);
return json({ ok: true });
}