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:
2026-08-01 11:04:01 +02:00
parent 888de712de
commit e910017e51
8 changed files with 5 additions and 1269 deletions
-22
View File
@@ -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 ---------- */
-81
View File
@@ -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
View File
@@ -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 &amp; 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 &amp; 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 &amp; Datenschutz</a> · <a href="postfach.html">🔒 Interner Login</a></span>
<span><a href="kontakt.html">Impressum &amp; 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
-266
View File
@@ -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);
});
-543
View File
@@ -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 &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();
});
// 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");
});
});
});
-113
View File
@@ -1,113 +0,0 @@
<!DOCTYPE html>
<html lang="de" data-theme="spicymedia">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow" />
<title>Postfach — DOGFATHER UNIVERSE</title>
<meta name="description" content="Internes Bewerbungs-Postfach — nur mit Code zugänglich." />
<link rel="icon" type="image/png" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/main.css" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" />
<style>
.app-card { display:flex; flex-direction:column; gap:.5rem; }
.app-card .fields p { margin:.15rem 0; }
.app-card .actions { display:flex; flex-wrap:wrap; gap:.4rem; margin-top:.6rem; }
.app-card select.status-select { background:var(--bg-alt); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:.35rem .6rem; font-size:.8rem; }
.notes-box { margin-top:.7rem; padding-top:.7rem; border-top:1px solid var(--border); display:none; }
.notes-box.open { display:block; }
.note-item { font-size:.82rem; padding:.4rem 0; border-bottom:1px solid var(--border); }
.filter-row { display:flex; gap:.6rem; flex-wrap:wrap; margin-bottom:1.2rem; }
</style>
</head>
<body>
<div id="site-header"></div>
<main>
<section class="hero">
<div class="container">
<span class="eyebrow">🔒 Intern</span>
<h1>Bewerbungs-Postfach</h1>
<p class="lead worlds">Nur für Dogfather &amp; ausgewählte Personen mit Code.</p>
</div>
</section>
<!-- ===== Login-Gate ===== -->
<section class="section-tight" id="gate-container">
<div class="container" style="max-width:520px;">
<form id="code-gate" class="app-form" novalidate>
<div class="field">
<label for="code-input">Zugangscode</label>
<input id="code-input" name="code" type="password" autocomplete="off" required />
</div>
<button class="btn btn-primary" type="submit">Postfach öffnen</button>
<div class="form-status" id="gate-status"></div>
</form>
<p class="small muted" style="margin-top:1rem;">
Zugänge, Rollen und Rechte fürs Team verwaltest du unter
<a href="zugaenge.html">Zugänge &amp; Teammitglieder →</a>
</p>
</div>
</section>
<!-- ===== Session-Leiste ===== -->
<section class="section-tight" id="session-bar" style="display:none;padding-top:0;">
<div class="container" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;">
<span class="badge rank-1" id="role-badge">Eingeloggt</span>
<div class="btn-row" style="margin:0;">
<a class="btn btn-outline" href="zugaenge.html" id="team-link" style="display:none;">Zugänge &amp; Team</a>
<button class="btn btn-outline" id="logout-btn" type="button">Abmelden</button>
</div>
</div>
</section>
<!-- ===== Live-Status (nur Owner) ===== -->
<section class="section-tight" id="live-toggle-section" style="display:none;">
<div class="container text-center">
<h2>Live-Status</h2>
<p class="lead" style="max-width:56ch;margin:0 auto;">
Läuft <strong>automatisch</strong>: Das System schaut jede Minute bei TikTok nach,
ob du gerade sendest. Du musst nichts melden.
</p>
<div class="card" style="max-width:420px;margin:1rem auto 0;">
<span class="badge rank-1"><span class="live-dot is-unknown" id="admin-live-dot"></span><span id="admin-live-text">Wird geladen …</span></span>
<p class="small muted" style="margin:.7rem 0 0;" id="admin-live-detail"></p>
<div class="btn-row" style="justify-content:center;margin-top:1rem;">
<button class="btn btn-primary" id="go-live-btn" type="button">🟢 Von Hand live</button>
<button class="btn btn-outline" id="go-offline-btn" type="button">Von Hand offline</button>
</div>
<button class="btn btn-outline" id="go-auto-btn" type="button" style="margin-top:.6rem;display:none;">
↩︎ Zurück zur Automatik
</button>
<p class="small muted" style="margin:.8rem 0 0;">
Die Handschaltung ist nur ein Notfall-Vorrang (z.&nbsp;B. wenn TikTok mal klemmt)
und läuft nach 3 Stunden von selbst wieder aus.
</p>
</div>
</div>
</section>
<!-- ===== Mailbox ===== -->
<section class="section-tight" id="inbox-section" style="display:none;">
<div class="container">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;margin-bottom:1rem;">
<h2 style="margin:0;">Eingegangene Bewerbungen</h2>
<button class="btn btn-outline" id="refresh-btn" type="button">Aktualisieren</button>
</div>
<div class="filter-row">
<label class="small"><input type="checkbox" id="show-archived" /> Archivierte anzeigen</label>
</div>
<div id="inbox-list" class="grid grid-2"></div>
</div>
</section>
</main>
<div id="site-footer"></div>
<script src="assets/js/main.js"></script>
<script src="assets/js/admin-auth.js"></script>
<script src="assets/js/postfach.js"></script>
</body>
</html>
-1
View File
@@ -4,7 +4,6 @@
User-agent: *
Allow: /
Disallow: /postfach.html
Disallow: /cloudflare-worker/
Sitemap: https://REPLACE-WITH-YOUR-DOMAIN.tld/sitemap.xml
-228
View File
@@ -1,228 +0,0 @@
<!DOCTYPE html>
<html lang="de" data-theme="spicymedia">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex, nofollow" />
<title>Zugänge und Teammitglieder — DOGFATHER UNIVERSE</title>
<meta name="description" content="Interner Bereich: Zugänge, Rollen und Rechte verwalten." />
<link rel="icon" type="image/png" href="assets/img/favicon.png" />
<link rel="stylesheet" href="assets/css/main.css" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" />
<style>
.perm-table { width:100%; border-collapse:collapse; font-size:.85rem; }
.perm-table th, .perm-table td { text-align:left; padding:.5rem .6rem; border-bottom:1px solid var(--border); }
.perm-group-title { font-weight:700; color:var(--accent); margin:1.2rem 0 .4rem; font-size:.8rem; text-transform:uppercase; letter-spacing:.08em; }
.user-row { display:grid; grid-template-columns: 1.4fr 1fr 1fr .8fr 1fr; gap:.6rem; align-items:center; padding:.8rem 0; border-bottom:1px solid var(--border); font-size:.88rem; }
.user-row .actions { display:flex; flex-wrap:wrap; gap:.4rem; grid-column:1/-1; margin-top:.4rem; }
.status-pill { display:inline-block; padding:.15rem .55rem; border-radius:999px; font-size:.7rem; font-weight:700; text-transform:uppercase; }
.status-pill.active { background:rgba(34,197,94,.15); color:#22c55e; }
.status-pill.locked { background:rgba(217,74,92,.15); color:var(--accent); }
.status-pill.pending { background:rgba(232,147,58,.15); color:var(--accent-2); }
.modal-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.7); display:none; align-items:center; justify-content:center; z-index:300; padding:5vh 4vw; }
.modal-backdrop.open { display:flex; }
.modal-box { background:var(--card-bg); border:1px solid var(--border); border-radius:var(--radius); padding:1.6rem; max-width:640px; width:100%; max-height:88vh; overflow:auto; }
.code-reveal-box { font-family:monospace; font-size:1.3rem; letter-spacing:.05em; background:var(--bg-alt); border:1px dashed var(--accent); border-radius:8px; padding:.8rem 1rem; margin:.6rem 0; text-align:center; }
.audit-row { padding:.6rem 0; border-bottom:1px solid var(--border); font-size:.85rem; }
</style>
</head>
<body>
<div id="site-header"></div>
<main>
<section class="hero">
<div class="container">
<span class="eyebrow">🔒 Interner Bereich</span>
<h1>Zugänge und Teammitglieder</h1>
<p class="lead worlds">Rollen, Rechte, Codes — vollständig unter Dogis Kontrolle.</p>
</div>
</section>
<!-- ===== Login-Gate ===== -->
<section class="section-tight" id="gate-container">
<div class="container" style="max-width:480px;">
<form id="code-gate" class="app-form" novalidate>
<div class="field">
<label for="code-input">Zugangscode</label>
<input id="code-input" name="code" type="password" autocomplete="off" required />
</div>
<button class="btn btn-primary" type="submit">Anmelden</button>
<div class="form-status" id="gate-status"></div>
</form>
<p class="small muted" style="margin-top:1rem;">
<a href="postfach.html">← Zurück zum Postfach</a>
</p>
</div>
</section>
<!-- ===== Session-Leiste ===== -->
<section class="section-tight" id="session-bar" style="display:none;padding-top:0;">
<div class="container" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;">
<span class="badge rank-1" id="role-badge">Eingeloggt</span>
<div class="btn-row" style="margin:0;">
<a class="btn btn-outline" href="postfach.html">Zum Postfach</a>
<button class="btn btn-outline" id="logout-btn" type="button">Abmelden</button>
</div>
</div>
</section>
<!-- ===== Hauptbereich ===== -->
<section id="main-area" style="display:none;">
<div class="container">
<div class="tabs">
<button class="tab-btn active" data-tab="tab-users">Zugänge</button>
<button class="tab-btn" data-tab="tab-roles">Rollen</button>
<button class="tab-btn" data-tab="tab-mycode">Mein Zugang</button>
<button class="tab-btn" data-tab="tab-audit">Aktivitätsprotokoll</button>
</div>
<!-- ---- Tab: Zugänge ---- -->
<div class="tab-panel active" id="tab-users">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;margin-bottom:1rem;">
<h2 style="margin:0;">Alle Zugänge</h2>
<div class="btn-row" style="margin:0;">
<button class="btn btn-outline" id="refresh-users-btn" type="button">Aktualisieren</button>
<button class="btn btn-primary" id="new-user-btn" type="button">+ Neuer Zugang</button>
</div>
</div>
<div id="users-list"></div>
</div>
<!-- ---- Tab: Rollen ---- -->
<div class="tab-panel" id="tab-roles">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem;margin-bottom:1rem;">
<h2 style="margin:0;">Rollen</h2>
<button class="btn btn-primary" id="new-role-btn" type="button">+ Neue Rolle</button>
</div>
<div id="roles-list" class="grid grid-2"></div>
</div>
<!-- ---- Tab: Mein Zugang (nur Owner) ---- -->
<div class="tab-panel" id="tab-mycode">
<h2>Mein Zugangscode</h2>
<p class="small muted" style="max-width:60ch;">
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.
</p>
<form id="owner-code-form" class="app-form" style="max-width:420px;margin-top:1.2rem;" novalidate>
<div class="field">
<label for="oc-current">Aktueller Code</label>
<input id="oc-current" type="password" autocomplete="off" required />
</div>
<div class="field">
<label for="oc-new">Neuer Code (mindestens 10 Zeichen)</label>
<input id="oc-new" type="password" autocomplete="off" required minlength="10" />
</div>
<div class="field">
<label for="oc-new2">Neuer Code wiederholen</label>
<input id="oc-new2" type="password" autocomplete="off" required minlength="10" />
</div>
<button class="btn btn-primary" type="submit">Code ändern</button>
<div class="form-status" id="owner-code-status"></div>
</form>
</div>
<!-- ---- Tab: Aktivitätsprotokoll ---- -->
<div class="tab-panel" id="tab-audit">
<h2>Aktivitätsprotokoll</h2>
<p class="small muted">Zugangscodes erscheinen hier niemals im Klartext.</p>
<div id="audit-list"></div>
</div>
</div>
</section>
</main>
<!-- ===== Modal: Neuer/Bearbeiten Zugang ===== -->
<div class="modal-backdrop" id="user-modal">
<div class="modal-box">
<h2 id="user-modal-title">Neuer Zugang</h2>
<form id="user-form" class="app-form" novalidate>
<input type="hidden" id="uf-pending-id" />
<div class="field"><label for="uf-name">Name</label><input id="uf-name" required /></div>
<div class="field"><label for="uf-username">Benutzername</label><input id="uf-username" required /></div>
<div class="field"><label for="uf-email">E-Mail (optional)</label><input id="uf-email" type="email" /></div>
<div class="field">
<label for="uf-role">Rolle</label>
<select id="uf-role"></select>
</div>
<div class="field" id="uf-code-field">
<label>Zugangscode</label>
<div class="btn-row" style="margin:.3rem 0;">
<label class="small"><input type="radio" name="uf-code-mode" value="auto" checked /> Automatisch sicher erstellen</label>
<label class="small"><input type="radio" name="uf-code-mode" value="manual" /> Selbst festlegen</label>
</div>
<input id="uf-code-manual" placeholder="eigener Code" style="display:none;" />
</div>
<div class="field">
<label for="uf-expiry">Gültigkeit</label>
<select id="uf-expiry">
<option value="">Dauerhaft</option>
<option value="7">7 Tage</option>
<option value="30">30 Tage</option>
<option value="90">90 Tage</option>
<option value="date">Bestimmtes Datum…</option>
</select>
<input id="uf-expiry-date" type="date" style="display:none;margin-top:.5rem;" />
</div>
<div class="field"><label for="uf-note">Interne Notiz</label><textarea id="uf-note"></textarea></div>
<button class="btn btn-primary" type="submit" id="uf-submit-btn">Zugang erstellen</button>
<div class="form-status" id="user-form-status"></div>
</form>
<div class="code-reveal-box" id="uf-new-code" style="display:none;"></div>
<button class="btn btn-outline" id="user-modal-close" type="button" style="margin-top:1rem;">Schließen</button>
</div>
</div>
<!-- ===== Modal: Code anzeigen (Re-Auth) ===== -->
<div class="modal-backdrop" id="reveal-modal">
<div class="modal-box" style="max-width:420px;">
<h2>Code anzeigen</h2>
<p class="small muted">Aus Sicherheitsgründen bitte deinen Owner-Code erneut eingeben.</p>
<form id="reveal-form" class="app-form" novalidate>
<div class="field"><label for="reveal-reauth">Dein Owner-Code</label><input id="reveal-reauth" type="password" required /></div>
<button class="btn btn-primary" type="submit">Code anzeigen</button>
<div class="form-status" id="reveal-status"></div>
</form>
<div class="code-reveal-box" id="reveal-code-box" style="display:none;"></div>
<button class="btn btn-outline" id="reveal-modal-close" type="button" style="margin-top:1rem;">Schließen</button>
</div>
</div>
<!-- ===== Modal: Rechte pro Person ===== -->
<div class="modal-backdrop" id="perm-modal">
<div class="modal-box">
<h2>Individuelle Rechte</h2>
<p class="small muted">Ausnahmen wirken zusätzlich zur Rolle. „Rolle" = Standardwert der Rolle übernehmen.</p>
<div id="perm-matrix"></div>
<div class="btn-row">
<button class="btn btn-primary" id="perm-save-btn" type="button">Speichern</button>
<button class="btn btn-outline" id="perm-modal-close" type="button">Abbrechen</button>
</div>
<div class="form-status" id="perm-status"></div>
</div>
</div>
<!-- ===== Modal: Rolle erstellen/bearbeiten ===== -->
<div class="modal-backdrop" id="role-modal">
<div class="modal-box">
<h2 id="role-modal-title">Neue Rolle</h2>
<input type="hidden" id="rf-id" />
<div class="field"><label for="rf-name">Name der Rolle</label><input id="rf-name" required /></div>
<div id="role-perm-matrix" style="margin-top:1rem;"></div>
<div class="btn-row">
<button class="btn btn-primary" id="role-save-btn" type="button">Speichern</button>
<button class="btn btn-outline" id="role-modal-close" type="button">Abbrechen</button>
</div>
<div class="form-status" id="role-form-status"></div>
</div>
</div>
<div id="site-footer"></div>
<script src="assets/js/main.js"></script>
<script src="assets/js/admin-auth.js"></script>
<script src="assets/js/zugaenge.js"></script>
</body>
</html>