umbau 1.0
This commit is contained in:
@ -1,13 +1,14 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const ROLE_NAMES = [
|
||||
"ssh_connect", "rdp_connect", "file_transfer", "clipboard",
|
||||
"session_recording_view", "admin_hostgroup", "credentials_view", "credentials_manage",
|
||||
];
|
||||
// Wird beim ersten Aufruf von loadRolesTab() aus GET /admin/roles/names
|
||||
// gefuellt (Teil D.6 Schritt 1: keine Hartkodierung mehr, siehe
|
||||
// Umsetzungsauftrag_Sonnet5.md D.5 -- die Rollenliste soll aus der DB
|
||||
// kommen, nicht aus dem Frontend-Code dupliziert werden).
|
||||
let ROLE_NAMES = [];
|
||||
|
||||
const bannerBox = document.getElementById("banner-box");
|
||||
let meInfo = { is_admin: false, tenant_admin_of: [] };
|
||||
let meInfo = { is_admin: false };
|
||||
|
||||
function showBanner(message, type) {
|
||||
const div = document.createElement("div");
|
||||
@ -111,7 +112,6 @@
|
||||
credentials: loadCredentialsTab,
|
||||
roles: loadRolesTab,
|
||||
tokens: loadTokensTab,
|
||||
tenants: loadTenantsTab,
|
||||
sessions: loadSessionsTab,
|
||||
connlog: loadConnLogTab,
|
||||
audit: loadAuditTab,
|
||||
@ -139,28 +139,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 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
|
||||
// ---------------------------------------------------------------------
|
||||
@ -173,16 +151,7 @@
|
||||
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(
|
||||
@ -217,7 +186,6 @@
|
||||
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,
|
||||
]);
|
||||
@ -277,8 +245,6 @@
|
||||
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();
|
||||
}
|
||||
|
||||
@ -292,7 +258,6 @@
|
||||
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)),
|
||||
@ -367,9 +332,7 @@
|
||||
try {
|
||||
const name = document.getElementById("gc-name").value.trim();
|
||||
const description = document.getElementById("gc-description").value.trim() || null;
|
||||
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 });
|
||||
await sendJson("/admin/user-groups", "POST", { name, description });
|
||||
showBanner(`Benutzergruppe '${name}' angelegt.`, "ok");
|
||||
ev.target.reset();
|
||||
await refreshGroups();
|
||||
@ -400,12 +363,11 @@
|
||||
let cachedHosts = [];
|
||||
let cachedSshKeys = [];
|
||||
let cachedRdpCredentials = [];
|
||||
let cachedSshPasswordCredentials = [];
|
||||
let activeHostId = null;
|
||||
let editingHostGroupId = null;
|
||||
|
||||
async function loadHostsTab() {
|
||||
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(), refreshRdpCredentialsCache()]);
|
||||
await refreshHosts();
|
||||
}
|
||||
@ -420,7 +382,6 @@
|
||||
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 () => {
|
||||
@ -472,7 +433,6 @@
|
||||
el("td", { textContent: h.protocol }),
|
||||
el("td", { textContent: h.os_type }),
|
||||
el("td", { textContent: groupName(h.host_group_id) }),
|
||||
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)),
|
||||
@ -501,6 +461,10 @@
|
||||
cachedRdpCredentials = await getJson("/admin/rdp-credentials");
|
||||
}
|
||||
|
||||
async function refreshSshPasswordCredentialsCache() {
|
||||
cachedSshPasswordCredentials = await getJson("/admin/ssh-password-credentials");
|
||||
}
|
||||
|
||||
function setDetailBanner(message, type) {
|
||||
const box = document.getElementById("host-detail-banner");
|
||||
if (!message) {
|
||||
@ -540,7 +504,7 @@
|
||||
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("host-detail-name").textContent = `${host.hostname} (#${host.id})`;
|
||||
|
||||
document.getElementById("he-hostname").value = host.hostname;
|
||||
document.getElementById("he-address").value = host.address;
|
||||
@ -640,9 +604,7 @@
|
||||
try {
|
||||
const name = document.getElementById("hgc-name").value.trim();
|
||||
const description = document.getElementById("hgc-description").value.trim() || null;
|
||||
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 });
|
||||
await sendJson("/admin/host-groups", "POST", { name, description });
|
||||
showBanner(`Hostgruppe '${name}' angelegt.`, "ok");
|
||||
ev.target.reset();
|
||||
await refreshHostGroups();
|
||||
@ -760,12 +722,6 @@
|
||||
let editingSshKeyId = null;
|
||||
|
||||
async function loadCredentialsTab() {
|
||||
document.getElementById("skc-tenant-box").classList.toggle("hidden", !meInfo.is_admin && meInfo.tenant_admin_of.length <= 1);
|
||||
document.getElementById("rcc-tenant-box").classList.toggle("hidden", !meInfo.is_admin && meInfo.tenant_admin_of.length <= 1);
|
||||
await Promise.all([
|
||||
populateTenantSelect(document.getElementById("skc-tenant")),
|
||||
populateTenantSelect(document.getElementById("rcc-tenant")),
|
||||
]);
|
||||
await Promise.all([refreshSshKeys(), refreshRdpCredentials()]);
|
||||
}
|
||||
|
||||
@ -780,7 +736,6 @@
|
||||
el("td", { textContent: k.label }),
|
||||
el("td", { textContent: k.username || "-" }),
|
||||
el("td", { textContent: k.key_type }),
|
||||
el("td", { textContent: k.tenant_name }),
|
||||
el("td", { textContent: k.has_passphrase ? "hinterlegt" : "-" }),
|
||||
el("td", { textContent: k.created_at }),
|
||||
el("td", { textContent: k.rotated_at || "-" }),
|
||||
@ -899,7 +854,6 @@
|
||||
document.getElementById("ssh-key-create-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const tenantSelect = document.getElementById("skc-tenant");
|
||||
const payload = {
|
||||
label: document.getElementById("skc-label").value.trim(),
|
||||
username: document.getElementById("skc-username").value.trim() || null,
|
||||
@ -908,7 +862,6 @@
|
||||
public_key: document.getElementById("skc-public").value,
|
||||
// Leeres Feld -> null: der Schluessel gilt dann als unverschluesselt.
|
||||
passphrase: document.getElementById("skc-passphrase").value || null,
|
||||
tenant_id: tenantSelect.value ? Number(tenantSelect.value) : null,
|
||||
};
|
||||
await sendJson("/admin/ssh-keys", "POST", payload);
|
||||
showBanner(`SSH-Key '${payload.label}' angelegt.`, "ok");
|
||||
@ -932,7 +885,6 @@
|
||||
el("td", { textContent: r.label }),
|
||||
el("td", { textContent: r.username || "-" }),
|
||||
el("td", { textContent: r.domain || "-" }),
|
||||
el("td", { textContent: r.tenant_name }),
|
||||
el("td", {
|
||||
textContent: r.assigned_hosts.length
|
||||
? r.assigned_hosts.map((h) => h.hostname).join(", ")
|
||||
@ -998,13 +950,11 @@
|
||||
document.getElementById("rdp-cred-create-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const tenantSelect = document.getElementById("rcc-tenant");
|
||||
const payload = {
|
||||
label: document.getElementById("rcc-label").value.trim(),
|
||||
username: document.getElementById("rcc-username").value.trim(),
|
||||
domain: document.getElementById("rcc-domain").value.trim() || null,
|
||||
password: document.getElementById("rcc-password").value,
|
||||
tenant_id: tenantSelect.value ? Number(tenantSelect.value) : null,
|
||||
};
|
||||
await sendJson("/admin/rdp-credentials", "POST", payload);
|
||||
showBanner(`RDP-Zugangsdaten '${payload.label}' angelegt.`, "ok");
|
||||
@ -1020,13 +970,21 @@
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
async function loadRolesTab() {
|
||||
roleCheckboxGrid(document.getElementById("rg-role-grid"));
|
||||
if (ROLE_NAMES.length === 0) {
|
||||
ROLE_NAMES = await getJson("/admin/roles/names");
|
||||
}
|
||||
roleCheckboxGrid(document.getElementById("grg-role-grid"));
|
||||
await refreshSharedSelects();
|
||||
await Promise.all([refreshRoleGrants(), refreshGroupRoleGrants()]);
|
||||
await Promise.all([refreshSshKeysCache(), refreshRdpCredentialsCache(), refreshSshPasswordCredentialsCache()]);
|
||||
populateCredentialSelect();
|
||||
await Promise.all([refreshRoleGrants(), refreshGroupRoleGrants(), refreshGroupCredentialGrants()]);
|
||||
}
|
||||
|
||||
async function refreshRoleGrants() {
|
||||
// Teil D Schritt 5: abgeleitete Sicht (GET /admin/roles liefert seither
|
||||
// effektive Rechte statt einer Vergabe-Tabelle) -- nicht editierbar,
|
||||
// daher keine Aktionen-Spalte mehr. Aendern laeuft ausschliesslich ueber
|
||||
// die Gruppenvergabe (refreshGroupRoleGrants() unten).
|
||||
const grants = await getJson("/admin/roles");
|
||||
const tbody = document.querySelector("#role-grants-table tbody");
|
||||
fillTable(
|
||||
@ -1036,15 +994,8 @@
|
||||
el("td", { textContent: g.username }),
|
||||
el("td", { textContent: g.host_group_name }),
|
||||
el("td", { textContent: g.role_name }),
|
||||
el("td", { textContent: g.via_group_name }),
|
||||
el("td", { textContent: g.expires_at || "-" }),
|
||||
el("td", {}, [
|
||||
actionButton("Entziehen", "btn-danger", async () => {
|
||||
await sendJson("/admin/roles/revoke", "POST", {
|
||||
user_id: g.user_id, host_group_id: g.host_group_id, role_name: g.role_name,
|
||||
});
|
||||
await refreshRoleGrants();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
@ -1074,25 +1025,6 @@
|
||||
);
|
||||
}
|
||||
|
||||
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_names,
|
||||
expires_at: document.getElementById("rg-expires").value.trim() || null,
|
||||
});
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("group-role-grant-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
@ -1112,6 +1044,84 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Zugangsdaten-Freigabe an Benutzergruppen (Achse B, Teil D.6 Schritt 5)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
const CREDENTIAL_KIND_LABELS = {
|
||||
ssh_key: "SSH-Schluessel",
|
||||
rdp_credential: "RDP-Zugangsdaten",
|
||||
ssh_password_credential: "SSH-Passwort",
|
||||
};
|
||||
|
||||
function credentialListForKind(kind) {
|
||||
if (kind === "ssh_key") return cachedSshKeys;
|
||||
if (kind === "rdp_credential") return cachedRdpCredentials;
|
||||
return cachedSshPasswordCredentials;
|
||||
}
|
||||
|
||||
function credentialLabel(kind, credentialId) {
|
||||
const item = credentialListForKind(kind).find((c) => c.id === credentialId);
|
||||
return item ? `${item.label} (#${item.id})` : `#${credentialId}`;
|
||||
}
|
||||
|
||||
function populateCredentialSelect() {
|
||||
const kind = document.getElementById("gcg-kind").value;
|
||||
const select = document.getElementById("gcg-credential");
|
||||
select.replaceChildren(...optionsFromList(credentialListForKind(kind), "id", (c) => `${c.label} (#${c.id})`));
|
||||
}
|
||||
|
||||
document.getElementById("gcg-kind").addEventListener("change", populateCredentialSelect);
|
||||
|
||||
async function refreshGroupCredentialGrants() {
|
||||
const kinds = ["ssh_key", "rdp_credential", "ssh_password_credential"];
|
||||
const perKind = await Promise.all(kinds.map((kind) => getJson(`/admin/group-credentials/${kind}`)));
|
||||
const rows = [];
|
||||
kinds.forEach((kind, idx) => {
|
||||
for (const g of perKind[idx]) rows.push({ kind, ...g });
|
||||
});
|
||||
rows.sort((a, b) => a.user_group_name.localeCompare(b.user_group_name) || a.kind.localeCompare(b.kind));
|
||||
const tbody = document.querySelector("#group-credential-grants-table tbody");
|
||||
fillTable(
|
||||
tbody,
|
||||
rows.map((g) =>
|
||||
el("tr", {}, [
|
||||
el("td", { textContent: g.user_group_name }),
|
||||
el("td", { textContent: CREDENTIAL_KIND_LABELS[g.kind] || g.kind }),
|
||||
el("td", { textContent: credentialLabel(g.kind, g.credential_id) }),
|
||||
el("td", { textContent: g.expires_at || "-" }),
|
||||
el("td", {}, [
|
||||
actionButton("Entziehen", "btn-danger", async () => {
|
||||
await sendJson(`/admin/group-credentials/${g.kind}/revoke`, "POST", {
|
||||
user_group_id: g.user_group_id, credential_id: g.credential_id,
|
||||
});
|
||||
await refreshGroupCredentialGrants();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
document.getElementById("group-credential-grant-form").addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const kind = document.getElementById("gcg-kind").value;
|
||||
const credentialId = Number(document.getElementById("gcg-credential").value);
|
||||
if (!credentialId) throw new Error("Zugangsdatensatz auswaehlen.");
|
||||
await sendJson(`/admin/group-credentials/${kind}/grant`, "POST", {
|
||||
user_group_id: Number(document.getElementById("gcg-group").value),
|
||||
credential_id: credentialId,
|
||||
expires_at: document.getElementById("gcg-expires").value.trim() || null,
|
||||
});
|
||||
showBanner(`Zugangsdatensatz freigegeben: ${CREDENTIAL_KIND_LABELS[kind]}.`, "ok");
|
||||
document.getElementById("gcg-expires").value = "";
|
||||
await refreshGroupCredentialGrants();
|
||||
} catch (err) {
|
||||
showBanner(err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// API-Tokens
|
||||
// ---------------------------------------------------------------------
|
||||
@ -1135,8 +1145,6 @@
|
||||
})
|
||||
);
|
||||
}
|
||||
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();
|
||||
}
|
||||
@ -1166,7 +1174,6 @@
|
||||
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 }),
|
||||
@ -1183,13 +1190,11 @@
|
||||
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");
|
||||
@ -1204,125 +1209,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 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");
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Sessions (nur Super-Admin) -- aktive + historische Sitzungen, Beenden,
|
||||
// Link zur Aufzeichnung.
|
||||
@ -1874,12 +1760,10 @@
|
||||
const hostGroupOpts = () => optionsFromList(cachedHostGroups, "id", (g) => `${g.name} (#${g.id})`);
|
||||
const groupOpts = () => optionsFromList(cachedGroups, "id", (g) => `${g.name} (#${g.id})`);
|
||||
|
||||
const rgUser = document.getElementById("rg-user");
|
||||
if (rgUser) rgUser.replaceChildren(...userOpts());
|
||||
const rgHostgroup = document.getElementById("rg-hostgroup");
|
||||
if (rgHostgroup) rgHostgroup.replaceChildren(...hostGroupOpts());
|
||||
const grgGroup = document.getElementById("grg-group");
|
||||
if (grgGroup) grgGroup.replaceChildren(...groupOpts());
|
||||
const gcgGroup = document.getElementById("gcg-group");
|
||||
if (gcgGroup) gcgGroup.replaceChildren(...groupOpts());
|
||||
const grgHostgroup = document.getElementById("grg-hostgroup");
|
||||
if (grgHostgroup) grgHostgroup.replaceChildren(...hostGroupOpts());
|
||||
const tcUser = document.getElementById("tc-user");
|
||||
@ -1897,16 +1781,12 @@
|
||||
|
||||
async function main() {
|
||||
const me = await getJson("/auth/me");
|
||||
if (!me.is_any_admin) {
|
||||
if (!me.is_admin) {
|
||||
window.location.href = "/dashboard";
|
||||
return;
|
||||
}
|
||||
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);
|
||||
document.getElementById("whoami").textContent = `${me.username} (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");
|
||||
|
||||
Reference in New Issue
Block a user