more admin stuff
This commit is contained in:
@ -5,8 +5,15 @@
|
||||
"ssh_connect", "rdp_connect", "file_transfer", "clipboard",
|
||||
"session_recording_view", "admin_hostgroup",
|
||||
];
|
||||
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: [] };
|
||||
|
||||
function showBanner(message, type) {
|
||||
const div = document.createElement("div");
|
||||
@ -80,6 +87,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
function roleCheckboxGrid(container) {
|
||||
container.replaceChildren(
|
||||
...ROLE_NAMES.map((name) => {
|
||||
const id = `${container.id}-${name}`;
|
||||
const row = el("div", { className: "checkbox-row" });
|
||||
const checkbox = el("input", { type: "checkbox", id, value: name });
|
||||
const label = el("label", { for: id, textContent: name });
|
||||
row.appendChild(checkbox);
|
||||
row.appendChild(label);
|
||||
return row;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function checkedValues(container) {
|
||||
return Array.from(container.querySelectorAll("input:checked")).map((c) => c.value);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Tabs
|
||||
// ---------------------------------------------------------------------
|
||||
@ -88,8 +113,11 @@
|
||||
users: loadUsersTab,
|
||||
groups: loadGroupsTab,
|
||||
hosts: loadHostsTab,
|
||||
credentials: loadCredentialsTab,
|
||||
roles: loadRolesTab,
|
||||
tokens: loadTokensTab,
|
||||
tenants: loadTenantsTab,
|
||||
authlog: loadAuthLogTab,
|
||||
audit: loadAuditTab,
|
||||
};
|
||||
const loadedTabs = new Set();
|
||||
@ -108,23 +136,57 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Mandanten-Auswahlfelder (fuer Super-Admins Dropdown aller Mandanten,
|
||||
// fuer Mandanten-Admins nur die eigenen -- kein Server-Request noetig,
|
||||
// steht bereits in meInfo.tenant_admin_of).
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
let cachedTenants = [];
|
||||
|
||||
async function populateTenantSelect(select) {
|
||||
if (meInfo.is_admin) {
|
||||
if (cachedTenants.length === 0) {
|
||||
const result = await getJson("/admin/tenants");
|
||||
cachedTenants = result;
|
||||
}
|
||||
select.replaceChildren(...optionsFromList(cachedTenants, "id", (t) => `${t.name} (#${t.id})`));
|
||||
} else {
|
||||
select.replaceChildren(
|
||||
...optionsFromList(meInfo.tenant_admin_of, "id", (t) => `${t.name} (#${t.id})`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Benutzer
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
async function loadUsersTab() {
|
||||
document.getElementById("uc-is-admin-row").classList.toggle("hidden", !meInfo.is_admin);
|
||||
await refreshUsers();
|
||||
}
|
||||
|
||||
let cachedUsers = [];
|
||||
let editingUserId = null;
|
||||
|
||||
function tenantName(tenantId) {
|
||||
if (tenantId === null || tenantId === undefined) return "-";
|
||||
const t = cachedTenants.find((x) => x.id === tenantId);
|
||||
return t ? t.name : `#${tenantId}`;
|
||||
}
|
||||
|
||||
async function refreshUsers() {
|
||||
if (meInfo.is_admin && cachedTenants.length === 0) {
|
||||
cachedTenants = await getJson("/admin/tenants");
|
||||
}
|
||||
cachedUsers = await getJson("/admin/users");
|
||||
const tbody = document.querySelector("#users-table tbody");
|
||||
fillTable(
|
||||
tbody,
|
||||
cachedUsers.map((u) => {
|
||||
const actions = el("td", {});
|
||||
actions.appendChild(actionButton("Bearbeiten", "btn-secondary", () => showUserEdit(u)));
|
||||
if (u.is_active) {
|
||||
actions.appendChild(
|
||||
actionButton("Deaktivieren", "btn-danger", async () => {
|
||||
@ -134,12 +196,25 @@
|
||||
})
|
||||
);
|
||||
}
|
||||
actions.appendChild(
|
||||
actionButton("Loeschen", "btn-danger", async () => {
|
||||
const result = await apiFetch(`/admin/users/${u.id}`, { method: "DELETE" });
|
||||
showBanner(
|
||||
result.hard_deleted
|
||||
? `Benutzer '${u.username}' vollstaendig geloescht.`
|
||||
: `Benutzer '${u.username}' hat Audit-Historie und wurde deaktiviert/anonymisiert.`,
|
||||
"ok"
|
||||
);
|
||||
await refreshUsers();
|
||||
})
|
||||
);
|
||||
return el("tr", {}, [
|
||||
el("td", { textContent: String(u.id) }),
|
||||
el("td", { textContent: u.username }),
|
||||
el("td", { textContent: u.is_admin ? "ja" : "nein" }),
|
||||
el("td", { textContent: u.is_active ? "ja" : "nein" }),
|
||||
el("td", { textContent: u.totp_enrolled ? "ja" : "nein" }),
|
||||
el("td", { textContent: tenantName(u.home_tenant_id) }),
|
||||
el("td", { textContent: u.created_at }),
|
||||
actions,
|
||||
]);
|
||||
@ -147,12 +222,37 @@
|
||||
);
|
||||
}
|
||||
|
||||
function showUserEdit(u) {
|
||||
editingUserId = u.id;
|
||||
document.getElementById("user-edit-panel").classList.remove("hidden");
|
||||
document.getElementById("user-edit-name").textContent = `${u.username} (#${u.id})`;
|
||||
document.getElementById("ue-is-admin-row").classList.toggle("hidden", !meInfo.is_admin);
|
||||
document.getElementById("ue-is-admin").checked = u.is_admin;
|
||||
document.getElementById("ue-is-active").checked = u.is_active;
|
||||
document.getElementById("ue-new-password").value = "";
|
||||
}
|
||||
|
||||
document.getElementById("user-edit-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const payload = { is_active: document.getElementById("ue-is-active").checked };
|
||||
if (meInfo.is_admin) payload.is_admin = document.getElementById("ue-is-admin").checked;
|
||||
const pw = document.getElementById("ue-new-password").value;
|
||||
if (pw) payload.new_password = pw;
|
||||
await sendJson(`/admin/users/${editingUserId}`, "PUT", payload);
|
||||
showBanner("Benutzer aktualisiert.", "ok");
|
||||
await refreshUsers();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("user-create-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const username = document.getElementById("uc-username").value.trim();
|
||||
const password = document.getElementById("uc-password").value;
|
||||
const isAdmin = document.getElementById("uc-is-admin").checked;
|
||||
const isAdmin = meInfo.is_admin && document.getElementById("uc-is-admin").checked;
|
||||
await sendJson("/admin/users", "POST", {
|
||||
username, initial_password: password, is_admin: isAdmin,
|
||||
});
|
||||
@ -171,8 +271,11 @@
|
||||
|
||||
let cachedGroups = [];
|
||||
let activeGroupId = null;
|
||||
let editingGroupId = null;
|
||||
|
||||
async function loadGroupsTab() {
|
||||
document.getElementById("gc-tenant-box").classList.toggle("hidden", !meInfo.is_admin && meInfo.tenant_admin_of.length <= 1);
|
||||
await populateTenantSelect(document.getElementById("gc-tenant"));
|
||||
await refreshGroups();
|
||||
}
|
||||
|
||||
@ -186,15 +289,44 @@
|
||||
el("td", { textContent: String(g.id) }),
|
||||
el("td", { textContent: g.name }),
|
||||
el("td", { textContent: g.description || "" }),
|
||||
el("td", { textContent: g.tenant_name }),
|
||||
el("td", { textContent: String(g.member_count) }),
|
||||
el("td", {}, [
|
||||
actionButton("Mitglieder", "btn-secondary", () => showGroupMembers(g)),
|
||||
actionButton("Bearbeiten", "btn-secondary", () => showGroupEdit(g)),
|
||||
actionButton("Loeschen", "btn-danger", async () => {
|
||||
await apiFetch(`/admin/user-groups/${g.id}`, { method: "DELETE" });
|
||||
showBanner(`Gruppe '${g.name}' geloescht.`, "ok");
|
||||
await refreshGroups();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function showGroupEdit(g) {
|
||||
editingGroupId = g.id;
|
||||
document.getElementById("group-edit-panel").classList.remove("hidden");
|
||||
document.getElementById("group-edit-name").textContent = g.name;
|
||||
document.getElementById("ge-name").value = g.name;
|
||||
document.getElementById("ge-description").value = g.description || "";
|
||||
}
|
||||
|
||||
document.getElementById("group-edit-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
await sendJson(`/admin/user-groups/${editingGroupId}`, "PUT", {
|
||||
name: document.getElementById("ge-name").value.trim(),
|
||||
description: document.getElementById("ge-description").value.trim() || null,
|
||||
});
|
||||
showBanner("Gruppe aktualisiert.", "ok");
|
||||
await refreshGroups();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
async function showGroupMembers(group) {
|
||||
activeGroupId = group.id;
|
||||
document.getElementById("group-members-panel").classList.remove("hidden");
|
||||
@ -232,7 +364,9 @@
|
||||
try {
|
||||
const name = document.getElementById("gc-name").value.trim();
|
||||
const description = document.getElementById("gc-description").value.trim() || null;
|
||||
await sendJson("/admin/user-groups", "POST", { name, description });
|
||||
const tenantSelect = document.getElementById("gc-tenant");
|
||||
const tenant_id = tenantSelect.value ? Number(tenantSelect.value) : null;
|
||||
await sendJson("/admin/user-groups", "POST", { name, description, tenant_id });
|
||||
showBanner(`Benutzergruppe '${name}' angelegt.`, "ok");
|
||||
ev.target.reset();
|
||||
await refreshGroups();
|
||||
@ -263,9 +397,12 @@
|
||||
let cachedHosts = [];
|
||||
let cachedSshKeys = [];
|
||||
let activeHostId = null;
|
||||
let editingHostGroupId = null;
|
||||
|
||||
async function loadHostsTab() {
|
||||
await Promise.all([refreshHostGroups(), refreshSshKeys()]);
|
||||
document.getElementById("hgc-tenant-box").classList.toggle("hidden", !meInfo.is_admin && meInfo.tenant_admin_of.length <= 1);
|
||||
await populateTenantSelect(document.getElementById("hgc-tenant"));
|
||||
await Promise.all([refreshHostGroups(), refreshSshKeysCache()]);
|
||||
await refreshHosts();
|
||||
}
|
||||
|
||||
@ -279,6 +416,15 @@
|
||||
el("td", { textContent: String(g.id) }),
|
||||
el("td", { textContent: g.name }),
|
||||
el("td", { textContent: g.description || "" }),
|
||||
el("td", { textContent: g.tenant_name }),
|
||||
el("td", {}, [
|
||||
actionButton("Bearbeiten", "btn-secondary", () => showHostGroupEdit(g)),
|
||||
actionButton("Loeschen", "btn-danger", async () => {
|
||||
await apiFetch(`/admin/host-groups/${g.id}`, { method: "DELETE" });
|
||||
showBanner(`Hostgruppe '${g.name}' geloescht.`, "ok");
|
||||
await refreshHostGroups();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
@ -286,6 +432,28 @@
|
||||
hcSelect.replaceChildren(...optionsFromList(cachedHostGroups, "id", (g) => `${g.name} (#${g.id})`));
|
||||
}
|
||||
|
||||
function showHostGroupEdit(g) {
|
||||
editingHostGroupId = g.id;
|
||||
document.getElementById("hostgroup-edit-panel").classList.remove("hidden");
|
||||
document.getElementById("hostgroup-edit-name").textContent = g.name;
|
||||
document.getElementById("hge-name").value = g.name;
|
||||
document.getElementById("hge-description").value = g.description || "";
|
||||
}
|
||||
|
||||
document.getElementById("hostgroup-edit-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
await sendJson(`/admin/host-groups/${editingHostGroupId}`, "PUT", {
|
||||
name: document.getElementById("hge-name").value.trim(),
|
||||
description: document.getElementById("hge-description").value.trim() || null,
|
||||
});
|
||||
showBanner("Hostgruppe aktualisiert.", "ok");
|
||||
await refreshHostGroups();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshHosts() {
|
||||
cachedHosts = await getJson("/admin/hosts");
|
||||
const groupName = (id) => (cachedHostGroups.find((g) => g.id === id) || {}).name || `#${id}`;
|
||||
@ -300,40 +468,124 @@
|
||||
el("td", { textContent: h.protocol }),
|
||||
el("td", { textContent: h.os_type }),
|
||||
el("td", { textContent: groupName(h.host_group_id) }),
|
||||
el("td", {}, [actionButton("Details", "btn-secondary", () => showHostDetail(h))]),
|
||||
el("td", { textContent: h.tenant_name }),
|
||||
el("td", {}, [el("span", { className: `badge ${h.is_active ? "ok" : "danger"}`, textContent: h.is_active ? "aktiv" : "inaktiv" })]),
|
||||
el("td", {}, [
|
||||
actionButton("Details", "btn-secondary", () => showHostDetail(h.id)),
|
||||
actionButton(h.is_active ? "Loeschen" : "Reaktivieren", h.is_active ? "btn-danger" : "btn-secondary", async () => {
|
||||
if (h.is_active) {
|
||||
await apiFetch(`/admin/hosts/${h.id}`, { method: "DELETE" });
|
||||
showBanner(`Host '${h.hostname}' deaktiviert/geloescht.`, "ok");
|
||||
} else {
|
||||
await sendJson(`/admin/hosts/${h.id}`, "PUT", { is_active: true });
|
||||
showBanner(`Host '${h.hostname}' reaktiviert.`, "ok");
|
||||
}
|
||||
await refreshHosts();
|
||||
if (activeHostId === h.id) document.getElementById("host-detail-panel").classList.add("hidden");
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function refreshSshKeys() {
|
||||
async function refreshSshKeysCache() {
|
||||
cachedSshKeys = await getJson("/admin/ssh-keys");
|
||||
const tbody = document.querySelector("#ssh-keys-table tbody");
|
||||
fillTable(
|
||||
tbody,
|
||||
cachedSshKeys.map((k) =>
|
||||
el("tr", {}, [
|
||||
el("td", { textContent: String(k.id) }),
|
||||
el("td", { textContent: k.label }),
|
||||
el("td", { textContent: k.key_type }),
|
||||
el("td", { textContent: k.owner_user_id === null ? "-" : String(k.owner_user_id) }),
|
||||
el("td", { textContent: k.created_at }),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function showHostDetail(host) {
|
||||
activeHostId = host.id;
|
||||
document.getElementById("host-detail-panel").classList.remove("hidden");
|
||||
document.getElementById("host-detail-name").textContent = `${host.hostname} (#${host.id})`;
|
||||
document.getElementById("host-detail-ssh").classList.toggle("hidden", host.protocol !== "ssh");
|
||||
document.getElementById("host-detail-rdp").classList.toggle("hidden", host.protocol !== "rdp");
|
||||
document.getElementById("host-key-result").classList.add("hidden");
|
||||
const keySelect = document.getElementById("hkm-key-select");
|
||||
keySelect.replaceChildren(...optionsFromList(cachedSshKeys, "id", (k) => `${k.label} (#${k.id})`));
|
||||
function setDetailBanner(message, type) {
|
||||
const box = document.getElementById("host-detail-banner");
|
||||
if (!message) {
|
||||
box.replaceChildren();
|
||||
return;
|
||||
}
|
||||
box.replaceChildren(el("div", { className: `banner ${type === "error" ? "error" : "ok"}`, textContent: message }));
|
||||
}
|
||||
|
||||
async function showHostDetail(hostId) {
|
||||
setDetailBanner("", "ok");
|
||||
try {
|
||||
const host = await getJson(`/admin/hosts/${hostId}`);
|
||||
activeHostId = host.id;
|
||||
document.getElementById("host-detail-panel").classList.remove("hidden");
|
||||
document.getElementById("host-detail-name").textContent = `${host.hostname} (#${host.id}, ${host.tenant_name})`;
|
||||
|
||||
document.getElementById("he-hostname").value = host.hostname;
|
||||
document.getElementById("he-address").value = host.address;
|
||||
document.getElementById("he-port").value = String(host.port);
|
||||
document.getElementById("he-ssh-username").value = host.ssh_username || "";
|
||||
document.getElementById("he-rdp-username").value = host.rdp_username || "";
|
||||
document.getElementById("he-rdp-domain").value = host.rdp_domain || "";
|
||||
document.getElementById("he-clipboard").checked = host.clipboard_enabled;
|
||||
document.getElementById("he-filetransfer").checked = host.file_transfer_enabled;
|
||||
document.getElementById("he-nla").checked = host.rdp_require_nla;
|
||||
document.getElementById("he-active").checked = host.is_active;
|
||||
|
||||
const isRdp = host.protocol === "rdp";
|
||||
document.getElementById("he-ssh-username-box").classList.toggle("hidden", isRdp);
|
||||
document.getElementById("he-rdp-username-box").classList.toggle("hidden", !isRdp);
|
||||
document.getElementById("he-rdp-domain-box").classList.toggle("hidden", !isRdp);
|
||||
document.getElementById("he-nla-box").classList.toggle("hidden", !isRdp);
|
||||
document.getElementById("host-detail-ssh").classList.toggle("hidden", isRdp);
|
||||
document.getElementById("host-detail-rdp").classList.toggle("hidden", !isRdp);
|
||||
|
||||
document.getElementById("host-key-result").classList.add("hidden");
|
||||
if (host.ssh_host_key_fingerprint) {
|
||||
const box = document.getElementById("host-key-result");
|
||||
box.textContent = `Aktueller Fingerprint: ${host.ssh_host_key_fingerprint}`;
|
||||
box.classList.remove("hidden");
|
||||
}
|
||||
|
||||
const keysTbody = document.querySelector("#host-ssh-keys-table tbody");
|
||||
fillTable(
|
||||
keysTbody,
|
||||
host.ssh_keys.map((k) =>
|
||||
el("tr", {}, [
|
||||
el("td", { textContent: String(k.id) }),
|
||||
el("td", { textContent: k.label }),
|
||||
el("td", {}, [
|
||||
actionButton("Entfernen", "btn-danger", async () => {
|
||||
await apiFetch(`/admin/hosts/${host.id}/ssh-keys/${k.id}`, { method: "DELETE" });
|
||||
await showHostDetail(host.id);
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
const keySelect = document.getElementById("hkm-key-select");
|
||||
keySelect.replaceChildren(...optionsFromList(cachedSshKeys, "id", (k) => `${k.label} (#${k.id})`));
|
||||
|
||||
document.getElementById("rdp-cred-status").textContent = host.rdp_credentials_set
|
||||
? `RDP-Passwort ist gesetzt (zuletzt aktualisiert: ${host.rdp_credentials_updated_at}).`
|
||||
: "Noch kein RDP-Passwort gesetzt.";
|
||||
} catch (err) {
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("host-edit-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
await sendJson(`/admin/hosts/${activeHostId}`, "PUT", {
|
||||
hostname: document.getElementById("he-hostname").value.trim(),
|
||||
address: document.getElementById("he-address").value.trim(),
|
||||
port: Number(document.getElementById("he-port").value),
|
||||
ssh_username: document.getElementById("he-ssh-username").value.trim() || null,
|
||||
rdp_username: document.getElementById("he-rdp-username").value.trim() || null,
|
||||
rdp_domain: document.getElementById("he-rdp-domain").value.trim() || null,
|
||||
clipboard_enabled: document.getElementById("he-clipboard").checked,
|
||||
file_transfer_enabled: document.getElementById("he-filetransfer").checked,
|
||||
rdp_require_nla: document.getElementById("he-nla").checked,
|
||||
is_active: document.getElementById("he-active").checked,
|
||||
});
|
||||
showBanner("Host aktualisiert.", "ok");
|
||||
await refreshHosts();
|
||||
await showHostDetail(activeHostId);
|
||||
} catch (err) {
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("hc-protocol").addEventListener("change", (ev) => {
|
||||
const isRdp = ev.target.value === "rdp";
|
||||
document.getElementById("hc-ssh-username-box").classList.toggle("hidden", isRdp);
|
||||
@ -349,7 +601,9 @@
|
||||
try {
|
||||
const name = document.getElementById("hgc-name").value.trim();
|
||||
const description = document.getElementById("hgc-description").value.trim() || null;
|
||||
await sendJson("/admin/host-groups", "POST", { name, description });
|
||||
const tenantSelect = document.getElementById("hgc-tenant");
|
||||
const tenant_id = tenantSelect.value ? Number(tenantSelect.value) : null;
|
||||
await sendJson("/admin/host-groups", "POST", { name, description, tenant_id });
|
||||
showBanner(`Hostgruppe '${name}' angelegt.`, "ok");
|
||||
ev.target.reset();
|
||||
await refreshHostGroups();
|
||||
@ -396,7 +650,7 @@
|
||||
box.classList.remove("hidden");
|
||||
showBanner("Host-Key ermittelt und gespeichert.", "ok");
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@ -406,8 +660,9 @@
|
||||
const keyId = document.getElementById("hkm-key-select").value;
|
||||
await sendJson(`/admin/hosts/${activeHostId}/ssh-keys/${keyId}`, "POST", {});
|
||||
showBanner("SSH-Key zugeordnet.", "ok");
|
||||
await showHostDetail(activeHostId);
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@ -418,6 +673,89 @@
|
||||
await sendJson(`/admin/hosts/${activeHostId}/rdp-credentials`, "PUT", { password });
|
||||
showBanner("RDP-Zugangsdaten gespeichert.", "ok");
|
||||
ev.target.reset();
|
||||
await showHostDetail(activeHostId);
|
||||
} catch (err) {
|
||||
setDetailBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Zugangsdaten (SSH-Keys + RDP-Passwoerter)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
let editingSshKeyId = null;
|
||||
|
||||
async function loadCredentialsTab() {
|
||||
document.getElementById("skc-tenant-box").classList.toggle("hidden", !meInfo.is_admin && meInfo.tenant_admin_of.length <= 1);
|
||||
await populateTenantSelect(document.getElementById("skc-tenant"));
|
||||
await Promise.all([refreshSshKeys(), refreshRdpCredentials()]);
|
||||
}
|
||||
|
||||
async function refreshSshKeys() {
|
||||
cachedSshKeys = await getJson("/admin/ssh-keys");
|
||||
const tbody = document.querySelector("#ssh-keys-table tbody");
|
||||
fillTable(
|
||||
tbody,
|
||||
cachedSshKeys.map((k) =>
|
||||
el("tr", {}, [
|
||||
el("td", { textContent: String(k.id) }),
|
||||
el("td", { textContent: k.label }),
|
||||
el("td", { textContent: k.key_type }),
|
||||
el("td", { textContent: k.owner_user_id === null ? "-" : String(k.owner_user_id) }),
|
||||
el("td", { textContent: k.tenant_name }),
|
||||
el("td", { textContent: k.created_at }),
|
||||
el("td", { textContent: k.rotated_at || "-" }),
|
||||
el("td", {}, [
|
||||
actionButton("Bearbeiten", "btn-secondary", () => showSshKeyEdit(k)),
|
||||
actionButton("Loeschen", "btn-danger", async () => {
|
||||
const result = await apiFetch(`/admin/ssh-keys/${k.id}`, { method: "DELETE" });
|
||||
const suffix = result.unmapped_host_ids.length
|
||||
? ` (Zuordnung zu Host(s) ${result.unmapped_host_ids.join(", ")} entfernt)`
|
||||
: "";
|
||||
showBanner(`SSH-Key '${k.label}' geloescht.${suffix}`, "ok");
|
||||
await refreshSshKeys();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function showSshKeyEdit(k) {
|
||||
editingSshKeyId = k.id;
|
||||
document.getElementById("ssh-key-edit-panel").classList.remove("hidden");
|
||||
document.getElementById("ssh-key-edit-name").textContent = `${k.label} (#${k.id})`;
|
||||
document.getElementById("ske-label").value = k.label;
|
||||
document.getElementById("ske-owner").value = k.owner_user_id === null ? "" : String(k.owner_user_id);
|
||||
document.getElementById("skr-type").value = k.key_type;
|
||||
}
|
||||
|
||||
document.getElementById("ssh-key-edit-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const ownerRaw = document.getElementById("ske-owner").value.trim();
|
||||
await sendJson(`/admin/ssh-keys/${editingSshKeyId}`, "PUT", {
|
||||
label: document.getElementById("ske-label").value.trim() || null,
|
||||
owner_user_id: ownerRaw ? Number(ownerRaw) : null,
|
||||
});
|
||||
showBanner("SSH-Key aktualisiert.", "ok");
|
||||
await refreshSshKeys();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("ssh-key-rotate-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
await sendJson(`/admin/ssh-keys/${editingSshKeyId}`, "PUT", {
|
||||
key_type: document.getElementById("skr-type").value,
|
||||
private_key_pem: document.getElementById("skr-private").value,
|
||||
public_key: document.getElementById("skr-public").value,
|
||||
});
|
||||
showBanner("SSH-Key rotiert.", "ok");
|
||||
ev.target.reset();
|
||||
await refreshSshKeys();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
@ -427,42 +765,61 @@
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const ownerRaw = document.getElementById("skc-owner").value.trim();
|
||||
const tenantSelect = document.getElementById("skc-tenant");
|
||||
const payload = {
|
||||
label: document.getElementById("skc-label").value.trim(),
|
||||
owner_user_id: ownerRaw ? Number(ownerRaw) : null,
|
||||
key_type: document.getElementById("skc-type").value,
|
||||
private_key_pem: document.getElementById("skc-private").value,
|
||||
public_key: document.getElementById("skc-public").value,
|
||||
tenant_id: tenantSelect.value ? Number(tenantSelect.value) : null,
|
||||
};
|
||||
await sendJson("/admin/ssh-keys", "POST", payload);
|
||||
showBanner(`SSH-Key '${payload.label}' angelegt.`, "ok");
|
||||
ev.target.reset();
|
||||
await refreshSshKeys();
|
||||
if (activeHostId !== null) {
|
||||
const keySelect = document.getElementById("hkm-key-select");
|
||||
keySelect.replaceChildren(...optionsFromList(cachedSshKeys, "id", (k) => `${k.label} (#${k.id})`));
|
||||
}
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshRdpCredentials() {
|
||||
const rows = await getJson("/admin/rdp-credentials");
|
||||
const tbody = document.querySelector("#rdp-creds-table tbody");
|
||||
fillTable(
|
||||
tbody,
|
||||
rows.map((r) =>
|
||||
el("tr", {}, [
|
||||
el("td", { textContent: r.hostname }),
|
||||
el("td", { textContent: r.address }),
|
||||
el("td", { textContent: r.host_group_name }),
|
||||
el("td", {}, [el("span", { className: `badge ${r.credentials_set ? "ok" : ""}`, textContent: r.credentials_set ? "gesetzt" : "nicht gesetzt" })]),
|
||||
el("td", { textContent: r.updated_at || "-" }),
|
||||
el("td", {}, [
|
||||
actionButton("Zum Host", "btn-secondary", async () => {
|
||||
document.querySelector('.tab-btn[data-tab="hosts"]').click();
|
||||
await showHostDetail(r.host_id);
|
||||
}),
|
||||
...(r.credentials_set
|
||||
? [actionButton("Entfernen", "btn-danger", async () => {
|
||||
await apiFetch(`/admin/hosts/${r.host_id}/rdp-credentials`, { method: "DELETE" });
|
||||
showBanner("RDP-Passwort entfernt.", "ok");
|
||||
await refreshRdpCredentials();
|
||||
})]
|
||||
: []),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Rollen
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
function roleOptions() {
|
||||
return ROLE_NAMES.map((name) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
return opt;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRolesTab() {
|
||||
document.getElementById("rg-role").replaceChildren(...roleOptions());
|
||||
document.getElementById("grg-role").replaceChildren(...roleOptions());
|
||||
roleCheckboxGrid(document.getElementById("rg-role-grid"));
|
||||
roleCheckboxGrid(document.getElementById("grg-role-grid"));
|
||||
await refreshSharedSelects();
|
||||
await Promise.all([refreshRoleGrants(), refreshGroupRoleGrants()]);
|
||||
}
|
||||
@ -518,13 +875,16 @@
|
||||
document.getElementById("role-grant-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const role_names = checkedValues(document.getElementById("rg-role-grid"));
|
||||
if (role_names.length === 0) throw new Error("Mindestens eine Rolle auswaehlen.");
|
||||
await sendJson("/admin/roles/grant", "POST", {
|
||||
user_id: Number(document.getElementById("rg-user").value),
|
||||
host_group_id: Number(document.getElementById("rg-hostgroup").value),
|
||||
role_name: document.getElementById("rg-role").value,
|
||||
role_names,
|
||||
expires_at: document.getElementById("rg-expires").value.trim() || null,
|
||||
});
|
||||
showBanner("Rolle vergeben.", "ok");
|
||||
showBanner(`Rolle(n) vergeben: ${role_names.join(", ")}.`, "ok");
|
||||
document.querySelectorAll("#rg-role-grid input:checked").forEach((c) => { c.checked = false; });
|
||||
await refreshRoleGrants();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
@ -534,13 +894,16 @@
|
||||
document.getElementById("group-role-grant-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const role_names = checkedValues(document.getElementById("grg-role-grid"));
|
||||
if (role_names.length === 0) throw new Error("Mindestens eine Rolle auswaehlen.");
|
||||
await sendJson("/admin/group-roles/grant", "POST", {
|
||||
user_group_id: Number(document.getElementById("grg-group").value),
|
||||
host_group_id: Number(document.getElementById("grg-hostgroup").value),
|
||||
role_name: document.getElementById("grg-role").value,
|
||||
role_names,
|
||||
expires_at: document.getElementById("grg-expires").value.trim() || null,
|
||||
});
|
||||
showBanner("Rolle an Gruppe vergeben.", "ok");
|
||||
showBanner(`Rolle(n) an Gruppe vergeben: ${role_names.join(", ")}.`, "ok");
|
||||
document.querySelectorAll("#grg-role-grid input:checked").forEach((c) => { c.checked = false; });
|
||||
await refreshGroupRoleGrants();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
@ -570,6 +933,8 @@
|
||||
})
|
||||
);
|
||||
}
|
||||
document.getElementById("tc-tenant-box").classList.toggle("hidden", !meInfo.is_admin && meInfo.tenant_admin_of.length <= 1);
|
||||
await populateTenantSelect(document.getElementById("tc-tenant"));
|
||||
await refreshSharedSelects();
|
||||
await refreshTokens();
|
||||
}
|
||||
@ -599,6 +964,7 @@
|
||||
el("td", { textContent: t.label }),
|
||||
el("td", { textContent: t.username }),
|
||||
el("td", { textContent: t.prefix }),
|
||||
el("td", { textContent: t.tenant_name }),
|
||||
scopesCell,
|
||||
el("td", { textContent: t.last_used_at || "nie" }),
|
||||
el("td", { textContent: status }),
|
||||
@ -615,11 +981,13 @@
|
||||
if (scopes.length === 0) {
|
||||
throw new Error("Mindestens einen Scope auswaehlen.");
|
||||
}
|
||||
const tenantSelect = document.getElementById("tc-tenant");
|
||||
const payload = {
|
||||
label: document.getElementById("tc-label").value.trim(),
|
||||
user_id: Number(document.getElementById("tc-user").value),
|
||||
scopes,
|
||||
expires_at: document.getElementById("tc-expires").value.trim() || null,
|
||||
tenant_id: tenantSelect.value ? Number(tenantSelect.value) : null,
|
||||
};
|
||||
const result = await sendJson("/admin/tokens", "POST", payload);
|
||||
const box = document.getElementById("token-reveal-box");
|
||||
@ -634,6 +1002,156 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Mandanten (nur Super-Admin -- Tab-Button ist fuer alle anderen hidden)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
let editingTenantId = null;
|
||||
let activeTenantAdminsId = null;
|
||||
|
||||
async function loadTenantsTab() {
|
||||
await refreshTenants();
|
||||
}
|
||||
|
||||
async function refreshTenants() {
|
||||
cachedTenants = await getJson("/admin/tenants");
|
||||
const tbody = document.querySelector("#tenants-table tbody");
|
||||
fillTable(
|
||||
tbody,
|
||||
cachedTenants.map((t) =>
|
||||
el("tr", {}, [
|
||||
el("td", { textContent: String(t.id) }),
|
||||
el("td", { textContent: t.name }),
|
||||
el("td", { textContent: t.description || "" }),
|
||||
el("td", { textContent: t.is_active ? "ja" : "nein" }),
|
||||
el("td", { textContent: String(t.host_group_count) }),
|
||||
el("td", { textContent: String(t.user_group_count) }),
|
||||
el("td", {}, [
|
||||
actionButton("Admins", "btn-secondary", () => showTenantAdmins(t)),
|
||||
actionButton("Bearbeiten", "btn-secondary", () => showTenantEdit(t)),
|
||||
actionButton("Loeschen", "btn-danger", async () => {
|
||||
await apiFetch(`/admin/tenants/${t.id}`, { method: "DELETE" });
|
||||
showBanner(`Mandant '${t.name}' geloescht.`, "ok");
|
||||
await refreshTenants();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function showTenantEdit(t) {
|
||||
editingTenantId = t.id;
|
||||
document.getElementById("tenant-edit-panel").classList.remove("hidden");
|
||||
document.getElementById("tenant-edit-name").textContent = t.name;
|
||||
document.getElementById("tne-name").value = t.name;
|
||||
document.getElementById("tne-description").value = t.description || "";
|
||||
document.getElementById("tne-active").checked = t.is_active;
|
||||
}
|
||||
|
||||
document.getElementById("tenant-edit-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
await sendJson(`/admin/tenants/${editingTenantId}`, "PUT", {
|
||||
name: document.getElementById("tne-name").value.trim(),
|
||||
description: document.getElementById("tne-description").value.trim() || null,
|
||||
is_active: document.getElementById("tne-active").checked,
|
||||
});
|
||||
showBanner("Mandant aktualisiert.", "ok");
|
||||
await refreshTenants();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
async function showTenantAdmins(t) {
|
||||
activeTenantAdminsId = t.id;
|
||||
document.getElementById("tenant-admins-panel").classList.remove("hidden");
|
||||
document.getElementById("tenant-admins-name").textContent = t.name;
|
||||
await refreshTenantAdmins();
|
||||
const select = document.getElementById("ta-user-select");
|
||||
if (cachedUsers.length === 0) await refreshUsers();
|
||||
select.replaceChildren(...optionsFromList(cachedUsers, "id", (u) => `${u.username} (#${u.id})`));
|
||||
}
|
||||
|
||||
async function refreshTenantAdmins() {
|
||||
const admins = await getJson(`/admin/tenants/${activeTenantAdminsId}/admins`);
|
||||
const tbody = document.querySelector("#tenant-admins-table tbody");
|
||||
fillTable(
|
||||
tbody,
|
||||
admins.map((a) =>
|
||||
el("tr", {}, [
|
||||
el("td", { textContent: String(a.user_id) }),
|
||||
el("td", { textContent: a.username }),
|
||||
el("td", { textContent: a.granted_at }),
|
||||
el("td", {}, [
|
||||
actionButton("Entfernen", "btn-danger", async () => {
|
||||
await apiFetch(`/admin/tenants/${activeTenantAdminsId}/admins/${a.user_id}`, { method: "DELETE" });
|
||||
await refreshTenantAdmins();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
document.getElementById("tenant-create-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const name = document.getElementById("tnc-name").value.trim();
|
||||
const description = document.getElementById("tnc-description").value.trim() || null;
|
||||
await sendJson("/admin/tenants", "POST", { name, description });
|
||||
showBanner(`Mandant '${name}' angelegt.`, "ok");
|
||||
ev.target.reset();
|
||||
await refreshTenants();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("tenant-admin-add-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const userId = Number(document.getElementById("ta-user-select").value);
|
||||
await sendJson(`/admin/tenants/${activeTenantAdminsId}/admins`, "POST", { user_id: userId });
|
||||
showBanner("Mandanten-Admin hinzugefuegt.", "ok");
|
||||
await refreshTenantAdmins();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Login-Verlauf (Auth-Log) -- clientseitig aus dem Audit-Log gefiltert
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
async function loadAuthLogTab() {
|
||||
await refreshAuthLog();
|
||||
}
|
||||
|
||||
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");
|
||||
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 }),
|
||||
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",
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Audit-Log
|
||||
// ---------------------------------------------------------------------
|
||||
@ -710,11 +1228,16 @@
|
||||
|
||||
async function main() {
|
||||
const me = await getJson("/auth/me");
|
||||
if (!me.is_admin) {
|
||||
if (!me.is_any_admin) {
|
||||
window.location.href = "/dashboard";
|
||||
return;
|
||||
}
|
||||
document.getElementById("whoami").textContent = `${me.username} (Admin)`;
|
||||
meInfo = me;
|
||||
const roleLabel = me.is_admin
|
||||
? "Super-Admin"
|
||||
: `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);
|
||||
loadedTabs.add("users");
|
||||
await loadUsersTab();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user