Statisch sichtbare Erklaerungstexte (.hint/.field-hint) app-weit auf ein
einheitliches "i"-Icon umgestellt: Hover zeigt den Text neben dem
Mauszeiger, Klick/Tap oeffnet ein wiederverwendetes Modal mit dem vollen
Text. Zentral in app.js (initHintIcons, ensureHintTooltip/-Modal) via
Event-Delegation, kein Aufwand pro Seite. Hover ist bewusst per
matchMedia("(hover: hover) and (pointer: fine)") gated -- auf Touch-
Geraeten faellt automatisch nur Tap-zu-Modal an, da echte
mouseenter/mousemove-Events dort ohnehin nicht zuverlaessig auftreten.
Nicht jede .hint/.field-hint wurde umgestellt -- nach folgenden Kriterien
entschieden:
- Live-Status-Anzeigen (aktuelle Log-Pfade, Netzwerkstatus, Auditlog-
Schwellenwerte, aktive Reservierungs-/Lease-Zaehler, certbot-Timer-
Status usw.) bleiben sichtbar, da sie aktuellen Zustand zeigen, keine
generische Erklaerung.
- Kurze, einzeilige Abschnitts-Untertitel (z.B. "Server, Bind-Konto und
Suchparameter fuer die Anbindung an AD/LDAP.") bleiben als Orientierung
direkt unter der Ueberschrift sichtbar.
- Bereits JS-live-aktualisierte Status-Felder (id-tragend, z.B.
"Fragt live... ab." -> "N Gruppen geladen.") bleiben unangetastet.
- Laengere, mehrsaetzige Abschnitts-Erklaerungen (z.B. AD-Gruppenzu-
ordnungen, Fileshare-Gruppenzuordnung, Subnetze, DHCP-Options, Wartung)
sowie so gut wie alle Feld-Erklaerungen wandern ins Icon.
Nebenbei: Karte "Fileshare-Gruppen" in LDAP-Einstellungen zu "Fileshare-
Gruppenzuordnung" umbenannt (passt jetzt zur bereits bestehenden
log_action-Zielbezeichnung in app.py).
Neue gemeinsame Macro-Datei templates/_hint_icon.html, importiert von
15 Templates. Verifiziert: alle 18 betroffenen Seiten laden fehlerfrei
(200, keine JS-Fehler), Icon-Anzahl je Seite geprueft, Hover-Tooltip und
Klick-Modal auf Desktop, Tap-Modal auf simuliertem Mobilgeraet (390x844,
Touch), sowie stichprobenartig bestaetigt dass die bewusst belassenen
Hinweise unveraendert sichtbar bleiben.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
395 lines
18 KiB
HTML
395 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "groups" %}
|
|
{% block page_title %}Gruppen{% endblock %}
|
|
{% block page_sub %}<div class="topbar-sub">{{ groups|length + 1 }} Gruppen · Rechteverwaltung</div>{% endblock %}
|
|
|
|
{% block content %}
|
|
{% import "_hint_icon.html" as hi %}
|
|
|
|
{% macro permission_table(group, group_key, checked_keys, readonly) %}
|
|
{% set row_types = group_row_types[group_key] %}
|
|
<div class="permission-group-col">
|
|
<div style="overflow-x:auto;">
|
|
<table class="permission-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="permission-group-header-cell">
|
|
<label class="permission-group-toggle">
|
|
<input type="checkbox" name="permissions" value="{{ group['view_key'] }}"
|
|
title="{{ permission_labels.get(group['view_key'], group['label']) }}"
|
|
{% if group['view_key'] in checked_keys %}checked{% endif %}
|
|
{% if readonly %}disabled{% endif %}
|
|
class="permission-area-toggle-cb">
|
|
<span class="permission-group-name">{{ group['label'] }}</span>
|
|
</label>
|
|
</th>
|
|
{% for row_key, row_letter, row_label in row_types %}
|
|
<th title="{{ row_label }}">{{ row_letter }}</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for child_key, child in group['children'].items() %}
|
|
<tr>
|
|
<td class="permission-row-label">{{ child['label'] }}</td>
|
|
{% for row_key, row_letter, row_label in row_types %}
|
|
{% set perm_key = child['rows'].get(row_key) %}
|
|
<td>
|
|
{% if perm_key %}
|
|
<input type="checkbox" name="permissions" value="{{ perm_key }}"
|
|
title="{{ permission_labels.get(perm_key, perm_key) }}"
|
|
{% if perm_key in checked_keys %}checked{% endif %}
|
|
{% if readonly %}disabled{% endif %}
|
|
class="permission-child-cb">
|
|
{% else %}
|
|
<input type="checkbox" disabled class="permission-cb-na" tabindex="-1">
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro permission_tree(checked_keys, readonly, compact=false) %}
|
|
<div class="permission-groups-row{{ ' permission-groups-row--compact' if compact }}">
|
|
{% for group_key, group in permission_catalog.items() %}
|
|
{{ permission_table(group, group_key, checked_keys, readonly) }}
|
|
{% endfor %}
|
|
</div>
|
|
<div class="permission-legend">
|
|
<strong>R</strong> = Read (Lesen) · <strong>W</strong> = Write (Anlegen) ·
|
|
<strong>E</strong> = Edit (Ändern, inkl. Löschen — bei Im-/Export: Import ausführen) ·
|
|
<strong>D</strong> = Dashboard (PoE-Neustart über das Dashboard-Popup, nur bei Clients) ·
|
|
<strong>X</strong> = Export (nur bei Im-/Export — Export-Datei enthält Passwörter im Klartext)
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
<div class="section-head">
|
|
<div>
|
|
<h2 style="font-size:16px;">Gruppen {{ hi.hint_icon("Rechte je Gruppe granular vergeben, Mitgliedschaft in mehreren Gruppen addiert sich. „Admin“ und „Benutzer“ sind feste Systemgruppen. Legende direkt bei den Rechten.", "Gruppen") }}</h2>
|
|
</div>
|
|
{% if current_user.has_permission('groups.create') %}
|
|
<button type="button" class="btn btn-primary" data-open-modal="addGroupModal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
Neue Gruppe
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="table-wrap">
|
|
<div class="table-toolbar">
|
|
<div class="search-input">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
|
<input type="text" id="groupSearch" placeholder="Gruppen durchsuchen…" oninput="filterGroupsTable()">
|
|
</div>
|
|
</div>
|
|
<div style="overflow-x:auto;">
|
|
<table class="data-table" id="groupsTable" data-sortable>
|
|
<thead><tr>
|
|
<th data-sort-key="name">Gruppe</th>
|
|
<th data-sort-key="members">Mitglieder</th>
|
|
<th style="width:1%;">Aktionen</th>
|
|
</tr></thead>
|
|
|
|
<tbody data-sort-pinned>
|
|
<tr>
|
|
<td class="cell-name">Admin <span class="pill admin">Systemrolle</span></td>
|
|
<td class="text-dim">{{ admin_virtual_group.member_names|length }}</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button class="icon-btn" title="Rechte anzeigen" data-open-modal="adminGroupModal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
</button>
|
|
<button class="icon-btn" title="Mitglieder verwalten" data-open-modal="adminMembersModal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6"/><circle cx="17.5" cy="8.5" r="2.4"/><path d="M15.8 14.2c2.7.3 4.7 2.4 4.7 5.3"/></svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
|
|
{% for g in groups %}
|
|
{% set can_edit_this = current_user.has_permission('groups.edit') and not g.is_system %}
|
|
{% set can_unlock_system = g.is_system and current_user.is_admin %}
|
|
<tbody data-sort-name="{{ g.name|lower }}" data-sort-members="{{ g.member_names|length }}">
|
|
<tr>
|
|
<td class="cell-name">
|
|
{{ g.name }}
|
|
{% if g.is_system %}<span class="pill user" style="white-space:nowrap;">Standard</span>{% endif %}
|
|
</td>
|
|
<td class="text-dim">{{ g.member_names|length }}</td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button class="icon-btn" title="{{ 'Bearbeiten' if (can_edit_this or can_unlock_system) else 'Anzeigen' }}" data-open-modal="editGroupModal{{ loop.index }}">
|
|
{% if can_edit_this or can_unlock_system %}
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
{% else %}
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
{% endif %}
|
|
</button>
|
|
<button class="icon-btn" title="Mitglieder verwalten" data-open-modal="membersModal{{ loop.index }}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6"/><circle cx="17.5" cy="8.5" r="2.4"/><path d="M15.8 14.2c2.7.3 4.7 2.4 4.7 5.3"/></svg>
|
|
</button>
|
|
{% if current_user.has_permission('groups.edit') and not g.is_default and not g.is_system %}
|
|
<form method="post" data-confirm="Gruppe „{{ g.name }}“ wirklich löschen? Mitglieder verlieren die zugehörigen Rechte.">
|
|
<input type="hidden" name="delete_group" value="{{ g.id }}">
|
|
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
{% else %}
|
|
<tbody data-sort-pinned>
|
|
<tr class="empty-row"><td colspan="3">Noch keine weiteren Gruppen angelegt.</td></tr>
|
|
</tbody>
|
|
{% endfor %}
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="adminGroupModal">
|
|
<div class="modal" style="max-width:1000px;">
|
|
<div class="modal-header">
|
|
<h3>Admin — Rechte</h3>
|
|
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
{{ permission_tree(admin_virtual_group.permissions, true, true) }}
|
|
<p class="text-faint" style="font-size:11.5px; margin:12px 0 0;">Admins dürfen immer alles — diese Rechte sind fest und nicht änderbar.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="adminMembersModal">
|
|
<div class="modal" style="max-width:380px;">
|
|
<form method="post">
|
|
<input type="hidden" name="assign_admins" value="1">
|
|
<div class="modal-header">
|
|
<h3>Admin-Mitglieder</h3>
|
|
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-faint" style="font-size:11.5px; margin:0 0 12px;">Mindestens ein Admin muss bestehen bleiben.</p>
|
|
<div class="check-list" style="max-height:320px; overflow-y:auto; padding-right:4px;">
|
|
{% for u in all_users_all %}
|
|
<label class="check-row">
|
|
<input type="checkbox" name="members" value="{{ u['id'] }}" {% if u['is_admin'] %}checked{% endif %}>
|
|
{{ u['username'] }}
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
|
<button type="submit" class="btn btn-primary">Speichern</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{% for g in groups %}
|
|
{% set can_edit_this = current_user.has_permission('groups.edit') and not g.is_system %}
|
|
{% set can_unlock_system = g.is_system and current_user.is_admin %}
|
|
<div class="modal-overlay" id="membersModal{{ loop.index }}">
|
|
<div class="modal" style="max-width:380px;">
|
|
<form method="post">
|
|
<input type="hidden" name="save_group" value="1">
|
|
<input type="hidden" name="members_submitted" value="1">
|
|
<input type="hidden" name="group_id" value="{{ g.id }}">
|
|
<input type="hidden" name="name" value="{{ g.name }}">
|
|
<div class="modal-header">
|
|
<h3>Mitglieder — {{ g.name }}</h3>
|
|
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="check-list" style="max-height:320px; overflow-y:auto; padding-right:4px;">
|
|
{% for u in all_users %}
|
|
<label class="check-row">
|
|
<input type="checkbox" name="members" value="{{ u['id'] }}" {% if u['id'] in g.members %}checked{% endif %}>
|
|
{{ u['username'] }}
|
|
</label>
|
|
{% else %}
|
|
<p class="text-faint" style="font-size:12px;">Keine Nicht-Admin-Benutzer vorhanden.</p>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
|
<button type="submit" class="btn btn-primary">Speichern</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="editGroupModal{{ loop.index }}">
|
|
<div class="modal" style="max-width:1000px;">
|
|
{% if can_edit_this %}
|
|
<form method="post">
|
|
<input type="hidden" name="save_group" value="1">
|
|
<input type="hidden" name="permissions_submitted" value="1">
|
|
<input type="hidden" name="group_id" value="{{ g.id }}">
|
|
<div class="modal-header">
|
|
<h3>Gruppe bearbeiten</h3>
|
|
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="field">
|
|
<label>Name</label>
|
|
<input type="text" name="name" value="{{ g.name }}" required>
|
|
</div>
|
|
{{ permission_tree(g.permissions, false, true) }}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Speichern
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{% elif can_unlock_system %}
|
|
<div class="modal-header">
|
|
<h3>Gruppe „{{ g.name }}“</h3>
|
|
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="field"><label>Name</label><input type="text" value="{{ g.name }}" disabled></div>
|
|
<div id="readonly-{{ g.id }}">
|
|
{{ permission_tree(g.permissions, true, true) }}
|
|
<div class="flex" style="justify-content:space-between; align-items:center; margin-top:12px; flex-wrap:wrap;">
|
|
<p class="text-faint" style="font-size:11.5px; margin:0;">Die Standardgruppe „Benutzer“ ist eine Systemgruppe — ihre Rechte sind normalerweise fest.</p>
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="unlockSystemGroup({{ g.id }})">Freischalten</button>
|
|
</div>
|
|
</div>
|
|
<form method="post" class="hidden" id="unlock-{{ g.id }}">
|
|
<input type="hidden" name="save_group" value="1">
|
|
<input type="hidden" name="permissions_submitted" value="1">
|
|
<input type="hidden" name="unlock_system_group" value="1">
|
|
<input type="hidden" name="group_id" value="{{ g.id }}">
|
|
<input type="hidden" name="name" value="{{ g.name }}">
|
|
{{ permission_tree(g.permissions, false, true) }}
|
|
<p class="text-faint" style="font-size:11.5px; margin:12px 0;">
|
|
⚠ Diese Gruppe ist die Standardgruppe für neue Benutzer (auch neu angelegte AD/LDAP-Konten). Zu restriktive
|
|
Rechte hier können den Erst-Login neuer Konten einschränken.
|
|
</p>
|
|
<div class="flex" style="justify-content:flex-end;">
|
|
<button type="submit" class="btn btn-primary btn-sm">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
|
Rechte speichern
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div class="modal-header">
|
|
<h3>Gruppe „{{ g.name }}“</h3>
|
|
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
{{ permission_tree(g.permissions, true, true) }}
|
|
{% if g.is_system %}
|
|
<p class="text-faint" style="font-size:11.5px; margin:12px 0 0;">Die Standardgruppe „Benutzer“ ist eine Systemgruppe — ihre Rechte sind fest und nicht änderbar.</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<div class="modal-overlay" id="addGroupModal">
|
|
<div class="modal" style="max-width:1000px;">
|
|
<form method="post">
|
|
<div class="modal-header">
|
|
<h3>Neue Gruppe</h3>
|
|
<button type="button" class="modal-close" data-close-modal>×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="add_group" value="1">
|
|
<div class="field">
|
|
<label>Name {{ hi.hint_icon("Mitglieder werden danach über die Gruppentabelle zugeordnet.", "Name") }}</label>
|
|
<input type="text" name="name" required placeholder="z.B. Facility-Team">
|
|
</div>
|
|
{{ permission_tree([], false) }}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
|
|
<button type="submit" class="btn btn-primary">Anlegen</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function filterGroupsTable() {
|
|
const q = document.getElementById("groupSearch").value.trim().toLowerCase();
|
|
document.querySelectorAll("#groupsTable tbody").forEach(tbody => {
|
|
if (tbody.hasAttribute("data-sort-pinned")) return;
|
|
tbody.style.display = tbody.innerText.toLowerCase().includes(q) ? "" : "none";
|
|
});
|
|
}
|
|
|
|
function unlockSystemGroup(id) {
|
|
window.confirmAction(
|
|
"Rechte der Standardgruppe „Benutzer“ wirklich bearbeiten? Diese Gruppe ist der Login-Fallback für neue Benutzer (auch neue AD/LDAP-Konten) — zu restriktive Rechte können deren Erst-Login einschränken.",
|
|
() => {
|
|
document.getElementById("readonly-" + id).classList.add("hidden");
|
|
document.getElementById("unlock-" + id).classList.remove("hidden");
|
|
},
|
|
"Standardgruppe freischalten?"
|
|
);
|
|
}
|
|
|
|
function applyPermissionGating() {
|
|
document.querySelectorAll(".permission-group-col").forEach(function (area) {
|
|
const toggle = area.querySelector(".permission-area-toggle-cb");
|
|
if (!toggle || toggle.disabled) return;
|
|
const tbody = area.querySelector("tbody");
|
|
const children = area.querySelectorAll(".permission-child-cb");
|
|
const sync = function () {
|
|
children.forEach(function (cb) {
|
|
cb.disabled = !toggle.checked;
|
|
if (!toggle.checked) cb.checked = false;
|
|
});
|
|
if (tbody) tbody.classList.toggle("permission-locked", !toggle.checked);
|
|
};
|
|
toggle.addEventListener("change", sync);
|
|
sync();
|
|
});
|
|
}
|
|
|
|
function applyRowViewPrerequisite() {
|
|
document.querySelectorAll(".permission-table tbody tr").forEach(function (tr) {
|
|
const boxes = Array.from(tr.querySelectorAll(".permission-child-cb"));
|
|
if (boxes.length < 2) return;
|
|
const viewBox = boxes[0];
|
|
const restBoxes = boxes.slice(1);
|
|
restBoxes.forEach(function (cb) {
|
|
cb.addEventListener("change", function () {
|
|
if (cb.checked && !viewBox.checked && !viewBox.disabled) {
|
|
viewBox.checked = true;
|
|
}
|
|
});
|
|
});
|
|
viewBox.addEventListener("change", function () {
|
|
if (!viewBox.checked) {
|
|
restBoxes.forEach(function (cb) { cb.checked = false; });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
document.addEventListener("DOMContentLoaded", applyPermissionGating);
|
|
document.addEventListener("DOMContentLoaded", applyRowViewPrerequisite);
|
|
</script>
|
|
{% endblock %}
|