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 <[email protected]>
This commit is contained in:
+6
-1
@@ -158,11 +158,16 @@
|
|||||||
<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">Team-Postfach 🔒</a></span>
|
<span><a href="kontakt.html">Impressum & Datenschutz</a> · <a href="postfach.html">🔒 Interner Login</a></span>
|
||||||
</div>
|
</div>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ function showApp(session) {
|
|||||||
const canViewAudit = session.isOwner || sessionHasPermission(session, "SETTINGS_VIEW_ACTIVITY_LOG");
|
const canViewAudit = session.isOwner || sessionHasPermission(session, "SETTINGS_VIEW_ACTIVITY_LOG");
|
||||||
if (auditTabBtn) auditTabBtn.style.display = canViewAudit ? "" : "none";
|
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);
|
loadRoles().then(loadUsers);
|
||||||
if (session.isOwner || sessionHasPermission(session, "SETTINGS_VIEW_ACTIVITY_LOG")) loadAudit();
|
if (session.isOwner || sessionHasPermission(session, "SETTINGS_VIEW_ACTIVITY_LOG")) loadAudit();
|
||||||
}
|
}
|
||||||
@@ -501,6 +505,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
showGate();
|
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
|
// Modals per Klick auf Backdrop schließen
|
||||||
document.querySelectorAll(".modal-backdrop").forEach((m) => {
|
document.querySelectorAll(".modal-backdrop").forEach((m) => {
|
||||||
m.addEventListener("click", (e) => {
|
m.addEventListener("click", (e) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -59,10 +59,24 @@ export async function clearAttempts(env, key) {
|
|||||||
|
|
||||||
/** Prüft einen eingegebenen Code gegen Owner-Secret und Team-Datenbank. */
|
/** Prüft einen eingegebenen Code gegen Owner-Secret und Team-Datenbank. */
|
||||||
export async function verifyCode(env, code) {
|
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 };
|
return { ok: true, isOwner: true };
|
||||||
}
|
}
|
||||||
const hash = await sha256Hex(code);
|
|
||||||
|
// 3) Normale Teammitglieder
|
||||||
const user = await env.DB.prepare(
|
const user = await env.DB.prepare(
|
||||||
`SELECT id, name, username, role_id, status, expires_at FROM users WHERE code_hash = ?`
|
`SELECT id, name, username, role_id, status, expires_at FROM users WHERE code_hash = ?`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,48 @@
|
|||||||
===================================================================== */
|
===================================================================== */
|
||||||
import { verifyCode, createSession, endSession, isLockedOut, recordFailedAttempt, clearAttempts, clientKey } from "../lib/auth.js";
|
import { verifyCode, createSession, endSession, isLockedOut, recordFailedAttempt, clearAttempts, clientKey } from "../lib/auth.js";
|
||||||
import { logAction } from "../lib/audit.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";
|
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) {
|
export async function login(request, env) {
|
||||||
const body = await request.json().catch(() => ({}));
|
const body = await request.json().catch(() => ({}));
|
||||||
const code = String(body.code || "");
|
const code = String(body.code || "");
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
===================================================================== */
|
===================================================================== */
|
||||||
import { json, corsHeaders } from "./lib/http.js";
|
import { json, corsHeaders } from "./lib/http.js";
|
||||||
import { requireSession } from "./lib/auth.js";
|
import { requireSession } from "./lib/auth.js";
|
||||||
import { login, logout } from "./routes/auth.js";
|
import { login, logout, setOwnerCode } from "./routes/auth.js";
|
||||||
import {
|
import {
|
||||||
listRoles, createRole, updateRole, deleteRole,
|
listRoles, createRole, updateRole, deleteRole,
|
||||||
} from "./routes/roles.js";
|
} from "./routes/roles.js";
|
||||||
@@ -59,6 +59,7 @@ async function handleLiveStatusSet(request, env, session) {
|
|||||||
// Routen, die eine gültige Session brauchen. `session` wird injiziert.
|
// Routen, die eine gültige Session brauchen. `session` wird injiziert.
|
||||||
const AUTHED_ROUTES = {
|
const AUTHED_ROUTES = {
|
||||||
"POST /auth/logout": (req, env) => logout(req, env),
|
"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 /live-status/set": (req, env, session) => handleLiveStatusSet(req, env, session),
|
||||||
|
|
||||||
"POST /admin/applications/list": (req, env, session) => listApplications(req, env, session),
|
"POST /admin/applications/list": (req, env, session) => listApplications(req, env, session),
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
<button class="btn btn-primary" type="submit">Postfach öffnen</button>
|
<button class="btn btn-primary" type="submit">Postfach öffnen</button>
|
||||||
<div class="form-status" id="gate-status"></div>
|
<div class="form-status" id="gate-status"></div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" data-tab="tab-users">Zugänge</button>
|
<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-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>
|
<button class="tab-btn" data-tab="tab-audit">Aktivitätsprotokoll</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,6 +99,32 @@
|
|||||||
<div id="roles-list" class="grid grid-2"></div>
|
<div id="roles-list" class="grid grid-2"></div>
|
||||||
</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 ---- -->
|
<!-- ---- Tab: Aktivitätsprotokoll ---- -->
|
||||||
<div class="tab-panel" id="tab-audit">
|
<div class="tab-panel" id="tab-audit">
|
||||||
<h2>Aktivitätsprotokoll</h2>
|
<h2>Aktivitätsprotokoll</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user