more admin stuff 2

This commit is contained in:
2026-08-20 17:10:19 +02:00
parent e8216b14e9
commit cd5957cbd2
19 changed files with 1120 additions and 78 deletions

View File

@ -3,14 +3,8 @@
const ROLE_NAMES = [
"ssh_connect", "rdp_connect", "file_transfer", "clipboard",
"session_recording_view", "admin_hostgroup",
"session_recording_view", "admin_hostgroup", "credentials_view", "credentials_manage",
];
const AUTH_EVENT_TYPES = new Set([
"login_password_ok", "login_failed", "login_success", "login_totp_failed",
"login_recovery_code_used", "totp_enroll_started", "totp_enroll_confirmed",
"totp_enroll_failed", "logout", "logout_everywhere", "password_changed",
]);
const FAILURE_EVENT_TYPES = new Set(["login_failed", "login_totp_failed", "totp_enroll_failed"]);
const bannerBox = document.getElementById("banner-box");
let meInfo = { is_admin: false, tenant_admin_of: [] };
@ -112,15 +106,18 @@
const tabLoaders = {
users: loadUsersTab,
groups: loadGroupsTab,
hosts: loadHostsTab,
hostgroups: loadHostsTab,
servers: loadHostsTab,
credentials: loadCredentialsTab,
roles: loadRolesTab,
tokens: loadTokensTab,
tenants: loadTenantsTab,
authlog: loadAuthLogTab,
sessions: loadSessionsTab,
connlog: loadConnLogTab,
audit: loadAuditTab,
};
const loadedTabs = new Set();
let currentTab = null;
document.getElementById("tabs").addEventListener("click", (ev) => {
const btn = ev.target.closest(".tab-btn");
@ -130,9 +127,15 @@
document.querySelectorAll(".tab-panel").forEach((panel) => {
panel.classList.toggle("hidden", panel.id !== `tab-${tab}`);
});
if (currentTab === "connlog" && tab !== "connlog") disconnectLogStream();
currentTab = tab;
if (!loadedTabs.has(tab)) {
loadedTabs.add(tab);
tabLoaders[tab]().catch((err) => showBanner(err.message, "error"));
} else if (tab === "connlog") {
connectLogStream();
} else if (tab === "sessions") {
refreshSessions().catch((err) => showBanner(err.message, "error"));
}
});
@ -797,7 +800,7 @@
el("td", { textContent: r.updated_at || "-" }),
el("td", {}, [
actionButton("Zum Host", "btn-secondary", async () => {
document.querySelector('.tab-btn[data-tab="hosts"]').click();
document.querySelector('.tab-btn[data-tab="servers"]').click();
await showHostDetail(r.host_id);
}),
...(r.credentials_set
@ -1122,36 +1125,151 @@
});
// ---------------------------------------------------------------------
// Login-Verlauf (Auth-Log) -- clientseitig aus dem Audit-Log gefiltert
// Sessions (nur Super-Admin) -- aktive + historische Sitzungen, Beenden,
// Link zur Aufzeichnung.
// ---------------------------------------------------------------------
async function loadAuthLogTab() {
await refreshAuthLog();
let sessionsActiveOnly = true;
let sessionsAutoTimer = null;
async function loadSessionsTab() {
document.getElementById("sessions-active-only").checked = sessionsActiveOnly;
await refreshSessions();
if (sessionsAutoTimer === null) {
sessionsAutoTimer = window.setInterval(() => {
if (currentTab === "sessions") refreshSessions().catch(() => {});
}, 5000);
}
}
async function refreshAuthLog() {
const entries = await getJson("/admin/audit-log?limit=500");
const authEntries = entries.filter((e) => AUTH_EVENT_TYPES.has(e.event_type));
const tbody = document.querySelector("#authlog-table tbody");
document.getElementById("sessions-active-only").addEventListener("change", (ev) => {
sessionsActiveOnly = ev.target.checked;
refreshSessions().catch((err) => showBanner(err.message, "error"));
});
async function refreshSessions() {
const rows = await getJson(`/admin/sessions?active_only=${sessionsActiveOnly}&limit=300`);
const tbody = document.querySelector("#sessions-table tbody");
fillTable(
tbody,
authEntries.map((e) =>
el("tr", {}, [
el("td", { textContent: e.ts }),
el("td", { textContent: e.user_id === null ? "-" : String(e.user_id) }),
el("td", { textContent: e.client_ip || "-" }),
el("td", { textContent: e.event_type }),
rows.map((s) => {
const actions = el("td", {});
if (s.is_active && s.killable) {
actions.appendChild(
actionButton("Beenden", "btn-danger", async () => {
await sendJson(`/admin/sessions/${s.id}/terminate`, "POST", {});
showBanner(`Sitzung #${s.id} beendet.`, "ok");
await refreshSessions();
})
);
} else if (s.is_active) {
actions.appendChild(el("span", { className: "hint", textContent: "anderer Prozess" }));
}
if (s.has_recording) {
actions.appendChild(
actionButton("Aufzeichnung", "btn-secondary", () => showSessionRecording(s.id))
);
}
return el("tr", {}, [
el("td", { textContent: String(s.id) }),
el("td", { textContent: s.username }),
el("td", { textContent: `${s.hostname} (${s.host_group_name})` }),
el("td", { textContent: s.protocol.toUpperCase() }),
el("td", { textContent: s.started_at }),
el("td", {}, [
el("span", {
className: `badge ${FAILURE_EVENT_TYPES.has(e.event_type) ? "danger" : "ok"}`,
textContent: FAILURE_EVENT_TYPES.has(e.event_type) ? "fehlgeschlagen" : "erfolgreich",
className: `badge ${s.is_active ? "ok" : ""}`,
textContent: s.is_active ? "aktiv" : (s.end_reason || "beendet"),
}),
]),
])
)
el("td", { textContent: s.client_ip }),
actions,
]);
})
);
}
async function showSessionRecording(sessionId) {
try {
const result = await getJson(`/admin/sessions/${sessionId}/recording`);
const box = document.getElementById("session-recording-box");
box.classList.remove("hidden");
box.className = `banner ${result.verified ? "ok" : "error"}`;
box.textContent = result.verified
? `Aufzeichnung #${sessionId}: Integritaet OK, ${result.entry_count} Eintraege.`
: `Aufzeichnung #${sessionId}: WARNUNG -- Hash-Kette gebrochen, moeglicherweise manipuliert!`;
} catch (err) {
showBanner(err.message, "error");
}
}
// ---------------------------------------------------------------------
// Verbindungslog (nur Super-Admin) -- Live-Tail der Anwendungslogs
// (inkl. Debug fuer SSH/RDP-Verbindungsaufbau) per WebSocket.
// ---------------------------------------------------------------------
let logSocket = null;
let logLines = [];
const LOG_MAX_LINES = 2000;
function connectLogStream() {
if (logSocket && (logSocket.readyState === WebSocket.OPEN || logSocket.readyState === WebSocket.CONNECTING)) {
return;
}
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
logSocket = new WebSocket(`${proto}//${window.location.host}/admin/ws/logs`);
setConnLogStatus("verbinde...");
logSocket.addEventListener("open", () => setConnLogStatus("verbunden"));
logSocket.addEventListener("close", () => setConnLogStatus("getrennt"));
logSocket.addEventListener("error", () => setConnLogStatus("Fehler"));
logSocket.addEventListener("message", (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === "line") appendLogLine(msg.line);
} catch (_err) {
// ungueltige Nachricht ignorieren
}
});
}
function disconnectLogStream() {
if (logSocket) {
logSocket.close();
logSocket = null;
}
setConnLogStatus("getrennt");
}
function setConnLogStatus(text) {
const el2 = document.getElementById("connlog-status");
if (el2) el2.textContent = text;
}
function appendLogLine(line) {
logLines.push(line);
if (logLines.length > LOG_MAX_LINES) logLines = logLines.slice(-LOG_MAX_LINES);
renderConnLog();
}
function renderConnLog() {
const pre = document.getElementById("connlog-output");
if (!pre) return;
const filter = document.getElementById("connlog-filter").value.trim().toLowerCase();
const filtered = filter ? logLines.filter((l) => l.toLowerCase().includes(filter)) : logLines;
const wasAtBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 20;
pre.textContent = filtered.join("\n");
if (wasAtBottom) pre.scrollTop = pre.scrollHeight;
}
async function loadConnLogTab() {
connectLogStream();
document.getElementById("connlog-filter").addEventListener("input", renderConnLog);
document.getElementById("connlog-clear-btn").addEventListener("click", () => {
logLines = [];
renderConnLog();
});
}
// ---------------------------------------------------------------------
// Audit-Log
// ---------------------------------------------------------------------
@ -1238,6 +1356,8 @@
: `Mandanten-Admin: ${me.tenant_admin_of.map((t) => t.name).join(", ")}`;
document.getElementById("whoami").textContent = `${me.username} (${roleLabel})`;
document.getElementById("tenants-tab-btn").classList.toggle("hidden", !me.is_admin);
document.getElementById("sessions-tab-btn").classList.toggle("hidden", !me.is_admin);
document.getElementById("connlog-tab-btn").classList.toggle("hidden", !me.is_admin);
loadedTabs.add("users");
await loadUsersTab();
}