Interne Bewerbungs-Rückseiten (Postfach, Zugänge, Login) komplett entfernt
Auf Wunsch: die öffentlichen Bewerbungsseiten (bewerben*.html) bleiben unverändert bestehen, aber die internen "Rückseiten", wo die Bewerbungen verwaltet wurden, werden nicht mehr gebraucht und sind komplett raus: - postfach.html (Bewerbungs-Postfach) und zugaenge.html (Zugänge & Rollen) gelöscht - zugehörige tote Skripte gelöscht: postfach.js, zugaenge.js, admin-auth.js - "Login"-Knopf in der Navigation entfernt - "Intern"-Block im Footer (Bewerbungs-Postfach / Zugänge & Rollen) entfernt - KEIN_NAHTLOSER_WECHSEL-Ausnahmeliste geleert (referenzierte nur die jetzt gelöschten Seiten) - .nav-login CSS-Regeln entfernt (totes CSS) - robots.txt: Disallow-Zeile für /postfach.html entfernt Der Cloudflare-Worker (Backend/D1) und die Bewerbungsformulare selbst bleiben unangetastet — Bewerbungen können weiterhin eingereicht werden.
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
/* =====================================================================
|
||||
admin-auth.js — gemeinsame Session-Logik für postfach.html und
|
||||
zugaenge.html. Session (Token + Rolle/Rechte) liegt in sessionStorage
|
||||
(pro Tab, verschwindet beim Schließen) — der Server erzwingt zusätzlich
|
||||
Inaktivitäts-Timeout (30 Min) und eine absolute Höchstdauer (12 Std).
|
||||
===================================================================== */
|
||||
|
||||
const SESSION_KEY = "dogfather_admin_session";
|
||||
|
||||
function getSession() {
|
||||
try {
|
||||
return JSON.parse(sessionStorage.getItem(SESSION_KEY) || "null");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setSession(data) {
|
||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify(data));
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
}
|
||||
|
||||
/** Prüft, ob die aktuelle Session (Owner oder Rolle+Overrides) ein Recht hat. */
|
||||
function sessionHasPermission(session, key) {
|
||||
if (!session) return false;
|
||||
if (session.isOwner) return true;
|
||||
const overrides = session.overrides || {};
|
||||
if (Object.prototype.hasOwnProperty.call(overrides, key)) return !!overrides[key];
|
||||
return Array.isArray(session.permissions) && session.permissions.includes(key);
|
||||
}
|
||||
|
||||
async function apiCall(apiBaseUrl, path, body, { auth = true } = {}) {
|
||||
const session = getSession();
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (auth && session?.token) headers["Authorization"] = `Bearer ${session.token}`;
|
||||
|
||||
const res = await fetch(`${apiBaseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body || {}),
|
||||
});
|
||||
let data;
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch {
|
||||
data = { ok: false, error: "Ungültige Antwort vom Server." };
|
||||
}
|
||||
if (res.status === 401 && auth) {
|
||||
clearSession(); // Session ist abgelaufen/ungültig -> zurück zum Login zwingen
|
||||
}
|
||||
return { httpOk: res.ok, ...data };
|
||||
}
|
||||
|
||||
async function doLogin(apiBaseUrl, code) {
|
||||
const res = await apiCall(apiBaseUrl, "/auth/login", { code }, { auth: false });
|
||||
if (res.ok) {
|
||||
setSession({
|
||||
token: res.token,
|
||||
isOwner: res.isOwner,
|
||||
name: res.name,
|
||||
username: res.username,
|
||||
roleName: res.roleName,
|
||||
permissions: res.permissions || [],
|
||||
overrides: res.overrides || {},
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function doLogout(apiBaseUrl) {
|
||||
await apiCall(apiBaseUrl, "/auth/logout", {});
|
||||
clearSession();
|
||||
}
|
||||
|
||||
// Für Einbindung ohne Modul-Bundler global verfügbar machen.
|
||||
if (typeof window !== "undefined") {
|
||||
window.AdminAuth = { getSession, setSession, clearSession, sessionHasPermission, apiCall, doLogin, doLogout };
|
||||
}
|
||||
+5
-15
@@ -95,11 +95,6 @@
|
||||
<nav class="nav-links">
|
||||
${NAV.map(renderNavItem).join("")}
|
||||
<a class="nav-cta" href="bewerben.html"><span>Bewerben 🩵</span></a>
|
||||
<!-- Interner Einstieg: klein, aber auf JEDER Seite oben sichtbar,
|
||||
damit Dogi und das Team nie danach suchen müssen. -->
|
||||
<a class="nav-login" href="postfach.html" title="Interner Login — Postfach & Zugänge">
|
||||
<span aria-hidden="true">🔒</span> Login
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>`;
|
||||
@@ -175,16 +170,11 @@
|
||||
<li><a href="bewerben.html">Bewerben</a></li>
|
||||
<li><a href="kontakt.html">Kontakt</a></li>
|
||||
</ul>
|
||||
<h4 style="margin-top:1.4rem;">Intern 🔒</h4>
|
||||
<ul>
|
||||
<li><a href="postfach.html">Bewerbungs-Postfach</a></li>
|
||||
<li><a href="zugaenge.html">Zugänge & Rollen</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container footer-bottom">
|
||||
<span>© ${year} Dogfather Universe. Team Dogi ist Familie 🩵</span>
|
||||
<span><a href="kontakt.html">Impressum & Datenschutz</a> · <a href="postfach.html">🔒 Interner Login</a></span>
|
||||
<span><a href="kontakt.html">Impressum & Datenschutz</a></span>
|
||||
</div>
|
||||
</footer>`;
|
||||
}
|
||||
@@ -286,14 +276,14 @@
|
||||
Inhaltsbereich <main> aus. Kopfzeile, Fußzeile und der Musik-Knopf
|
||||
bleiben stehen — die Musik läuft einfach weiter.
|
||||
|
||||
Wichtig für die Sicherheit: Die internen Seiten (Postfach, Zugänge)
|
||||
sind bewusst AUSGENOMMEN und laden weiterhin ganz normal. An Login,
|
||||
Sitzung und Rechten ändert sich dadurch nichts.
|
||||
Die Liste unten kann bei Bedarf wieder Seiten aufnehmen, die bewusst
|
||||
NICHT nahtlos geladen werden sollen (z.B. künftige interne Bereiche
|
||||
mit eigenem Login) — aktuell gibt es keine solche Seite mehr.
|
||||
|
||||
Absicherung: Geht beim Holen irgendetwas schief, wird ganz normal
|
||||
zur Zielseite navigiert. Schlimmstenfalls ist es also wie vorher.
|
||||
=================================================================== */
|
||||
const KEIN_NAHTLOSER_WECHSEL = ["postfach.html", "zugaenge.html"];
|
||||
const KEIN_NAHTLOSER_WECHSEL = [];
|
||||
|
||||
/* Seiteneigene Skripte melden sich fast alle über "DOMContentLoaded" an.
|
||||
Nach dem ersten Laden ist dieses Ereignis längst vorbei — ein später
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
/* =====================================================================
|
||||
postfach.js — Logik für postfach.html
|
||||
===================================================================== */
|
||||
|
||||
const API_BASE_URL = "https://dogfather-universe-postfach.dogfather1608.workers.dev";
|
||||
|
||||
const { getSession, sessionHasPermission, apiCall, doLogin, doLogout } = window.AdminAuth;
|
||||
|
||||
const STATUS_OPTIONS = ["neu", "in_bearbeitung", "beantwortet"];
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function showApp(session) {
|
||||
$("gate-container").style.display = "none";
|
||||
$("session-bar").style.display = "block";
|
||||
$("inbox-section").style.display = "block";
|
||||
$("role-badge").textContent = session.isOwner ? "Eingeloggt als: Dogi (Owner)" : `Eingeloggt als: ${session.name} (${session.roleName})`;
|
||||
|
||||
const canSeeTeam = session.isOwner || sessionHasPermission(session, "TEAM_VIEW");
|
||||
$("team-link").style.display = canSeeTeam ? "inline-flex" : "none";
|
||||
|
||||
if (session.isOwner) {
|
||||
$("live-toggle-section").style.display = "block";
|
||||
loadLiveStatus();
|
||||
}
|
||||
|
||||
loadInbox();
|
||||
}
|
||||
|
||||
function showGate() {
|
||||
$("gate-container").style.display = "block";
|
||||
$("session-bar").style.display = "none";
|
||||
$("live-toggle-section").style.display = "none";
|
||||
$("inbox-section").style.display = "none";
|
||||
}
|
||||
|
||||
/* ---------------- Bewerbungen ---------------- */
|
||||
async function loadInbox() {
|
||||
const session = getSession();
|
||||
const includeArchived = $("show-archived").checked;
|
||||
const res = await apiCall(API_BASE_URL, "/admin/applications/list", { includeArchived });
|
||||
if (!res.ok) {
|
||||
if (!getSession()) showGate();
|
||||
return;
|
||||
}
|
||||
renderInbox(res.applications, session);
|
||||
}
|
||||
|
||||
function renderInbox(applications, session) {
|
||||
const list = $("inbox-list");
|
||||
if (!applications.length) {
|
||||
list.innerHTML = `<p class="muted">Keine Bewerbungen in dieser Ansicht.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = applications
|
||||
.map((a) => {
|
||||
const fields = a.data
|
||||
? Object.entries(a.data).map(([k, v]) => `<p class="small"><strong>${k}:</strong> ${v}</p>`).join("")
|
||||
: `<p class="small muted">Keine Berechtigung, den vollen Inhalt zu öffnen.</p>`;
|
||||
const date = new Date(a.createdAt).toLocaleString("de-DE");
|
||||
|
||||
const actions = [];
|
||||
if (sessionHasPermission(session, "APPLICATIONS_TAKE_OVER")) {
|
||||
actions.push(`<button class="btn btn-outline take-btn" data-id="${a.id}">Übernehmen</button>`);
|
||||
}
|
||||
if (sessionHasPermission(session, "APPLICATIONS_MARK_ANSWERED")) {
|
||||
actions.push(`<button class="btn btn-outline answered-btn" data-id="${a.id}">Als beantwortet markieren</button>`);
|
||||
}
|
||||
if (sessionHasPermission(session, "APPLICATIONS_ARCHIVE") && !a.archived) {
|
||||
actions.push(`<button class="btn btn-outline archive-btn" data-id="${a.id}">Archivieren</button>`);
|
||||
}
|
||||
if (sessionHasPermission(session, "APPLICATIONS_RESTORE") && a.archived) {
|
||||
actions.push(`<button class="btn btn-outline restore-btn" data-id="${a.id}">Wiederherstellen</button>`);
|
||||
}
|
||||
if (sessionHasPermission(session, "APPLICATIONS_DELETE")) {
|
||||
actions.push(`<button class="btn btn-outline delete-btn" data-id="${a.id}" style="color:var(--accent);">Löschen</button>`);
|
||||
}
|
||||
if (sessionHasPermission(session, "NOTES_READ")) {
|
||||
actions.push(`<button class="btn btn-outline notes-toggle-btn" data-id="${a.id}">Notizen</button>`);
|
||||
}
|
||||
|
||||
const statusSelect = sessionHasPermission(session, "APPLICATIONS_CHANGE_STATUS")
|
||||
? `<select class="status-select" data-id="${a.id}">
|
||||
${STATUS_OPTIONS.map((s) => `<option value="${s}" ${s === a.status ? "selected" : ""}>${s}</option>`).join("")}
|
||||
</select>`
|
||||
: `<span class="tag">${a.status}</span>`;
|
||||
|
||||
return `
|
||||
<div class="card app-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span class="tag">${a.type}${a.archived ? " · archiviert" : ""}</span>
|
||||
${statusSelect}
|
||||
</div>
|
||||
<p class="small muted" style="margin:0;">${date}</p>
|
||||
<div class="fields">${fields}</div>
|
||||
<div class="actions">${actions.join("")}</div>
|
||||
<div class="notes-box" id="notes-${a.id}">
|
||||
<div class="notes-list" id="notes-list-${a.id}"><p class="small muted">Lädt…</p></div>
|
||||
${
|
||||
sessionHasPermission(session, "NOTES_WRITE")
|
||||
? `<div class="field" style="margin-top:.6rem;">
|
||||
<textarea class="note-input" data-id="${a.id}" placeholder="Neue Notiz…" style="min-height:60px;"></textarea>
|
||||
<button class="btn btn-outline add-note-btn" data-id="${a.id}" style="margin-top:.4rem;">Notiz speichern</button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
list.querySelectorAll(".take-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/take-over", b.dataset.id)));
|
||||
list.querySelectorAll(".answered-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/mark-answered", b.dataset.id)));
|
||||
list.querySelectorAll(".archive-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/archive", b.dataset.id)));
|
||||
list.querySelectorAll(".restore-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/restore", b.dataset.id)));
|
||||
list.querySelectorAll(".delete-btn").forEach((b) =>
|
||||
b.addEventListener("click", () => {
|
||||
if (confirm("Diese Bewerbung wirklich endgültig löschen?")) appAction("/admin/applications/delete", b.dataset.id);
|
||||
})
|
||||
);
|
||||
list.querySelectorAll(".status-select").forEach((sel) =>
|
||||
sel.addEventListener("change", () => appAction("/admin/applications/status", sel.dataset.id, { status: sel.value }, false))
|
||||
);
|
||||
list.querySelectorAll(".notes-toggle-btn").forEach((b) =>
|
||||
b.addEventListener("click", () => {
|
||||
const box = $(`notes-${b.dataset.id}`);
|
||||
box.classList.toggle("open");
|
||||
if (box.classList.contains("open")) loadNotes(b.dataset.id);
|
||||
})
|
||||
);
|
||||
list.querySelectorAll(".add-note-btn").forEach((b) =>
|
||||
b.addEventListener("click", async () => {
|
||||
const textarea = document.querySelector(`.note-input[data-id="${b.dataset.id}"]`);
|
||||
const text = textarea.value.trim();
|
||||
if (!text) return;
|
||||
const res = await apiCall(API_BASE_URL, "/admin/notes/add", { applicationId: b.dataset.id, text });
|
||||
if (res.ok) {
|
||||
textarea.value = "";
|
||||
loadNotes(b.dataset.id);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function appAction(path, id, extra, reload = true) {
|
||||
const res = await apiCall(API_BASE_URL, path, { id, ...(extra || {}) });
|
||||
if (res.ok && reload) loadInbox();
|
||||
else if (!res.ok) alert(res.error || "Fehler.");
|
||||
}
|
||||
|
||||
async function loadNotes(applicationId) {
|
||||
const box = $(`notes-list-${applicationId}`);
|
||||
const res = await apiCall(API_BASE_URL, "/admin/notes/list", { applicationId });
|
||||
if (!res.ok) {
|
||||
box.innerHTML = `<p class="small muted">Konnte Notizen nicht laden.</p>`;
|
||||
return;
|
||||
}
|
||||
box.innerHTML =
|
||||
res.notes
|
||||
.map((n) => `<div class="note-item"><strong>${n.author}</strong>: ${n.text}<br><span class="small muted">${new Date(n.created_at).toLocaleString("de-DE")}</span></div>`)
|
||||
.join("") || `<p class="small muted">Noch keine Notizen.</p>`;
|
||||
}
|
||||
|
||||
/* ---------------- Live-Status ----------------
|
||||
Der Status kommt automatisch von TikTok (Cron im Worker, jede Minute).
|
||||
Die Handschaltung ist nur ein zeitlich begrenzter Vorrang.
|
||||
------------------------------------------------------------------- */
|
||||
function formatSince(iso) {
|
||||
if (!iso) return null;
|
||||
const diffMin = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
||||
if (diffMin < 1) return "gerade eben";
|
||||
if (diffMin === 1) return "vor 1 Minute";
|
||||
if (diffMin < 60) return `vor ${diffMin} Minuten`;
|
||||
const h = Math.floor(diffMin / 60);
|
||||
return h === 1 ? "vor 1 Stunde" : `vor ${h} Stunden`;
|
||||
}
|
||||
|
||||
function renderLiveStatus(data) {
|
||||
const live = !!(data && data.live);
|
||||
const dot = $("admin-live-dot");
|
||||
dot.classList.remove("is-unknown");
|
||||
dot.classList.toggle("is-live", live);
|
||||
$("admin-live-text").textContent = live ? "Jetzt live" : "Gerade offline";
|
||||
|
||||
const manual = data && data.source === "manuell";
|
||||
$("go-auto-btn").style.display = manual ? "inline-flex" : "none";
|
||||
|
||||
const lines = [];
|
||||
if (manual) {
|
||||
lines.push("⚠️ Von Hand gesetzt — die Automatik ist gerade übersteuert.");
|
||||
if (data.overrideUntil) {
|
||||
const until = new Date(data.overrideUntil);
|
||||
lines.push(`Läuft aus um ${until.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr.`);
|
||||
}
|
||||
if (data.autoLive !== null && data.autoLive !== undefined) {
|
||||
lines.push(`TikTok meldet gerade: ${data.autoLive ? "live" : "offline"}.`);
|
||||
}
|
||||
} else {
|
||||
if (data && data.title && live) lines.push(`Titel: ${data.title}`);
|
||||
const since = formatSince(data && data.autoCheckedAt);
|
||||
if (since) lines.push(`Automatisch geprüft ${since}.`);
|
||||
if (data && data.autoHealthy === false) {
|
||||
lines.push("⚠️ Der letzte Abruf bei TikTok hat nicht geklappt — angezeigt wird der letzte bekannte Stand.");
|
||||
}
|
||||
}
|
||||
$("admin-live-detail").innerHTML = lines.join("<br>");
|
||||
}
|
||||
|
||||
async function loadLiveStatus() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/live-status`, { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
if (res.ok && data.ok) renderLiveStatus(data);
|
||||
} catch {
|
||||
/* keine Blockade */
|
||||
}
|
||||
}
|
||||
|
||||
async function setLiveStatus(live) {
|
||||
const res = await apiCall(API_BASE_URL, "/live-status/set", { live });
|
||||
if (res.ok) renderLiveStatus(res);
|
||||
}
|
||||
|
||||
async function backToAutoLive() {
|
||||
const res = await apiCall(API_BASE_URL, "/live-status/set", { auto: true });
|
||||
if (res.ok) renderLiveStatus(res);
|
||||
}
|
||||
|
||||
/* ---------------- Login / Logout ---------------- */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
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();
|
||||
});
|
||||
|
||||
$("refresh-btn").addEventListener("click", loadInbox);
|
||||
$("show-archived").addEventListener("change", loadInbox);
|
||||
$("go-live-btn").addEventListener("click", () => setLiveStatus(true));
|
||||
$("go-offline-btn").addEventListener("click", () => setLiveStatus(false));
|
||||
$("go-auto-btn").addEventListener("click", backToAutoLive);
|
||||
|
||||
// Anzeige alle 30 Sekunden auffrischen, damit man die Automatik arbeiten sieht.
|
||||
setInterval(() => {
|
||||
if (!document.hidden && getSession()) loadLiveStatus();
|
||||
}, 30000);
|
||||
});
|
||||
@@ -1,543 +0,0 @@
|
||||
/* =====================================================================
|
||||
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";
|
||||
|
||||
// "Mein Zugang" (eigenen Owner-Code ändern) gibt es nur für Dogi selbst.
|
||||
const myCodeTabBtn = document.querySelector('[data-tab="tab-mycode"]');
|
||||
if (myCodeTabBtn) myCodeTabBtn.style.display = session.isOwner ? "" : "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 & 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();
|
||||
});
|
||||
|
||||
// Eigenen Hauptadministrator-Code ändern
|
||||
$("owner-code-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const status = $("owner-code-status");
|
||||
const currentCode = $("oc-current").value;
|
||||
const newCode = $("oc-new").value;
|
||||
const repeat = $("oc-new2").value;
|
||||
|
||||
const fail = (msg) => {
|
||||
status.textContent = msg;
|
||||
status.className = "form-status show error";
|
||||
};
|
||||
|
||||
if (newCode !== repeat) return fail("Die beiden neuen Codes sind nicht gleich.");
|
||||
if (newCode.length < 10) return fail("Der neue Code muss mindestens 10 Zeichen haben.");
|
||||
|
||||
status.textContent = "Wird geändert …";
|
||||
status.className = "form-status show";
|
||||
|
||||
const res = await apiCall(API_BASE_URL, "/admin/owner/set-code", { currentCode, newCode });
|
||||
if (res.ok) {
|
||||
status.textContent = "Erledigt — ab sofort gilt dein neuer Code. Gut merken! 🩵";
|
||||
status.className = "form-status show success";
|
||||
$("owner-code-form").reset();
|
||||
} else {
|
||||
fail(res.error || "Hat nicht geklappt.");
|
||||
}
|
||||
});
|
||||
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user