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 <noreply@anthropic.com>
This commit is contained in:
+45
-13
@@ -126,9 +126,7 @@
|
||||
const head =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Mein Konto</div>' +
|
||||
'<div style="display:flex;gap:14px;align-items:center">' +
|
||||
(account.is_admin ? '<a class="hosting-link" href="/admin/">Admin</a>' : "") +
|
||||
'<button class="hosting-link" id="logout">Abmelden</button></div></div>' +
|
||||
'<button class="hosting-link" id="logout">Abmelden</button></div>' +
|
||||
'<div class="hosting-sub" style="text-align:left;margin-bottom:20px">' + esc(account.email) + "</div>" +
|
||||
'<div class="hosting-tabs">' +
|
||||
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(
|
||||
'<div class="hosting-title">Betreiber-Login</div>' +
|
||||
'<div class="hosting-sub">Interner Bereich</div>' +
|
||||
(errText ? '<div class="hosting-msg err">' + esc(errText) + "</div>" : "") +
|
||||
'<form id="af"><label class="hosting-label">Admin-Passwort</label>' +
|
||||
'<input class="hosting-input" type="password" id="apw" autofocus>' +
|
||||
'<button class="hosting-btn hosting-btn-primary" id="asub">Anmelden</button></form>'
|
||||
);
|
||||
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('<div class="hosting-sub">Lädt…</div>', 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('<div class="hosting-msg err">' + esc(err.message) + "</div>" +
|
||||
'<a class="hosting-link" href="/konto/">Zurück zum Konto</a>');
|
||||
if (err.status === 401 || err.status === 403) { adminTok.clear(); return renderAdminLogin("Bitte neu anmelden."); }
|
||||
root.innerHTML = card('<div class="hosting-msg err">' + esc(err.message) + "</div>");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,7 +332,7 @@
|
||||
|
||||
const rows = accounts.accounts.map((a) =>
|
||||
"<tr>" +
|
||||
"<td><b>" + esc(a.email) + "</b>" + (a.is_admin ? ' <span class="admin-tag">Admin</span>' : "") +
|
||||
"<td><b>" + esc(a.email) + "</b>" +
|
||||
(a.company ? '<div class="muted" style="font-size:12px">' + esc(a.company) + "</div>" : "") + "</td>" +
|
||||
"<td>" + (a.plan ? esc(a.plan) + '<div class="muted" style="font-size:12px">' + esc(a.sub_status || "") + "</div>" : '<span class="muted">—</span>') + "</td>" +
|
||||
"<td style=\"text-align:center\">" + esc(a.instance_count) + "</td>" +
|
||||
@@ -311,7 +343,7 @@
|
||||
const html =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Admin</div>' +
|
||||
'<a class="hosting-link" href="/konto/">Mein Konto</a></div>' +
|
||||
'<button class="hosting-link" id="alogout">Abmelden</button></div>' +
|
||||
'<div class="admin-stats">' +
|
||||
stat("Kunden", stats.accounts) +
|
||||
stat("Aktive Abos", stats.activeSubscriptions) +
|
||||
|
||||
+45
-13
@@ -126,9 +126,7 @@
|
||||
const head =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Mein Konto</div>' +
|
||||
'<div style="display:flex;gap:14px;align-items:center">' +
|
||||
(account.is_admin ? '<a class="hosting-link" href="/admin/">Admin</a>' : "") +
|
||||
'<button class="hosting-link" id="logout">Abmelden</button></div></div>' +
|
||||
'<button class="hosting-link" id="logout">Abmelden</button></div>' +
|
||||
'<div class="hosting-sub" style="text-align:left;margin-bottom:20px">' + esc(account.email) + "</div>" +
|
||||
'<div class="hosting-tabs">' +
|
||||
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(
|
||||
'<div class="hosting-title">Betreiber-Login</div>' +
|
||||
'<div class="hosting-sub">Interner Bereich</div>' +
|
||||
(errText ? '<div class="hosting-msg err">' + esc(errText) + "</div>" : "") +
|
||||
'<form id="af"><label class="hosting-label">Admin-Passwort</label>' +
|
||||
'<input class="hosting-input" type="password" id="apw" autofocus>' +
|
||||
'<button class="hosting-btn hosting-btn-primary" id="asub">Anmelden</button></form>'
|
||||
);
|
||||
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('<div class="hosting-sub">Lädt…</div>', 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('<div class="hosting-msg err">' + esc(err.message) + "</div>" +
|
||||
'<a class="hosting-link" href="/konto/">Zurück zum Konto</a>');
|
||||
if (err.status === 401 || err.status === 403) { adminTok.clear(); return renderAdminLogin("Bitte neu anmelden."); }
|
||||
root.innerHTML = card('<div class="hosting-msg err">' + esc(err.message) + "</div>");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,7 +332,7 @@
|
||||
|
||||
const rows = accounts.accounts.map((a) =>
|
||||
"<tr>" +
|
||||
"<td><b>" + esc(a.email) + "</b>" + (a.is_admin ? ' <span class="admin-tag">Admin</span>' : "") +
|
||||
"<td><b>" + esc(a.email) + "</b>" +
|
||||
(a.company ? '<div class="muted" style="font-size:12px">' + esc(a.company) + "</div>" : "") + "</td>" +
|
||||
"<td>" + (a.plan ? esc(a.plan) + '<div class="muted" style="font-size:12px">' + esc(a.sub_status || "") + "</div>" : '<span class="muted">—</span>') + "</td>" +
|
||||
"<td style=\"text-align:center\">" + esc(a.instance_count) + "</td>" +
|
||||
@@ -311,7 +343,7 @@
|
||||
const html =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Admin</div>' +
|
||||
'<a class="hosting-link" href="/konto/">Mein Konto</a></div>' +
|
||||
'<button class="hosting-link" id="alogout">Abmelden</button></div>' +
|
||||
'<div class="admin-stats">' +
|
||||
stat("Kunden", stats.accounts) +
|
||||
stat("Aktive Abos", stats.activeSubscriptions) +
|
||||
|
||||
Reference in New Issue
Block a user