more admin stuff 2
This commit is contained in:
@ -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();
|
||||
}
|
||||
|
||||
@ -10,6 +10,30 @@
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function showCredentials(host) {
|
||||
const box = document.getElementById("credentials-box");
|
||||
box.className = "reveal-box";
|
||||
box.textContent = `Lade Zugangsdaten fuer ${host.hostname} ...`;
|
||||
try {
|
||||
const info = await getJson(`/admin/hosts/${host.id}/credentials`);
|
||||
const lines = [`Zugangsdaten fuer ${host.hostname}:`];
|
||||
lines.push(
|
||||
info.ssh_keys.length
|
||||
? `SSH-Keys: ${info.ssh_keys.map((k) => k.label).join(", ")}`
|
||||
: "SSH-Keys: keine zugeordnet"
|
||||
);
|
||||
lines.push(
|
||||
info.rdp_credentials_set
|
||||
? `RDP-Passwort gesetzt (zuletzt aktualisiert: ${info.rdp_credentials_updated_at}).`
|
||||
: "RDP-Passwort: nicht gesetzt."
|
||||
);
|
||||
box.textContent = lines.join("\n");
|
||||
} catch (err) {
|
||||
box.className = "banner error";
|
||||
box.textContent = `Zugangsdaten konnten nicht geladen werden: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function hostCard(host) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "host-card";
|
||||
@ -32,6 +56,17 @@
|
||||
connect.textContent = "Verbinden";
|
||||
actions.appendChild(connect);
|
||||
|
||||
if (host.can_view_credentials) {
|
||||
const credBtn = document.createElement("button");
|
||||
credBtn.type = "button";
|
||||
credBtn.textContent = "Zugangsdaten";
|
||||
credBtn.addEventListener("click", () => {
|
||||
document.getElementById("credentials-box").classList.remove("hidden");
|
||||
showCredentials(host);
|
||||
});
|
||||
actions.appendChild(credBtn);
|
||||
}
|
||||
|
||||
div.appendChild(actions);
|
||||
return div;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user