Vollständiges Zugangs-/Rollen-/Rechtesystem gebaut (Anforderungsdokument): D1-Datenbank, AES-verschlüsselte Codes nur für Owner einsehbar, granulare Permissions, Session-Auth mit Lockout, Zugänge-&-Teammitglieder-Verwaltungsseite, Bewerbungen mit Status/Notizen/Entwürfen
This commit is contained in:
+30
-290
@@ -11,6 +11,16 @@
|
||||
<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>
|
||||
@@ -24,8 +34,9 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container" style="max-width:520px;" id="gate-container">
|
||||
<!-- ===== 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>
|
||||
@@ -34,37 +45,21 @@
|
||||
<button class="btn btn-primary" type="submit">Postfach öffnen</button>
|
||||
<div class="form-status" id="gate-status"></div>
|
||||
</form>
|
||||
|
||||
<div class="todo-note" id="not-configured-note">
|
||||
<strong>Noch nicht aktiv:</strong> Dieses Postfach ist erst nutzbar, sobald der
|
||||
Cloudflare-Worker deployed und die Worker-URL in dieser Datei
|
||||
(<code>API_BASE_URL</code>, ganz unten im Script) sowie in
|
||||
<code>assets/js/forms.js</code> eingetragen ist. Anleitung:
|
||||
<code>cloudflare-worker/README.md</code>.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Eingeloggt: Kopfzeile mit Rolle + Logout ===== -->
|
||||
<!-- ===== 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>
|
||||
<button class="btn btn-outline" id="logout-btn" type="button">Abmelden</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Mailbox (für alle mit gültigem Code) ===== -->
|
||||
<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:1.4rem;">
|
||||
<h2 style="margin:0;">Eingegangene Bewerbungen</h2>
|
||||
<button class="btn btn-outline" id="refresh-btn" type="button">Aktualisieren</button>
|
||||
<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 id="inbox-list" class="grid grid-2"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Live-Status umschalten (NUR für Dogi/Owner sichtbar) ===== -->
|
||||
<!-- ===== Live-Status (nur Owner) ===== -->
|
||||
<section class="section-tight" id="live-toggle-section" style="display:none;">
|
||||
<div class="container text-center">
|
||||
<h2>Live-Status</h2>
|
||||
@@ -79,279 +74,24 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Rollen-Verwaltung (NUR für Dogi/Owner sichtbar) ===== -->
|
||||
<section id="admin-section" style="display:none;">
|
||||
<!-- ===== Mailbox ===== -->
|
||||
<section class="section-tight" id="inbox-section" style="display:none;">
|
||||
<div class="container">
|
||||
<h2 class="text-center">Rollen verwalten</h2>
|
||||
<p class="lead text-center" style="max-width:60ch;margin:0 auto;">
|
||||
Vergib Codes an Rechte Hand, Linke Hand oder wen du sonst reinlassen willst. Diese Codes
|
||||
zeigen <strong>ausschließlich die Bewerbungen</strong> — nichts an der Website selbst kann
|
||||
damit verändert oder gelöscht werden, dafür gibt es hier schlicht keine Funktion.
|
||||
</p>
|
||||
|
||||
<form id="role-create-form" class="app-form" style="max-width:520px;margin:1.6rem auto;" novalidate>
|
||||
<div class="field">
|
||||
<label for="role-select">Rolle</label>
|
||||
<select id="role-select" name="role">
|
||||
<option value="Rechte Hand">Rechte Hand</option>
|
||||
<option value="Linke Hand">Linke Hand</option>
|
||||
<option value="Helfer">Sonstige / frei</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="role-label">Für wen? (Name/Bezeichnung)</label>
|
||||
<input id="role-label" name="label" type="text" placeholder="z.B. VanVan" required />
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Neuen Code erzeugen</button>
|
||||
<div class="form-status" id="role-create-status"></div>
|
||||
</form>
|
||||
|
||||
<div class="todo-note" id="new-code-reveal" style="display:none;max-width:520px;margin:0 auto 2rem;"></div>
|
||||
|
||||
<h3 class="text-center">Bestehende Codes</h3>
|
||||
<div id="roles-list" class="grid grid-2"></div>
|
||||
<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>
|
||||
// TODO: nach dem Deploy des Cloudflare Workers hier die Worker-URL ohne
|
||||
// abschließenden Slash eintragen (identisch zu assets/js/forms.js).
|
||||
const API_BASE_URL = "https://dogfather-universe-postfach.dogfather1608.workers.dev";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const gateContainer = document.getElementById("gate-container");
|
||||
const form = document.getElementById("code-gate");
|
||||
const status = document.getElementById("gate-status");
|
||||
const sessionBar = document.getElementById("session-bar");
|
||||
const roleBadge = document.getElementById("role-badge");
|
||||
const logoutBtn = document.getElementById("logout-btn");
|
||||
const inboxSection = document.getElementById("inbox-section");
|
||||
const inboxList = document.getElementById("inbox-list");
|
||||
const refreshBtn = document.getElementById("refresh-btn");
|
||||
const notConfigured = document.getElementById("not-configured-note");
|
||||
const adminSection = document.getElementById("admin-section");
|
||||
const roleCreateForm = document.getElementById("role-create-form");
|
||||
const roleCreateStatus = document.getElementById("role-create-status");
|
||||
const newCodeReveal = document.getElementById("new-code-reveal");
|
||||
const rolesList = document.getElementById("roles-list");
|
||||
const liveToggleSection = document.getElementById("live-toggle-section");
|
||||
const adminLiveDot = document.getElementById("admin-live-dot");
|
||||
const adminLiveText = document.getElementById("admin-live-text");
|
||||
const goLiveBtn = document.getElementById("go-live-btn");
|
||||
const goOfflineBtn = document.getElementById("go-offline-btn");
|
||||
|
||||
if (!API_BASE_URL) {
|
||||
notConfigured.style.display = "block";
|
||||
} else {
|
||||
notConfigured.style.display = "none";
|
||||
}
|
||||
|
||||
let session = { code: "", isOwner: false, roleLabel: "" };
|
||||
|
||||
function renderInbox(bewerbungen) {
|
||||
if (!bewerbungen.length) {
|
||||
inboxList.innerHTML = `<p class="muted">Noch keine Bewerbungen eingegangen.</p>`;
|
||||
return;
|
||||
}
|
||||
inboxList.innerHTML = bewerbungen
|
||||
.map((b) => {
|
||||
const fields = Object.entries(b.data)
|
||||
.map(([k, v]) => `<p class="small"><strong>${k}:</strong> ${v}</p>`)
|
||||
.join("");
|
||||
const date = new Date(b.timestamp).toLocaleString("de-DE");
|
||||
return `
|
||||
<div class="card">
|
||||
<span class="tag">${b.type}</span>
|
||||
<p class="small muted" style="margin-bottom:.6rem;">${date}</p>
|
||||
${fields}
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function loadInbox() {
|
||||
status.className = "form-status show";
|
||||
status.textContent = "Lade Postfach …";
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/postfach`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code: session.code }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
status.textContent = "Falscher Code oder Postfach nicht erreichbar.";
|
||||
status.className = "form-status show error";
|
||||
return false;
|
||||
}
|
||||
session.isOwner = data.isOwner;
|
||||
session.roleLabel = data.roleLabel;
|
||||
|
||||
gateContainer.style.display = "none";
|
||||
sessionBar.style.display = "block";
|
||||
inboxSection.style.display = "block";
|
||||
roleBadge.textContent = `Eingeloggt als: ${data.roleLabel}`;
|
||||
renderInbox(data.bewerbungen);
|
||||
|
||||
if (data.isOwner) {
|
||||
adminSection.style.display = "block";
|
||||
liveToggleSection.style.display = "block";
|
||||
loadRoles();
|
||||
loadLiveStatus();
|
||||
} else {
|
||||
adminSection.style.display = "none";
|
||||
liveToggleSection.style.display = "none";
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
status.textContent = "Postfach gerade nicht erreichbar. Bitte später erneut versuchen.";
|
||||
status.className = "form-status show error";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/admin/roles/list`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ownerCode: session.code }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
rolesList.innerHTML = `<p class="muted">Konnte Codes nicht laden.</p>`;
|
||||
return;
|
||||
}
|
||||
if (!data.roles.length) {
|
||||
rolesList.innerHTML = `<p class="muted">Noch keine Helfer-Codes vergeben.</p>`;
|
||||
return;
|
||||
}
|
||||
rolesList.innerHTML = data.roles
|
||||
.map(
|
||||
(r) => `
|
||||
<div class="card">
|
||||
<span class="tag">${r.role}</span>
|
||||
<h3>${r.label}</h3>
|
||||
<p class="small muted">Erstellt: ${new Date(r.createdAt).toLocaleDateString("de-DE")}</p>
|
||||
<button class="btn btn-outline revoke-btn" data-key="${r.keyId}" type="button">Code entziehen</button>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
rolesList.querySelectorAll(".revoke-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
if (!confirm("Diesen Code wirklich entziehen? Die Person kann sich danach nicht mehr einloggen.")) return;
|
||||
await fetch(`${API_BASE_URL}/admin/roles/revoke`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ownerCode: session.code, keyId: btn.dataset.key }),
|
||||
});
|
||||
loadRoles();
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
rolesList.innerHTML = `<p class="muted">Konnte Codes nicht laden.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderLiveStatus(live) {
|
||||
adminLiveDot.classList.toggle("is-live", !!live);
|
||||
adminLiveText.textContent = live ? "Live jetzt!" : "Aktuell offline";
|
||||
}
|
||||
|
||||
async function loadLiveStatus() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/live-status`);
|
||||
const data = await res.json();
|
||||
if (res.ok && data.ok) renderLiveStatus(data.live);
|
||||
} catch {
|
||||
// still, keine Blockade der restlichen Seite
|
||||
}
|
||||
}
|
||||
|
||||
async function setLiveStatus(live) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/live-status/set`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ownerCode: session.code, live }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.ok) renderLiveStatus(data.live);
|
||||
} catch {
|
||||
// keine Blockade
|
||||
}
|
||||
}
|
||||
|
||||
goLiveBtn.addEventListener("click", () => setLiveStatus(true));
|
||||
goOfflineBtn.addEventListener("click", () => setLiveStatus(false));
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (!API_BASE_URL) {
|
||||
status.textContent = "Postfach ist noch nicht eingerichtet (siehe Hinweis unten).";
|
||||
status.className = "form-status show error";
|
||||
return;
|
||||
}
|
||||
session.code = document.getElementById("code-input").value;
|
||||
const ok = await loadInbox();
|
||||
if (ok) {
|
||||
status.className = "form-status";
|
||||
form.reset();
|
||||
}
|
||||
});
|
||||
|
||||
logoutBtn.addEventListener("click", () => {
|
||||
session = { code: "", isOwner: false, roleLabel: "" };
|
||||
gateContainer.style.display = "block";
|
||||
sessionBar.style.display = "none";
|
||||
inboxSection.style.display = "none";
|
||||
adminSection.style.display = "none";
|
||||
liveToggleSection.style.display = "none";
|
||||
status.textContent = "";
|
||||
status.className = "form-status";
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener("click", () => {
|
||||
if (session.code) loadInbox();
|
||||
});
|
||||
|
||||
roleCreateForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const role = document.getElementById("role-select").value;
|
||||
const label = document.getElementById("role-label").value.trim();
|
||||
if (!label) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/admin/roles/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ownerCode: session.code, role, label }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
roleCreateStatus.textContent = "Konnte keinen Code erzeugen.";
|
||||
roleCreateStatus.className = "form-status show error";
|
||||
return;
|
||||
}
|
||||
newCodeReveal.style.display = "block";
|
||||
newCodeReveal.innerHTML = `
|
||||
<strong>Neuer Code für ${data.label} (${data.role}):</strong><br>
|
||||
<span style="font-size:1.3rem;font-family:monospace;letter-spacing:.05em;">${data.code}</span><br>
|
||||
<span class="small">Jetzt sichern/weitergeben — dieser Code wird nirgends mehr angezeigt!</span>`;
|
||||
roleCreateStatus.textContent = "";
|
||||
roleCreateStatus.className = "form-status";
|
||||
roleCreateForm.reset();
|
||||
loadRoles();
|
||||
} catch {
|
||||
roleCreateStatus.textContent = "Postfach gerade nicht erreichbar.";
|
||||
roleCreateStatus.className = "form-status show error";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="assets/js/admin-auth.js"></script>
|
||||
<script src="assets/js/postfach.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user