From a4063ca98522b12212e34b9d7d51a1bae8082ada Mon Sep 17 00:00:00 2001 From: karim Date: Sun, 31 May 2026 10:51:52 +0200 Subject: [PATCH] feat(admin): separates Betreiber-Login im Frontend + Footer/Rechts-Links - renderAdminLogin: eigenes Admin-Passwort-Formular, eigener Token (rapport_admin_token), getrennt vom Kundenkonto - renderAdmin nutzt adminApi (Admin-Token); Abmelden-Button - is_admin-Reste aus Konto-Header und Kundentabelle entfernt Co-Authored-By: Claude Opus 4.8 --- public/js/hosting-app.js | 58 +++++++++++++++++++++++++++++++--------- static/js/hosting-app.js | 58 +++++++++++++++++++++++++++++++--------- 2 files changed, 90 insertions(+), 26 deletions(-) diff --git a/public/js/hosting-app.js b/public/js/hosting-app.js index bae8cfe..aca7356 100644 --- a/public/js/hosting-app.js +++ b/public/js/hosting-app.js @@ -126,9 +126,7 @@ const head = '
' + '
Mein Konto
' + - '
' + - (account.is_admin ? 'Admin' : "") + - '
' + + '' + '
' + esc(account.email) + "
" + '
' + tabs.map(([id, label]) => @@ -276,21 +274,55 @@ }); } - // ── Admin / Betreiber-Bereich ──────────────────────────────────────────── + // ── Admin / Betreiber-Bereich (SEPARATES Login, eigener Token) ──────────── + const ADMIN_KEY = "rapport_admin_token"; + const adminTok = { + get: () => localStorage.getItem(ADMIN_KEY), + set: (t) => localStorage.setItem(ADMIN_KEY, t), + clear: () => localStorage.removeItem(ADMIN_KEY), + }; + // wie api(), aber mit Admin-Token statt Kunden-Token. + async function adminApi(method, path, body) { + const headers = { "Content-Type": "application/json" }; + const t = adminTok.get(); + if (t) headers.Authorization = `Bearer ${t}`; + const res = await fetch(`/api${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { const e = new Error(data.error || `Fehler ${res.status}`); e.status = res.status; throw e; } + return data; + } + + function renderAdminLogin(errText) { + root.innerHTML = card( + '
Betreiber-Login
' + + '
Interner Bereich
' + + (errText ? '
' + esc(errText) + "
" : "") + + '
' + + '' + + '
' + ); + root.querySelector("#af").onsubmit = async (e) => { + e.preventDefault(); + const sub = root.querySelector("#asub"); sub.disabled = true; + try { + const { token } = await adminApi("POST", "/admin/login", { password: root.querySelector("#apw").value }); + adminTok.set(token); renderAdmin(); + } catch (err) { renderAdminLogin(err.message); } + }; + } + async function renderAdmin() { - if (!tok.isLoggedIn) return go("/login/"); + if (!adminTok.get()) return renderAdminLogin(""); root.innerHTML = card('
Lädt…
', true); let stats, accounts; try { [stats, accounts] = await Promise.all([ - api("GET", "/admin/stats"), - api("GET", "/admin/accounts"), + adminApi("GET", "/admin/stats"), + adminApi("GET", "/admin/accounts"), ]); } catch (err) { - if (/angemeldet|abgelaufen|ungültig/i.test(err.message)) { tok.clear(); return go("/login/"); } - // 403 = eingeloggt, aber kein Admin - root.innerHTML = card('
' + esc(err.message) + "
" + - 'Zurück zum Konto'); + if (err.status === 401 || err.status === 403) { adminTok.clear(); return renderAdminLogin("Bitte neu anmelden."); } + root.innerHTML = card('
' + esc(err.message) + "
"); return; } @@ -300,7 +332,7 @@ const rows = accounts.accounts.map((a) => "" + - "" + esc(a.email) + "" + (a.is_admin ? ' Admin' : "") + + "" + esc(a.email) + "" + (a.company ? '
' + esc(a.company) + "
" : "") + "" + "" + (a.plan ? esc(a.plan) + '
' + esc(a.sub_status || "") + "
" : '') + "" + "" + esc(a.instance_count) + "" + @@ -311,7 +343,7 @@ const html = '
' + '
Admin
' + - 'Mein Konto
' + + '
' + '
' + stat("Kunden", stats.accounts) + stat("Aktive Abos", stats.activeSubscriptions) + diff --git a/static/js/hosting-app.js b/static/js/hosting-app.js index bae8cfe..aca7356 100644 --- a/static/js/hosting-app.js +++ b/static/js/hosting-app.js @@ -126,9 +126,7 @@ const head = '
' + '
Mein Konto
' + - '
' + - (account.is_admin ? 'Admin' : "") + - '
' + + '
' + '
' + esc(account.email) + "
" + '
' + tabs.map(([id, label]) => @@ -276,21 +274,55 @@ }); } - // ── Admin / Betreiber-Bereich ──────────────────────────────────────────── + // ── Admin / Betreiber-Bereich (SEPARATES Login, eigener Token) ──────────── + const ADMIN_KEY = "rapport_admin_token"; + const adminTok = { + get: () => localStorage.getItem(ADMIN_KEY), + set: (t) => localStorage.setItem(ADMIN_KEY, t), + clear: () => localStorage.removeItem(ADMIN_KEY), + }; + // wie api(), aber mit Admin-Token statt Kunden-Token. + async function adminApi(method, path, body) { + const headers = { "Content-Type": "application/json" }; + const t = adminTok.get(); + if (t) headers.Authorization = `Bearer ${t}`; + const res = await fetch(`/api${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { const e = new Error(data.error || `Fehler ${res.status}`); e.status = res.status; throw e; } + return data; + } + + function renderAdminLogin(errText) { + root.innerHTML = card( + '
Betreiber-Login
' + + '
Interner Bereich
' + + (errText ? '
' + esc(errText) + "
" : "") + + '
' + + '' + + '
' + ); + root.querySelector("#af").onsubmit = async (e) => { + e.preventDefault(); + const sub = root.querySelector("#asub"); sub.disabled = true; + try { + const { token } = await adminApi("POST", "/admin/login", { password: root.querySelector("#apw").value }); + adminTok.set(token); renderAdmin(); + } catch (err) { renderAdminLogin(err.message); } + }; + } + async function renderAdmin() { - if (!tok.isLoggedIn) return go("/login/"); + if (!adminTok.get()) return renderAdminLogin(""); root.innerHTML = card('
Lädt…
', true); let stats, accounts; try { [stats, accounts] = await Promise.all([ - api("GET", "/admin/stats"), - api("GET", "/admin/accounts"), + adminApi("GET", "/admin/stats"), + adminApi("GET", "/admin/accounts"), ]); } catch (err) { - if (/angemeldet|abgelaufen|ungültig/i.test(err.message)) { tok.clear(); return go("/login/"); } - // 403 = eingeloggt, aber kein Admin - root.innerHTML = card('
' + esc(err.message) + "
" + - 'Zurück zum Konto'); + if (err.status === 401 || err.status === 403) { adminTok.clear(); return renderAdminLogin("Bitte neu anmelden."); } + root.innerHTML = card('
' + esc(err.message) + "
"); return; } @@ -300,7 +332,7 @@ const rows = accounts.accounts.map((a) => "" + - "" + esc(a.email) + "" + (a.is_admin ? ' Admin' : "") + + "" + esc(a.email) + "" + (a.company ? '
' + esc(a.company) + "
" : "") + "" + "" + (a.plan ? esc(a.plan) + '
' + esc(a.sub_status || "") + "
" : '') + "" + "" + esc(a.instance_count) + "" + @@ -311,7 +343,7 @@ const html = '
' + '
Admin
' + - 'Mein Konto
' + + '
' + '
' + stat("Kunden", stats.accounts) + stat("Aktive Abos", stats.activeSubscriptions) +