From 597988a9a2ee0a3d1f6d7b585f960c2e2cc30b71 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 31 Jul 2026 02:19:36 +0200 Subject: [PATCH] Interner Zugang auffindbar + Owner kann eigenen Code selbst aendern - Footer hat jetzt einen eigenen Block "Intern" mit Links zum Bewerbungs-Postfach und zu Zugaenge & Rollen (vorher nur ein winziger Link ganz unten) - Postfach-Login verlinkt auf die Zugaenge-Seite - Neue Tabelle app_settings + Endpunkt /admin/owner/set-code: der Hauptadministrator kann seinen Code selbst aendern, mit Bestaetigung des aktuellen Codes; gespeichert wird nur der Hash - Neuer Tab "Mein Zugang" in zugaenge.html (nur fuer den Owner) Co-Authored-By: Claude Opus 5 --- assets/js/main.js | 7 +++- assets/js/zugaenge.js | 33 +++++++++++++++ .../migrations/0002_app_settings.sql | 12 ++++++ cloudflare-worker/src/lib/auth.js | 18 +++++++- cloudflare-worker/src/routes/auth.js | 41 ++++++++++++++++++- cloudflare-worker/src/worker.js | 3 +- postfach.html | 4 ++ zugaenge.html | 27 ++++++++++++ 8 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 cloudflare-worker/migrations/0002_app_settings.sql diff --git a/assets/js/main.js b/assets/js/main.js index 531095c..a62ff79 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -158,11 +158,16 @@
  • Bewerben
  • Kontakt
  • +

    Intern đź”’

    + `; } diff --git a/assets/js/zugaenge.js b/assets/js/zugaenge.js index cc398d3..7eadcb2 100644 --- a/assets/js/zugaenge.js +++ b/assets/js/zugaenge.js @@ -70,6 +70,10 @@ function showApp(session) { 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(); } @@ -501,6 +505,35 @@ document.addEventListener("DOMContentLoaded", () => { 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) => { diff --git a/cloudflare-worker/migrations/0002_app_settings.sql b/cloudflare-worker/migrations/0002_app_settings.sql new file mode 100644 index 0000000..5fa0258 --- /dev/null +++ b/cloudflare-worker/migrations/0002_app_settings.sql @@ -0,0 +1,12 @@ +-- ===================================================================== +-- Allgemeine Einstellungen des internen Bereichs. +-- Aktuell genutzt für: owner_code_hash — der SHA-256-Hash des +-- Hauptadministrator-Codes, damit Dogi seinen eigenen Zugangscode +-- selbst ändern kann (vorher lag er fest im Worker-Secret). +-- Der Klartext-Code wird NIRGENDS gespeichert. +-- ===================================================================== +CREATE TABLE IF NOT EXISTS app_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL +); diff --git a/cloudflare-worker/src/lib/auth.js b/cloudflare-worker/src/lib/auth.js index bb04478..67dcab7 100644 --- a/cloudflare-worker/src/lib/auth.js +++ b/cloudflare-worker/src/lib/auth.js @@ -59,10 +59,24 @@ export async function clearAttempts(env, key) { /** Prüft einen eingegebenen Code gegen Owner-Secret und Team-Datenbank. */ export async function verifyCode(env, code) { - if (env.POSTFACH_CODE && timingSafeEqual(code, env.POSTFACH_CODE)) { + const hash = await sha256Hex(code); + + // 1) Owner-Code aus der Datenbank — den kann Dogi selbst ändern. + // Sobald hier einer hinterlegt ist, gilt NUR noch dieser (kein Zweitschlüssel). + const ownerRow = await env.DB.prepare( + `SELECT value FROM app_settings WHERE key = 'owner_code_hash'` + ) + .first() + .catch(() => null); + + if (ownerRow && ownerRow.value) { + if (timingSafeEqual(hash, ownerRow.value)) return { ok: true, isOwner: true }; + } else if (env.POSTFACH_CODE && timingSafeEqual(code, env.POSTFACH_CODE)) { + // 2) Erstzugang: solange noch kein eigener Code gesetzt wurde, gilt das Worker-Secret. return { ok: true, isOwner: true }; } - const hash = await sha256Hex(code); + + // 3) Normale Teammitglieder const user = await env.DB.prepare( `SELECT id, name, username, role_id, status, expires_at FROM users WHERE code_hash = ?` ) diff --git a/cloudflare-worker/src/routes/auth.js b/cloudflare-worker/src/routes/auth.js index c6daca8..916b322 100644 --- a/cloudflare-worker/src/routes/auth.js +++ b/cloudflare-worker/src/routes/auth.js @@ -3,9 +3,48 @@ ===================================================================== */ import { verifyCode, createSession, endSession, isLockedOut, recordFailedAttempt, clearAttempts, clientKey } from "../lib/auth.js"; import { logAction } from "../lib/audit.js"; -import { nowIso } from "../lib/crypto.js"; +import { nowIso, sha256Hex } from "../lib/crypto.js"; import { json } from "../lib/http.js"; +/** + * Der Hauptadministrator ändert seinen eigenen Zugangscode. + * Nur mit gültiger Owner-Session UND erneuter Eingabe des aktuellen Codes. + * Gespeichert wird ausschließlich der SHA-256-Hash — nie der Klartext. + */ +export async function setOwnerCode(request, env, session) { + if (!session || !session.isOwner) { + return json({ ok: false, error: "Das darf nur der Hauptadministrator." }, 403); + } + + const body = await request.json().catch(() => ({})); + const currentCode = String(body.currentCode || ""); + const newCode = String(body.newCode || ""); + + const check = await verifyCode(env, currentCode); + if (!check.ok || !check.isOwner) { + await logAction(env, "owner", "owner.code_change_failed", null, null); + return json({ ok: false, error: "Der aktuelle Code stimmt nicht." }, 403); + } + + if (newCode.length < 10) { + return json({ ok: false, error: "Der neue Code muss mindestens 10 Zeichen haben." }, 400); + } + if (newCode === currentCode) { + return json({ ok: false, error: "Der neue Code muss sich vom alten unterscheiden." }, 400); + } + + const hash = await sha256Hex(newCode); + await env.DB.prepare( + `INSERT INTO app_settings (key, value, updated_at) VALUES ('owner_code_hash', ?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at` + ) + .bind(hash, nowIso()) + .run(); + + await logAction(env, "owner", "owner.code_changed", null, null); + return json({ ok: true }); +} + export async function login(request, env) { const body = await request.json().catch(() => ({})); const code = String(body.code || ""); diff --git a/cloudflare-worker/src/worker.js b/cloudflare-worker/src/worker.js index dddc29b..9df6efd 100644 --- a/cloudflare-worker/src/worker.js +++ b/cloudflare-worker/src/worker.js @@ -23,7 +23,7 @@ ===================================================================== */ import { json, corsHeaders } from "./lib/http.js"; import { requireSession } from "./lib/auth.js"; -import { login, logout } from "./routes/auth.js"; +import { login, logout, setOwnerCode } from "./routes/auth.js"; import { listRoles, createRole, updateRole, deleteRole, } from "./routes/roles.js"; @@ -59,6 +59,7 @@ async function handleLiveStatusSet(request, env, session) { // Routen, die eine gültige Session brauchen. `session` wird injiziert. const AUTHED_ROUTES = { "POST /auth/logout": (req, env) => logout(req, env), + "POST /admin/owner/set-code": (req, env, session) => setOwnerCode(req, env, session), "POST /live-status/set": (req, env, session) => handleLiveStatusSet(req, env, session), "POST /admin/applications/list": (req, env, session) => listApplications(req, env, session), diff --git a/postfach.html b/postfach.html index 685fdf9..5ca1f6f 100644 --- a/postfach.html +++ b/postfach.html @@ -45,6 +45,10 @@
    +

    + Zugänge, Rollen und Rechte fürs Team verwaltest du unter + Zugänge & Teammitglieder → +

    diff --git a/zugaenge.html b/zugaenge.html index ec103ac..5ac2766 100644 --- a/zugaenge.html +++ b/zugaenge.html @@ -74,6 +74,7 @@
    +
    @@ -98,6 +99,32 @@
    + +
    +

    Mein Zugangscode

    +

    + Hier änderst du deinen eigenen Hauptadministrator-Code. Er wird nur als nicht + umkehrbarer Prüfwert gespeichert — niemand (auch ich nicht) kann ihn auslesen. + Merk ihn dir deshalb gut. +

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +

    Aktivitätsprotokoll