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 @@
Postfach öffnen
+
+ 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 @@
Zugänge
Rollen
+ Mein Zugang
Aktivitätsprotokoll
@@ -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