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:
@@ -260,27 +260,6 @@ header.site-header {
|
|||||||
.nav-cta::before, .nav-cta::after { animation: none; }
|
.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 {
|
.nav-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -311,7 +290,6 @@ header.site-header {
|
|||||||
.nav-dropdown { position: static; transform: none; box-shadow: none; width: 100%; }
|
.nav-dropdown { position: static; transform: none; box-shadow: none; width: 100%; }
|
||||||
.nav-group.open .nav-dropdown { display: flex; }
|
.nav-group.open .nav-dropdown { display: flex; }
|
||||||
.nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; }
|
.nav-cta { align-self: flex-start; margin: 1rem 0 .6rem; }
|
||||||
.nav-login { align-self: flex-start; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Buttons ---------- */
|
/* ---------- Buttons ---------- */
|
||||||
|
|||||||
@@ -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 class="nav-links">
|
||||||
${NAV.map(renderNavItem).join("")}
|
${NAV.map(renderNavItem).join("")}
|
||||||
<a class="nav-cta" href="bewerben.html"><span>Bewerben 🩵</span></a>
|
<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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>`;
|
</header>`;
|
||||||
@@ -175,16 +170,11 @@
|
|||||||
<li><a href="bewerben.html">Bewerben</a></li>
|
<li><a href="bewerben.html">Bewerben</a></li>
|
||||||
<li><a href="kontakt.html">Kontakt</a></li>
|
<li><a href="kontakt.html">Kontakt</a></li>
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
<div class="container footer-bottom">
|
<div class="container footer-bottom">
|
||||||
<span>© ${year} Dogfather Universe. Team Dogi ist Familie 🩵</span>
|
<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>
|
</div>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
@@ -286,14 +276,14 @@
|
|||||||
Inhaltsbereich <main> aus. Kopfzeile, Fußzeile und der Musik-Knopf
|
Inhaltsbereich <main> aus. Kopfzeile, Fußzeile und der Musik-Knopf
|
||||||
bleiben stehen — die Musik läuft einfach weiter.
|
bleiben stehen — die Musik läuft einfach weiter.
|
||||||
|
|
||||||
Wichtig für die Sicherheit: Die internen Seiten (Postfach, Zugänge)
|
Die Liste unten kann bei Bedarf wieder Seiten aufnehmen, die bewusst
|
||||||
sind bewusst AUSGENOMMEN und laden weiterhin ganz normal. An Login,
|
NICHT nahtlos geladen werden sollen (z.B. künftige interne Bereiche
|
||||||
Sitzung und Rechten ändert sich dadurch nichts.
|
mit eigenem Login) — aktuell gibt es keine solche Seite mehr.
|
||||||
|
|
||||||
Absicherung: Geht beim Holen irgendetwas schief, wird ganz normal
|
Absicherung: Geht beim Holen irgendetwas schief, wird ganz normal
|
||||||
zur Zielseite navigiert. Schlimmstenfalls ist es also wie vorher.
|
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.
|
/* Seiteneigene Skripte melden sich fast alle über "DOMContentLoaded" an.
|
||||||
Nach dem ersten Laden ist dieses Ereignis längst vorbei — ein später
|
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-113
@@ -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 & 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 & 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 & 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. 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>
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
Disallow: /postfach.html
|
|
||||||
Disallow: /cloudflare-worker/
|
Disallow: /cloudflare-worker/
|
||||||
|
|
||||||
Sitemap: https://REPLACE-WITH-YOUR-DOMAIN.tld/sitemap.xml
|
Sitemap: https://REPLACE-WITH-YOUR-DOMAIN.tld/sitemap.xml
|
||||||
|
|||||||
-228
@@ -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>
|
|
||||||
Reference in New Issue
Block a user