/* ===================================================================== zugaenge.js — Logik für zugaenge.html ("Zugänge und Teammitglieder") WICHTIG: Diese Permission-Liste muss exakt mit cloudflare-worker/src/lib/permissions.js übereinstimmen (Anzeige only — die eigentliche Durchsetzung passiert immer serverseitig im Worker). ===================================================================== */ // Gleiche Worker-URL wie überall sonst (forms.js, postfach.html, index.html). const API_BASE_URL = "https://dogfather-universe-postfach.dogfather1608.workers.dev"; const PERMISSION_GROUPS = [ { label: "Bewerbungsbereiche", keys: ["APPLICATIONS_VIEW_CREATOR", "APPLICATIONS_VIEW_MANAGER_SCOUT", "APPLICATIONS_VIEW_ARCHIVED", "APPLICATIONS_OPEN_FULL"] }, { label: "Bearbeitung", keys: ["APPLICATIONS_TAKE_OVER", "APPLICATIONS_ASSIGN", "APPLICATIONS_CHANGE_STATUS", "APPLICATIONS_ARCHIVE", "APPLICATIONS_RESTORE", "APPLICATIONS_DELETE"] }, { label: "Antworten", keys: ["DRAFTS_CREATE", "DRAFTS_EDIT_OWN", "DRAFTS_EDIT_OTHERS", "REPLIES_COPY", "EMAILS_COPY", "APPLICATIONS_MARK_ANSWERED"] }, { label: "Interne Notizen", keys: ["NOTES_READ", "NOTES_WRITE", "NOTES_EDIT_OWN", "NOTES_EDIT_OTHERS", "NOTES_DELETE"] }, { label: "Teamverwaltung", keys: ["TEAM_VIEW", "TEAM_PREPARE_USER", "TEAM_LOCK_USER", "TEAM_ASSIGN_ROLE", "TEAM_CREATE_ROLE", "TEAM_EDIT_PERMISSIONS"] }, { label: "Einstellungen", keys: ["SETTINGS_EDIT_FORMS", "SETTINGS_EDIT_STATUS_TYPES", "SETTINGS_VIEW_STATS", "SETTINGS_VIEW_ACTIVITY_LOG", "SETTINGS_EDIT_GENERAL"] }, ]; const PERMISSION_LABELS = { APPLICATIONS_VIEW_CREATOR: "Creator-Bewerbungen ansehen", APPLICATIONS_VIEW_MANAGER_SCOUT: "Manager-/Scout-Bewerbungen ansehen", APPLICATIONS_VIEW_ARCHIVED: "Archivierte Bewerbungen ansehen", APPLICATIONS_OPEN_FULL: "Vollständige Bewerbungen öffnen", APPLICATIONS_TAKE_OVER: "Bewerbungen übernehmen", APPLICATIONS_ASSIGN: "Bewerbungen zuweisen", APPLICATIONS_CHANGE_STATUS: "Status verändern", APPLICATIONS_ARCHIVE: "Archivieren", APPLICATIONS_RESTORE: "Wiederherstellen", APPLICATIONS_DELETE: "Löschen", DRAFTS_CREATE: "Antwortentwürfe erstellen", DRAFTS_EDIT_OWN: "Eigene Entwürfe bearbeiten", DRAFTS_EDIT_OTHERS: "Fremde Entwürfe bearbeiten", REPLIES_COPY: "Antworten kopieren", EMAILS_COPY: "E-Mail-Adressen kopieren", APPLICATIONS_MARK_ANSWERED: "Als beantwortet markieren", NOTES_READ: "Notizen lesen", NOTES_WRITE: "Notizen schreiben", NOTES_EDIT_OWN: "Eigene Notizen bearbeiten", NOTES_EDIT_OTHERS: "Fremde Notizen bearbeiten", NOTES_DELETE: "Notizen löschen", TEAM_VIEW: "Teammitglieder ansehen", TEAM_PREPARE_USER: "Neue Benutzer vorbereiten", TEAM_LOCK_USER: "Benutzer sperren", TEAM_ASSIGN_ROLE: "Rollen vergeben", TEAM_CREATE_ROLE: "Rollen erstellen", TEAM_EDIT_PERMISSIONS: "Berechtigungen verändern", SETTINGS_EDIT_FORMS: "Bewerbungsformulare bearbeiten", SETTINGS_EDIT_STATUS_TYPES: "Statusarten verändern", SETTINGS_VIEW_STATS: "Statistiken ansehen", SETTINGS_VIEW_ACTIVITY_LOG: "Aktivitätsverlauf ansehen", SETTINGS_EDIT_GENERAL: "Allgemeine Einstellungen bearbeiten", }; const { getSession, clearSession, sessionHasPermission, apiCall, doLogin, doLogout } = window.AdminAuth; let currentRoles = []; let currentUsers = []; function $(id) { return document.getElementById(id); } function showApp(session) { $("gate-container").style.display = "none"; $("session-bar").style.display = "block"; $("main-area").style.display = "block"; $("role-badge").textContent = session.isOwner ? "Eingeloggt als: Dogi (Owner)" : `Eingeloggt als: ${session.name} (${session.roleName})`; $("new-user-btn").style.display = session.isOwner || sessionHasPermission(session, "TEAM_PREPARE_USER") ? "inline-flex" : "none"; $("new-role-btn").style.display = session.isOwner || sessionHasPermission(session, "TEAM_CREATE_ROLE") ? "inline-flex" : "none"; const auditTabBtn = document.querySelector('[data-tab="tab-audit"]'); const canViewAudit = session.isOwner || sessionHasPermission(session, "SETTINGS_VIEW_ACTIVITY_LOG"); if (auditTabBtn) auditTabBtn.style.display = canViewAudit ? "" : "none"; loadRoles().then(loadUsers); if (session.isOwner || sessionHasPermission(session, "SETTINGS_VIEW_ACTIVITY_LOG")) loadAudit(); } function showGate() { $("gate-container").style.display = "block"; $("session-bar").style.display = "none"; $("main-area").style.display = "none"; } /* ---------------- Tabs ---------------- */ function setupTabs() { document.querySelectorAll(".tab-btn").forEach((btn) => { btn.addEventListener("click", () => { document.querySelectorAll(".tab-btn").forEach((b) => b.classList.remove("active")); document.querySelectorAll(".tab-panel").forEach((p) => p.classList.remove("active")); btn.classList.add("active"); $(btn.dataset.tab).classList.add("active"); }); }); } /* ---------------- Users ---------------- */ async function loadUsers() { const res = await apiCall(API_BASE_URL, "/admin/users/list", {}); if (!res.ok) return; currentUsers = res.users; renderUsers(); } function roleNameById(id) { const r = currentRoles.find((x) => x.id === id); return r ? r.name : "—"; } function renderUsers() { const session = getSession(); const container = $("users-list"); if (!currentUsers.length) { container.innerHTML = `

Noch keine Zugänge angelegt.

`; return; } container.innerHTML = currentUsers .map((u) => { const statusClass = u.status === "active" ? "active" : u.status === "pending" ? "pending" : "locked"; const isOwner = session.isOwner; const canLock = isOwner || sessionHasPermission(session, "TEAM_LOCK_USER"); const canAssignRole = isOwner || sessionHasPermission(session, "TEAM_ASSIGN_ROLE"); const canEditPerms = isOwner || sessionHasPermission(session, "TEAM_EDIT_PERMISSIONS"); const actions = []; if (u.status === "pending" && isOwner) { actions.push(``); } if (isOwner && u.status !== "pending") { actions.push(``); actions.push(``); actions.push(``); } if (canLock && u.status !== "pending") { actions.push( u.status === "active" ? `` : `` ); } if (canAssignRole) { actions.push(``); } if (canEditPerms) { actions.push(``); } if (isOwner) { actions.push(``); } return `
${u.name}
@${u.username}${u.email ? " · " + u.email : ""}
${roleNameById(u.roleId)}
${u.status}
${u.expiresAt ? new Date(u.expiresAt).toLocaleDateString("de-DE") : "dauerhaft"}
${u.lastLoginAt ? new Date(u.lastLoginAt).toLocaleString("de-DE") : "nie"}
${actions.join("")}
`; }) .join(""); container.querySelectorAll(".lock-btn").forEach((b) => b.addEventListener("click", () => userAction("/admin/users/lock", b.dataset.id))); container.querySelectorAll(".unlock-btn").forEach((b) => b.addEventListener("click", () => userAction("/admin/users/unlock", b.dataset.id))); container.querySelectorAll(".delete-btn").forEach((b) => b.addEventListener("click", () => { if (confirm("Diesen Zugang wirklich vollständig löschen?")) userAction("/admin/users/delete", b.dataset.id); }) ); container.querySelectorAll(".endsessions-btn").forEach((b) => b.addEventListener("click", () => userAction("/admin/users/end-sessions", b.dataset.id, "Alle Geräte wurden abgemeldet."))); container.querySelectorAll(".reveal-btn").forEach((b) => b.addEventListener("click", () => openRevealModal(b.dataset.id))); container.querySelectorAll(".newcode-btn").forEach((b) => b.addEventListener("click", () => newCodeForUser(b.dataset.id))); container.querySelectorAll(".finalize-btn").forEach((b) => b.addEventListener("click", () => openUserModal({ pendingUserId: b.dataset.id }))); container.querySelectorAll(".role-btn").forEach((b) => b.addEventListener("click", () => changeUserRole(b.dataset.id))); container.querySelectorAll(".perms-btn").forEach((b) => b.addEventListener("click", () => openPermModal(b.dataset.id))); } async function userAction(path, id, successMsg) { const res = await apiCall(API_BASE_URL, path, { id }); if (res.ok) { if (successMsg) alert(successMsg); loadUsers(); } else { alert(res.error || "Fehler."); } } async function newCodeForUser(id) { if (!confirm("Neuen Code erstellen? Der alte Code funktioniert danach sofort nicht mehr.")) return; const res = await apiCall(API_BASE_URL, "/admin/users/set-code", { id }); if (res.ok) { alert(`Neuer Code: ${res.code}\n\nJetzt sichern/weitergeben — wird nirgends mehr angezeigt!`); } else { alert(res.error || "Fehler."); } } function changeUserRole(id) { const options = currentRoles.map((r) => `${r.name} (${r.id})`).join("\n"); const pick = prompt(`Neue Rolle für diese Person — Rollenname eingeben:\n\nVerfügbar:\n${currentRoles.map((r) => r.name).join(", ")}`); if (pick === null) return; const role = currentRoles.find((r) => r.name.toLowerCase() === pick.trim().toLowerCase()); if (!role) return alert("Rolle nicht gefunden."); apiCall(API_BASE_URL, "/admin/users/set-role", { id, roleId: role.id }).then((res) => { if (res.ok) loadUsers(); else alert(res.error || "Fehler."); }); } /* ---------------- Neuer Zugang / Vorbereiten ---------------- */ function openUserModal({ pendingUserId } = {}) { const session = getSession(); $("user-form").reset(); $("uf-new-code").style.display = "none"; $("uf-pending-id").value = pendingUserId || ""; $("user-modal-title").textContent = pendingUserId ? "Zugang freigeben" : session.isOwner ? "Neuer Zugang" : "Neuen Benutzer vorbereiten"; $("uf-submit-btn").textContent = pendingUserId ? "Freigeben & Code erstellen" : session.isOwner ? "Zugang erstellen" : "Zur Freigabe vorbereiten"; $("uf-code-field").style.display = session.isOwner ? "block" : "none"; const roleSelect = $("uf-role"); roleSelect.innerHTML = currentRoles.map((r) => ``).join(""); if (pendingUserId) { const u = currentUsers.find((x) => x.id === pendingUserId); if (u) { $("uf-name").value = u.name; $("uf-username").value = u.username; $("uf-email").value = u.email || ""; if (u.roleId) roleSelect.value = u.roleId; } } $("user-modal").classList.add("open"); } $("user-modal-close").addEventListener("click", () => $("user-modal").classList.remove("open")); $("new-user-btn").addEventListener("click", () => openUserModal()); document.querySelectorAll('input[name="uf-code-mode"]').forEach((el) => { el.addEventListener("change", () => { $("uf-code-manual").style.display = document.querySelector('input[name="uf-code-mode"]:checked').value === "manual" ? "block" : "none"; }); }); $("uf-expiry").addEventListener("change", () => { $("uf-expiry-date").style.display = $("uf-expiry").value === "date" ? "block" : "none"; }); $("user-form").addEventListener("submit", async (e) => { e.preventDefault(); const session = getSession(); const pendingUserId = $("uf-pending-id").value || null; const isOwner = session.isOwner; const payload = { name: $("uf-name").value.trim(), username: $("uf-username").value.trim(), email: $("uf-email").value.trim(), roleId: $("uf-role").value, note: $("uf-note").value.trim(), }; const statusEl = $("user-form-status"); if (!isOwner && !pendingUserId) { const res = await apiCall(API_BASE_URL, "/admin/users/prepare", payload); if (res.ok) { statusEl.textContent = "Vorbereitet — Dogi muss den Zugang jetzt noch mit einem Code freigeben."; statusEl.className = "form-status show success"; loadUsers(); setTimeout(() => $("user-modal").classList.remove("open"), 1800); } else { statusEl.textContent = res.error || "Fehler."; statusEl.className = "form-status show error"; } return; } if (pendingUserId) payload.pendingUserId = pendingUserId; const mode = document.querySelector('input[name="uf-code-mode"]:checked').value; if (mode === "manual") payload.code = $("uf-code-manual").value.trim(); const expiryChoice = $("uf-expiry").value; if (expiryChoice === "date") payload.expiresAt = $("uf-expiry-date").value; else if (expiryChoice) payload.expiresInDays = Number(expiryChoice); const res = await apiCall(API_BASE_URL, "/admin/users/finalize", payload); if (res.ok) { $("uf-new-code").style.display = "block"; $("uf-new-code").innerHTML = `Code: ${res.code}
Jetzt sichern/weitergeben — wird nirgends mehr angezeigt!`; statusEl.textContent = ""; statusEl.className = "form-status"; loadUsers(); } else { statusEl.textContent = res.error || "Fehler."; statusEl.className = "form-status show error"; } }); /* ---------------- Code anzeigen (Re-Auth) ---------------- */ let revealTargetId = null; function openRevealModal(id) { revealTargetId = id; $("reveal-form").reset(); $("reveal-code-box").style.display = "none"; $("reveal-status").className = "form-status"; $("reveal-modal").classList.add("open"); } $("reveal-modal-close").addEventListener("click", () => $("reveal-modal").classList.remove("open")); $("reveal-form").addEventListener("submit", async (e) => { e.preventDefault(); const res = await apiCall(API_BASE_URL, "/admin/users/reveal-code", { id: revealTargetId, reAuthCode: $("reveal-reauth").value, }); if (res.ok) { $("reveal-code-box").style.display = "block"; $("reveal-code-box").textContent = res.code; $("reveal-status").className = "form-status"; } else { $("reveal-status").textContent = res.error || "Fehler."; $("reveal-status").className = "form-status show error"; } }); /* ---------------- Individuelle Rechte ---------------- */ let permTargetId = null; function openPermModal(id) { permTargetId = id; const user = currentUsers.find((u) => u.id === id); const role = currentRoles.find((r) => r.id === user?.roleId); const roleDefaults = role ? role.permissions : []; $("perm-matrix").innerHTML = PERMISSION_GROUPS.map( (g) => `
${g.label}
${g.keys .map((k) => { const inRole = roleDefaults.includes(k); return ` `; }) .join("")}
${PERMISSION_LABELS[k]} ${inRole ? "(in Rolle enthalten)" : ""}
` ).join(""); $("perm-status").className = "form-status"; $("perm-modal").classList.add("open"); } $("perm-modal-close").addEventListener("click", () => $("perm-modal").classList.remove("open")); $("perm-save-btn").addEventListener("click", async () => { const overrides = {}; document.querySelectorAll("#perm-matrix select[data-perm]").forEach((sel) => { if (sel.value === "allow") overrides[sel.dataset.perm] = true; else if (sel.value === "deny") overrides[sel.dataset.perm] = false; }); const res = await apiCall(API_BASE_URL, "/admin/users/set-permissions", { id: permTargetId, overrides }); if (res.ok) { $("perm-modal").classList.remove("open"); } else { $("perm-status").textContent = res.error || "Fehler."; $("perm-status").className = "form-status show error"; } }); /* ---------------- Rollen ---------------- */ async function loadRoles() { const res = await apiCall(API_BASE_URL, "/admin/roles/list", {}); if (!res.ok) return; currentRoles = res.roles; renderRoles(); } function renderRoles() { const session = getSession(); const canManage = session.isOwner || sessionHasPermission(session, "TEAM_CREATE_ROLE") || sessionHasPermission(session, "TEAM_EDIT_PERMISSIONS"); const container = $("roles-list"); if (!currentRoles.length) { container.innerHTML = `

Noch keine Rollen erstellt.

`; return; } container.innerHTML = currentRoles .map( (r) => `

${r.name}

${r.permissions.length} Rechte aktiv

${ canManage ? `
` : "" }
` ) .join(""); container.querySelectorAll(".edit-role-btn").forEach((b) => b.addEventListener("click", () => openRoleModal(b.dataset.id))); container.querySelectorAll(".delete-role-btn").forEach((b) => b.addEventListener("click", async () => { if (!confirm("Rolle wirklich löschen?")) return; const res = await apiCall(API_BASE_URL, "/admin/roles/delete", { id: b.dataset.id }); if (res.ok) loadRoles(); else alert(res.error || "Fehler."); }) ); } function openRoleModal(id) { const role = id ? currentRoles.find((r) => r.id === id) : null; $("role-modal-title").textContent = role ? "Rolle bearbeiten" : "Neue Rolle"; $("rf-id").value = id || ""; $("rf-name").value = role ? role.name : ""; $("role-form-status").className = "form-status"; $("role-perm-matrix").innerHTML = PERMISSION_GROUPS.map( (g) => `
${g.label}
${g.keys .map( (k) => ` ` ) .join("")}` ).join(""); $("role-modal").classList.add("open"); } $("new-role-btn").addEventListener("click", () => openRoleModal(null)); $("role-modal-close").addEventListener("click", () => $("role-modal").classList.remove("open")); $("role-save-btn").addEventListener("click", async () => { const id = $("rf-id").value || null; const name = $("rf-name").value.trim(); if (!name) return; const permissions = [...document.querySelectorAll("#role-perm-matrix input[data-perm]:checked")].map((el) => el.dataset.perm); const res = await apiCall(API_BASE_URL, id ? "/admin/roles/update" : "/admin/roles/create", id ? { id, name, permissions } : { name, permissions }); if (res.ok) { $("role-modal").classList.remove("open"); loadRoles(); } else { $("role-form-status").textContent = res.error || "Fehler."; $("role-form-status").className = "form-status show error"; } }); /* ---------------- Aktivitätsprotokoll ---------------- */ async function loadAudit() { const res = await apiCall(API_BASE_URL, "/admin/audit/list", { limit: 150 }); if (!res.ok) return; $("audit-list").innerHTML = res.entries .map( (e) => `
${e.actor} — ${e.action}${e.target ? ` (${e.target})` : ""} ${new Date(e.created_at).toLocaleString("de-DE")}
` ) .join("") || `

Noch keine Einträge.

`; } /* ---------------- Login / Logout ---------------- */ document.addEventListener("DOMContentLoaded", () => { setupTabs(); const existing = getSession(); if (existing) showApp(existing); else showGate(); $("code-gate").addEventListener("submit", async (e) => { e.preventDefault(); const status = $("gate-status"); status.className = "form-status show"; status.textContent = "Prüfe Code …"; const res = await doLogin(API_BASE_URL, $("code-input").value); if (res.ok) { status.className = "form-status"; showApp(getSession()); } else { status.textContent = res.error || "Falscher Code."; status.className = "form-status show error"; } }); $("logout-btn").addEventListener("click", async () => { await doLogout(API_BASE_URL); showGate(); }); // Modals per Klick auf Backdrop schließen document.querySelectorAll(".modal-backdrop").forEach((m) => { m.addEventListener("click", (e) => { if (e.target === m) m.classList.remove("open"); }); }); });