diff --git a/assets/css/main.css b/assets/css/main.css
index c199cab..9394c5d 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -260,27 +260,6 @@ header.site-header {
.nav-cta::before, .nav-cta::after { animation: none; }
}
-/* Interner Login — dezent, aber auf jeder Seite oben rechts auffindbar */
-.nav-login {
- display: inline-flex;
- align-items: center;
- gap: .3rem;
- padding: .5rem .9rem;
- border-radius: 999px;
- border: 1px solid var(--border);
- color: var(--text-muted);
- font-weight: 700;
- font-size: .82rem;
- white-space: nowrap;
- transition: color .2s ease, border-color .2s ease, background .2s ease;
-}
-.nav-login:hover,
-.nav-login:focus-visible {
- color: var(--accent);
- border-color: var(--accent);
- background: color-mix(in srgb, var(--accent) 12%, transparent);
-}
-
.nav-toggle {
display: none;
background: none;
@@ -311,7 +290,6 @@ header.site-header {
.nav-dropdown { position: static; transform: none; box-shadow: none; width: 100%; }
.nav-group.open .nav-dropdown { display: flex; }
.nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; }
- .nav-login { align-self: flex-start; }
}
/* ---------- Buttons ---------- */
diff --git a/assets/js/admin-auth.js b/assets/js/admin-auth.js
deleted file mode 100644
index e6b361f..0000000
--- a/assets/js/admin-auth.js
+++ /dev/null
@@ -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 };
-}
diff --git a/assets/js/main.js b/assets/js/main.js
index d72a22d..c585847 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -95,11 +95,6 @@
${NAV.map(renderNavItem).join("")}
Bewerben 🩵
-
-
- 🔒 Login
-
`;
@@ -175,16 +170,11 @@
Bewerben
Kontakt
- Intern 🔒
-
`;
}
@@ -286,14 +276,14 @@
Inhaltsbereich 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
diff --git a/assets/js/postfach.js b/assets/js/postfach.js
deleted file mode 100644
index 46b7c53..0000000
--- a/assets/js/postfach.js
+++ /dev/null
@@ -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 = `Keine Bewerbungen in dieser Ansicht.
`;
- return;
- }
-
- list.innerHTML = applications
- .map((a) => {
- const fields = a.data
- ? Object.entries(a.data).map(([k, v]) => `${k}: ${v}
`).join("")
- : `Keine Berechtigung, den vollen Inhalt zu öffnen.
`;
- const date = new Date(a.createdAt).toLocaleString("de-DE");
-
- const actions = [];
- if (sessionHasPermission(session, "APPLICATIONS_TAKE_OVER")) {
- actions.push(`Übernehmen `);
- }
- if (sessionHasPermission(session, "APPLICATIONS_MARK_ANSWERED")) {
- actions.push(`Als beantwortet markieren `);
- }
- if (sessionHasPermission(session, "APPLICATIONS_ARCHIVE") && !a.archived) {
- actions.push(`Archivieren `);
- }
- if (sessionHasPermission(session, "APPLICATIONS_RESTORE") && a.archived) {
- actions.push(`Wiederherstellen `);
- }
- if (sessionHasPermission(session, "APPLICATIONS_DELETE")) {
- actions.push(`Löschen `);
- }
- if (sessionHasPermission(session, "NOTES_READ")) {
- actions.push(`Notizen `);
- }
-
- const statusSelect = sessionHasPermission(session, "APPLICATIONS_CHANGE_STATUS")
- ? `
- ${STATUS_OPTIONS.map((s) => `${s} `).join("")}
- `
- : `${a.status} `;
-
- return `
-
-
- ${a.type}${a.archived ? " · archiviert" : ""}
- ${statusSelect}
-
-
${date}
-
${fields}
-
${actions.join("")}
-
-
- ${
- sessionHasPermission(session, "NOTES_WRITE")
- ? `
-
- Notiz speichern
-
`
- : ""
- }
-
-
`;
- })
- .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 = `Konnte Notizen nicht laden.
`;
- return;
- }
- box.innerHTML =
- res.notes
- .map((n) => `${n.author} : ${n.text}${new Date(n.created_at).toLocaleString("de-DE")}
`)
- .join("") || `Noch keine Notizen.
`;
-}
-
-/* ---------------- 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(" ");
-}
-
-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);
-});
diff --git a/assets/js/zugaenge.js b/assets/js/zugaenge.js
deleted file mode 100644
index 7eadcb2..0000000
--- a/assets/js/zugaenge.js
+++ /dev/null
@@ -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 = `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(`Freigeben & Code erstellen `);
- }
- if (isOwner && u.status !== "pending") {
- actions.push(`Code anzeigen `);
- actions.push(`Neuer Code `);
- actions.push(`Alle Geräte abmelden `);
- }
- if (canLock && u.status !== "pending") {
- actions.push(
- u.status === "active"
- ? `Sperren `
- : `Entsperren `
- );
- }
- if (canAssignRole) {
- actions.push(`Rolle ändern `);
- }
- if (canEditPerms) {
- actions.push(`Rechte verwalten `);
- }
- if (isOwner) {
- actions.push(`Löschen `);
- }
-
- 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) => `${r.name} `).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 `
-
- ${PERMISSION_LABELS[k]} ${inRole ? "(in Rolle enthalten)" : ""}
-
-
- Rolle (${inRole ? "erlaubt" : "nicht erlaubt"})
- Erlaubt
- Verboten
-
-
- `;
- })
- .join("")}
-
`
- ).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
- ? `
- Bearbeiten
- Löschen
-
`
- : ""
- }
-
`
- )
- .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) => `
-
-
- ${PERMISSION_LABELS[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();
- });
-
- // 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");
- });
- });
-});
diff --git a/postfach.html b/postfach.html
deleted file mode 100644
index 3543bea..0000000
--- a/postfach.html
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-Postfach — DOGFATHER UNIVERSE
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🔒 Intern
-
Bewerbungs-Postfach
-
Nur für Dogfather & ausgewählte Personen mit Code.
-
-
-
-
-
-
-
-
-
-
-
-
-
Live-Status
-
- Läuft automatisch : Das System schaut jede Minute bei TikTok nach,
- ob du gerade sendest. Du musst nichts melden.
-
-
-
Wird geladen …
-
-
-
- 🟢 Von Hand live
- Von Hand offline
-
-
- ↩︎ Zurück zur Automatik
-
-
- Die Handschaltung ist nur ein Notfall-Vorrang (z. B. wenn TikTok mal klemmt)
- und läuft nach 3 Stunden von selbst wieder aus.
-
-
-
-
-
-
-
-
-
-
Eingegangene Bewerbungen
- Aktualisieren
-
-
- Archivierte anzeigen
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/robots.txt b/robots.txt
index 9a65a83..54d3809 100644
--- a/robots.txt
+++ b/robots.txt
@@ -4,7 +4,6 @@
User-agent: *
Allow: /
-Disallow: /postfach.html
Disallow: /cloudflare-worker/
Sitemap: https://REPLACE-WITH-YOUR-DOMAIN.tld/sitemap.xml
diff --git a/zugaenge.html b/zugaenge.html
deleted file mode 100644
index 5ac2766..0000000
--- a/zugaenge.html
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
-
-
-
-
-Zugänge und Teammitglieder — DOGFATHER UNIVERSE
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🔒 Interner Bereich
-
Zugänge und Teammitglieder
-
Rollen, Rechte, Codes — vollständig unter Dogis Kontrolle.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Zugänge
- Rollen
- Mein Zugang
- Aktivitätsprotokoll
-
-
-
-
-
-
Alle Zugänge
-
- Aktualisieren
- + Neuer Zugang
-
-
-
-
-
-
-
-
-
Rollen
- + Neue Rolle
-
-
-
-
-
-
-
Mein Zugangscode
-
- Hier änderst du deinen eigenen Hauptadministrator-Code. Er wird nur als nicht
- umkehrbarer Prüfwert gespeichert — niemand (auch ich nicht) kann ihn auslesen.
- Merk ihn dir deshalb gut.
-
-
-
-
-
-
-
Aktivitätsprotokoll
-
Zugangscodes erscheinen hier niemals im Klartext.
-
-
-
-
-
-
-
-
-
-
Neuer Zugang
-
-
-
Schließen
-
-
-
-
-
-
-
Code anzeigen
-
Aus Sicherheitsgründen bitte deinen Owner-Code erneut eingeben.
-
- Dein Owner-Code
- Code anzeigen
-
-
-
-
Schließen
-
-
-
-
-
-
-
Individuelle Rechte
-
Ausnahmen wirken zusätzlich zur Rolle. „Rolle" = Standardwert der Rolle übernehmen.
-
-
- Speichern
- Abbrechen
-
-
-
-
-
-
-
-
-
Neue Rolle
-
-
Name der Rolle
-
-
- Speichern
- Abbrechen
-
-
-
-
-
-
-
-
-
-
-