Files
dogfather-universe/assets/js/zugaenge.js
T

511 lines
21 KiB
JavaScript

/* =====================================================================
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 = `<p class="muted">Noch keine Zugänge angelegt.</p>`;
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(`<button class="btn btn-primary finalize-btn" data-id="${u.id}">Freigeben &amp; Code erstellen</button>`);
}
if (isOwner && u.status !== "pending") {
actions.push(`<button class="btn btn-outline reveal-btn" data-id="${u.id}">Code anzeigen</button>`);
actions.push(`<button class="btn btn-outline newcode-btn" data-id="${u.id}">Neuer Code</button>`);
actions.push(`<button class="btn btn-outline endsessions-btn" data-id="${u.id}">Alle Geräte abmelden</button>`);
}
if (canLock && u.status !== "pending") {
actions.push(
u.status === "active"
? `<button class="btn btn-outline lock-btn" data-id="${u.id}">Sperren</button>`
: `<button class="btn btn-outline unlock-btn" data-id="${u.id}">Entsperren</button>`
);
}
if (canAssignRole) {
actions.push(`<button class="btn btn-outline role-btn" data-id="${u.id}">Rolle ändern</button>`);
}
if (canEditPerms) {
actions.push(`<button class="btn btn-outline perms-btn" data-id="${u.id}">Rechte verwalten</button>`);
}
if (isOwner) {
actions.push(`<button class="btn btn-outline delete-btn" data-id="${u.id}" style="color:var(--accent);">Löschen</button>`);
}
return `
<div class="user-row">
<div><strong>${u.name}</strong><br><span class="small muted">@${u.username}${u.email ? " · " + u.email : ""}</span></div>
<div>${roleNameById(u.roleId)}</div>
<div><span class="status-pill ${statusClass}">${u.status}</span></div>
<div class="small muted">${u.expiresAt ? new Date(u.expiresAt).toLocaleDateString("de-DE") : "dauerhaft"}</div>
<div class="small muted">${u.lastLoginAt ? new Date(u.lastLoginAt).toLocaleString("de-DE") : "nie"}</div>
<div class="actions">${actions.join("")}</div>
</div>`;
})
.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) => `<option value="${r.id}">${r.name}</option>`).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 = `<strong>Code:</strong> ${res.code}<br><span class="small">Jetzt sichern/weitergeben — wird nirgends mehr angezeigt!</span>`;
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) => `
<div class="perm-group-title">${g.label}</div>
<table class="perm-table"><tbody>
${g.keys
.map((k) => {
const inRole = roleDefaults.includes(k);
return `
<tr>
<td>${PERMISSION_LABELS[k]} <span class="small muted">${inRole ? "(in Rolle enthalten)" : ""}</span></td>
<td>
<select data-perm="${k}" style="background:var(--bg-alt);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:.3rem;">
<option value="inherit">Rolle (${inRole ? "erlaubt" : "nicht erlaubt"})</option>
<option value="allow">Erlaubt</option>
<option value="deny">Verboten</option>
</select>
</td>
</tr>`;
})
.join("")}
</tbody></table>`
).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 = `<p class="muted">Noch keine Rollen erstellt.</p>`;
return;
}
container.innerHTML = currentRoles
.map(
(r) => `
<div class="card">
<h3>${r.name}</h3>
<p class="small muted">${r.permissions.length} Rechte aktiv</p>
${
canManage
? `<div class="btn-row" style="margin-top:.6rem;">
<button class="btn btn-outline edit-role-btn" data-id="${r.id}">Bearbeiten</button>
<button class="btn btn-outline delete-role-btn" data-id="${r.id}" style="color:var(--accent);">Löschen</button>
</div>`
: ""
}
</div>`
)
.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) => `
<div class="perm-group-title">${g.label}</div>
${g.keys
.map(
(k) => `
<label class="small" style="display:flex;gap:.5rem;align-items:center;padding:.25rem 0;">
<input type="checkbox" data-perm="${k}" ${role && role.permissions.includes(k) ? "checked" : ""} />
${PERMISSION_LABELS[k]}
</label>`
)
.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) => `
<div class="audit-row">
<strong>${e.actor}</strong> — ${e.action}${e.target ? ` <span class="small muted">(${e.target})</span>` : ""}
<span class="small muted" style="float:right;">${new Date(e.created_at).toLocaleString("de-DE")}</span>
</div>`
)
.join("") || `<p class="muted">Noch keine Einträge.</p>`;
}
/* ---------------- 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");
});
});
});