Rechtesystem: Löschen überall in Ändern falten, Gruppen-Tabelle transponieren

- "Ändern" deckt jetzt ausnahmslos überall Löschen mit ab (auch bei
  Clients) — es gibt kein eigenes *.delete-Recht mehr. Betroffen:
  devices/switches/credentials/users/groups.
- "D" in der Rechtetabelle bedeutet ausschließlich Dashboard-Neustart
  (devices.restart, PoE-Neustart über das Dashboard-Popup) und existiert
  nur bei Clients. Automatische Neustarts bei Ausfall (poe.sh) bleiben
  davon komplett unabhängig.
- Gruppen-Rechtetabelle transponiert: Kategorien jetzt als Zeilen,
  Rechte R(ead)/W(rite)/E(dit)/D(ashboard-Neustart) nebeneinander als
  Spalten statt untereinander. Deckt sich besser mit dem Legenden-Format
  und braucht weniger vertikalen Platz.
- Neue, einmalig laufende Migration (_migrated_permission_delete_fold_v2)
  überführt bestehende *.delete-Grants verlustfrei in *.edit, bevor die
  veralteten Rechte-Zeilen entfernt werden.
- Routen (delete_device/delete_switch/delete_credential/delete_user/
  delete_group) und die zugehörigen can_delete-Template-Variablen prüfen
  jetzt einheitlich *.edit statt *.delete.
- README-Rechtesystem-Abschnitt und Legende an das neue Tabellen-Layout
  angepasst.
- Live getestet: Kill-Switch, alle 5 Delete-Foldings (mit/ohne Recht),
  devices.restart-Gating unabhängig von devices.edit, Eskalationsschutz
  (Admin-Löschschutz, Systemgruppen-Schutz) — alles bestätigt korrekt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 19:49:28 +02:00
co-authored by Claude Sonnet 5
parent 4b7403f476
commit 813b17d002
8 changed files with 291 additions and 136 deletions
+117 -56
View File
@@ -125,10 +125,20 @@ login_manager.init_app(app)
# ---------------------------------------------------------------------------
# Zweistufig verschachtelt, gespiegelt an der Navbar-Struktur: jeder
# Top-Level-Bereich (Geräte/Logs/Einstellungen) hat ein eigenes "Bereich
# anzeigen"-Recht (view_key) und darunter mehrere Unterpunkte mit jeweils
# eigenen Lesen/Schreiben/Ändern/Löschen-Rechten. Der Bereichs-Name wird
# dadurch nicht in jedem Unterpunkt wiederholt (siehe groups.html: eine
# Zeile pro Top-Level-Bereich, Unterpunkte darunter eingerückt).
# anzeigen"-Recht (view_key) und darunter mehrere Unterpunkte (Spalten in der
# Gruppen-Tabelle). Jeder Unterpunkt liefert "rows": eine Teilmenge der vier
# kanonischen Rechte-Zeilen (view/create/edit/delete = L/S/Ä/D, siehe
# PERMISSION_ROW_TYPES) — fehlt eine Zeile für einen Unterpunkt (z.B.
# "delete" bei Systemeinstellungen), wird die Zelle in der Tabelle grau und
# nicht setzbar dargestellt statt einfach zu fehlen, damit alle Spalten
# eines Bereichs bündig nebeneinander in einer kompakten Tabelle stehen.
PERMISSION_ROW_TYPES = [
("view", "R", "Lesen (Read)"),
("create", "W", "Schreiben/Anlegen (Write)"),
("edit", "E", "Ändern inkl. Löschen (Edit)"),
("extra", "D", "Dashboard-Neustart (nur bei Clients — kein Löschen-Recht)"),
]
PERMISSIONS = {
"devices_group": {
"label": "Geräte",
@@ -136,30 +146,33 @@ PERMISSIONS = {
"children": {
"devices": {
"label": "Clients",
"items": {
"devices.view": "Lesen",
"devices.create": "Schreiben (Anlegen)",
"devices.edit": "Ändern (inkl. Aktivieren/Deaktivieren)",
"devices.delete": "Löschen",
"devices.restart": "PoE-Neustart über das Dashboard",
# "Ändern" deckt Löschen überall mit ab, auch hier — es gibt
# nirgends ein eigenes Löschen-Recht. Die vierte Spalte (D)
# ist bei Clients KEIN Löschen, sondern das Recht, einen
# PoE-Neustart über das Dashboard-Popup auszulösen
# (automatische Neustarts bei Ausfall bleiben davon unberührt).
"rows": {
"view": "devices.view",
"create": "devices.create",
"edit": "devices.edit",
"extra": "devices.restart",
},
},
"switches": {
"label": "Switche",
"items": {
"switches.view": "Lesen",
"switches.create": "Schreiben (Anlegen)",
"switches.edit": "Ändern",
"switches.delete": "Löschen",
# Kein eigenes Löschen-Recht — "Ändern" deckt das mit ab.
"rows": {
"view": "switches.view",
"create": "switches.create",
"edit": "switches.edit",
},
},
"credentials": {
"label": "Zugangsdaten",
"items": {
"credentials.view": "Lesen",
"credentials.create": "Schreiben (Anlegen)",
"credentials.edit": "Ändern",
"credentials.delete": "Löschen",
"rows": {
"view": "credentials.view",
"create": "credentials.create",
"edit": "credentials.edit",
},
},
},
@@ -168,14 +181,8 @@ PERMISSIONS = {
"label": "Logs",
"view_key": "logs_group.view",
"children": {
"logs_live": {
"label": "Live",
"items": {"logs_live.view": "Lesen"},
},
"logs_activity": {
"label": "Änderungen",
"items": {"logs_activity.view": "Lesen"},
},
"logs_live": {"label": "Live", "rows": {"view": "logs_live.view"}},
"logs_activity": {"label": "Änderungen", "rows": {"view": "logs_activity.view"}},
},
},
"settings_group": {
@@ -184,40 +191,55 @@ PERMISSIONS = {
"children": {
"users": {
"label": "Benutzer",
"items": {
"users.view": "Lesen",
"users.create": "Schreiben (Anlegen)",
"users.edit": "Ändern",
"users.delete": "Löschen",
"rows": {
"view": "users.view",
"create": "users.create",
"edit": "users.edit",
},
},
"groups": {
"label": "Gruppen",
"items": {
"groups.view": "Lesen",
"groups.create": "Schreiben (Anlegen)",
"groups.edit": "Ändern",
"groups.delete": "Löschen",
"rows": {
"view": "groups.view",
"create": "groups.create",
"edit": "groups.edit",
},
},
"settings_system": {
"label": "Systemeinstellungen",
"items": {
"settings_system.view": "Lesen",
"settings_system.edit": "Ändern",
},
"rows": {"view": "settings_system.view", "edit": "settings_system.edit"},
},
"settings_importexport": {
"label": "Im-/Export",
"items": {
"settings_importexport.view": "Lesen (Export)",
"settings_importexport.edit": "Ändern (Import)",
},
# L = Export (nur lesend), Ä = Import (verändert Bestand).
"rows": {"view": "settings_importexport.view", "edit": "settings_importexport.edit"},
},
},
},
}
# Menschenlesbare Einzel-Labels je Recht (Tooltips auf den Checkboxen in der
# Tabelle) — aus PERMISSIONS abgeleitet, damit es nur eine Quelle gibt.
PERMISSION_LABELS = {
"devices_group.view": "Geräte-Bereich anzeigen",
"logs_group.view": "Logs-Bereich anzeigen",
"settings_group.view": "Einstellungen-Bereich anzeigen",
"devices.view": "Clients lesen", "devices.create": "Clients anlegen",
"devices.edit": "Clients ändern (inkl. Aktivieren/Deaktivieren, Löschen)",
"devices.restart": "PoE-Neustart über das Dashboard ausführen (automatische Neustarts bei Ausfall bleiben unberührt)",
"switches.view": "Switche lesen", "switches.create": "Switche anlegen",
"switches.edit": "Switche ändern (inkl. Löschen)",
"credentials.view": "Zugangsdaten lesen", "credentials.create": "Zugangsdaten anlegen",
"credentials.edit": "Zugangsdaten ändern (inkl. Löschen)",
"logs_live.view": "Live-Log lesen", "logs_activity.view": "Änderungslog lesen",
"users.view": "Benutzer lesen", "users.create": "Benutzer anlegen",
"users.edit": "Benutzer ändern (inkl. Löschen)",
"groups.view": "Gruppen lesen", "groups.create": "Gruppen anlegen",
"groups.edit": "Gruppen ändern (inkl. Löschen)",
"settings_system.view": "Systemeinstellungen lesen", "settings_system.edit": "Systemeinstellungen ändern",
"settings_importexport.view": "Daten exportieren", "settings_importexport.edit": "Daten importieren",
}
# Aus der verschachtelten Struktur abgeleitet: alle vergebbaren Keys, sowie
# eine Zuordnung jedes Kind-Rechts zum "Bereich anzeigen"-Recht seines
# Top-Level-Bereichs. Ist das übergeordnete Recht einer Gruppe nicht
@@ -230,7 +252,7 @@ PERMISSION_PARENT_GROUP = {}
for _group in PERMISSIONS.values():
ALL_PERMISSION_KEYS.append(_group["view_key"])
for _child in _group["children"].values():
for _key in _child["items"]:
for _key in _child["rows"].values():
ALL_PERMISSION_KEYS.append(_key)
PERMISSION_PARENT_GROUP[_key] = _group["view_key"]
@@ -352,13 +374,13 @@ class User(UserMixin):
@property
def can_manage_switches(self):
return self.has_any_permission(
["switches.view", "switches.create", "switches.edit", "switches.delete"]
["switches.view", "switches.create", "switches.edit"]
)
@property
def can_manage_credentials(self):
return self.has_any_permission(
["credentials.view", "credentials.create", "credentials.edit", "credentials.delete"]
["credentials.view", "credentials.create", "credentials.edit"]
)
@property
@@ -372,13 +394,13 @@ class User(UserMixin):
@property
def can_manage_users(self):
return self.has_any_permission(
["users.view", "users.create", "users.edit", "users.delete"]
["users.view", "users.create", "users.edit"]
)
@property
def can_manage_groups(self):
return self.has_any_permission(
["groups.view", "groups.create", "groups.edit", "groups.delete"]
["groups.view", "groups.create", "groups.edit"]
)
@property
@@ -595,6 +617,38 @@ def _ensure_schema():
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key,)
)
# Migration v2: "Löschen" als eigenes Recht wieder abgeschafft — "Ändern"
# deckt das Löschen jetzt überall mit ab (auch bei Clients, keine
# Ausnahme mehr). Gruppen, die bisher nur *.delete ohne *.edit hatten,
# würden sonst durch das Update ihr Löschen-Recht stillschweigend
# verlieren — deshalb erst *.edit nachtragen, dann die veralteten
# *.delete-Zeilen entfernen. Ebenfalls einmalig per Guard, aus demselben
# Grund wie bei v1 (kein Zurückschreiben bei jedem Neustart nötig, da
# *.delete als Recht komplett entfällt und nie wieder vorkommen kann).
_migration_key_v2 = "_migrated_permission_delete_fold_v2"
if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_v2,)).fetchone():
_delete_to_edit = {
"devices.delete": "devices.edit",
"switches.delete": "switches.edit",
"credentials.delete": "credentials.edit",
"users.delete": "users.edit",
"groups.delete": "groups.edit",
}
for delete_perm, edit_perm in _delete_to_edit.items():
rows = conn.execute(
"SELECT DISTINCT group_id FROM group_permissions WHERE permission=?", (delete_perm,)
).fetchall()
for row in rows:
conn.execute(
"INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?, ?)",
(row["group_id"], edit_perm),
)
conn.execute("DELETE FROM group_permissions WHERE permission=?", (delete_perm,))
conn.execute(
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_v2,)
)
# Standardgruppe 'Benutzer' — SYSTEMGRUPPE: Name und Rechte sind fix und
# werden bei jedem Start auf den vorgegebenen Satz zurückgesetzt (nicht
# nur bei Erstanlage). Das ist unbedenklich, weil is_system die Gruppe in
@@ -1366,7 +1420,8 @@ def devices():
flash("Fehler beim Aktualisieren des Geräts!", "danger")
elif "delete_device" in request.form:
if not current_user.has_permission("devices.delete"):
# "Ändern" deckt Löschen mit ab (kein eigenes devices.delete-Recht).
if not current_user.has_permission("devices.edit"):
flash("Keine Berechtigung, Geräte zu löschen.", "danger")
return redirect(url_for("devices"))
@@ -1528,7 +1583,8 @@ def switches():
@app.route("/switches/delete/<hostname>", methods=["POST"])
@login_required
def delete_switch(hostname):
if not current_user.has_permission("switches.delete"):
# "Ändern" deckt Löschen mit ab (kein eigenes switches.delete-Recht).
if not current_user.has_permission("switches.edit"):
flash("Keine Berechtigung, Switche zu löschen.", "danger")
return redirect(url_for("switches"))
@@ -1613,7 +1669,8 @@ def credentials():
flash("Name und Username müssen ausgefüllt sein!", "danger")
elif request.method == "POST" and "delete_credential" in request.form:
if not current_user.has_permission("credentials.delete"):
# "Ändern" deckt Löschen mit ab (kein eigenes credentials.delete-Recht).
if not current_user.has_permission("credentials.edit"):
flash("Keine Berechtigung, Zugangsdaten zu löschen.", "danger")
return redirect(url_for("credentials"))
cred_id = request.form.get("delete_credential")
@@ -2098,7 +2155,8 @@ def users():
flash("Gruppe zugewiesen!", "success")
elif "delete_user" in request.form:
if not current_user.has_permission("users.delete"):
# "Ändern" deckt Löschen mit ab (kein eigenes users.delete-Recht).
if not current_user.has_permission("users.edit"):
flash("Keine Berechtigung, Benutzer zu löschen.", "danger")
return redirect(url_for("users"))
@@ -2254,7 +2312,8 @@ def groups():
flash("Admin-Zuweisung aktualisiert.", "success")
elif "delete_group" in request.form:
if not current_user.has_permission("groups.delete"):
# "Ändern" deckt Löschen mit ab (kein eigenes groups.delete-Recht).
if not current_user.has_permission("groups.edit"):
flash("Keine Berechtigung, Gruppen zu löschen.", "danger")
return redirect(url_for("groups"))
group_id = request.form.get("delete_group")
@@ -2318,6 +2377,8 @@ def groups():
all_users=all_users,
all_users_all=all_users_all,
permission_catalog=PERMISSIONS,
permission_row_types=PERMISSION_ROW_TYPES,
permission_labels=PERMISSION_LABELS,
)
+32 -16
View File
@@ -1043,26 +1043,42 @@ select {
margin-bottom: 8px;
}
/* Zweistufiger Rechte-Baum (Gruppen-Detailansicht): eine Zeile pro
Top-Level-Bereich (Geräte/Logs/Einstellungen) mit eigenem "Bereich
anzeigen"-Kästchen, darunter eingerückt die Unterpunkte. */
.permission-area { margin-bottom: 22px; }
/* Rechte-Tabelle (Gruppen-Detailansicht): eine kompakte Tabelle pro
Top-Level-Bereich (Geräte/Logs/Einstellungen). Erste Zeile = der Bereich
selbst "(gesamt)" (nur die R-Spalte aktiv, das ist der Kill-Switch),
weitere Zeilen = je ein Unterpunkt (Clients, Switche, ...). Spalten =
R/W/E/D nebeneinander (siehe Legende im Seitenkopf). */
.permission-area { margin-bottom: 20px; }
.permission-area:last-child { margin-bottom: 0; }
.permission-area-toggle {
font-size: 14.5px;
padding-bottom: 8px;
margin-bottom: 14px;
.permission-table {
border-collapse: collapse;
font-size: 13px;
white-space: nowrap;
}
.permission-table th, .permission-table td {
padding: 7px 14px;
text-align: center;
border-bottom: 1px solid var(--border-soft);
}
.permission-area-toggle strong { font-weight: 700; }
.permission-area-children {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding-left: 22px;
border-left: 2px solid var(--border-soft);
margin-left: 5px;
.permission-table th {
font-size: 11.5px;
font-weight: 650;
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.03em;
font-family: var(--font-mono);
}
.permission-table th:first-child { text-transform: none; font-family: inherit; letter-spacing: normal; }
.permission-table td:first-child, .permission-table th:first-child { text-align: left; padding-right: 20px; }
.permission-table tbody tr:last-child td { border-bottom: none; }
.permission-row-label { color: var(--text-dim); }
.permission-table input[type="checkbox"] {
width: 16px; height: 16px;
accent-color: var(--accent);
cursor: pointer;
}
.permission-table input[type="checkbox"]:disabled { cursor: not-allowed; }
.permission-cb-na { opacity: 0.25; accent-color: var(--text-faint) !important; }
.check-list { display: flex; flex-direction: column; gap: 9px; }
.check-row {
+1 -1
View File
@@ -2,7 +2,7 @@
{% set active_page = "credentials" %}
{% set can_create = current_user.has_permission('credentials.create') %}
{% set can_edit = current_user.has_permission('credentials.edit') %}
{% set can_delete = current_user.has_permission('credentials.delete') %}
{% set can_delete = current_user.has_permission('credentials.edit') %}
{% block page_title %}Zugangsdaten{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ credentials|length }} Zugangsdaten</div>{% endblock %}
+1 -1
View File
@@ -3,7 +3,7 @@
{% set can_toggle = current_user.has_permission('devices.edit') %}
{% set can_create = current_user.has_permission('devices.create') %}
{% set can_edit = current_user.has_permission('devices.edit') %}
{% set can_delete = current_user.has_permission('devices.delete') %}
{% set can_delete = current_user.has_permission('devices.edit') %}
{% set show_actions_col = can_edit or can_delete %}
{% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %}
{% block page_title %}Clients{% endblock %}
+99 -32
View File
@@ -5,38 +5,77 @@
{% block content %}
{# Rendert den zweistufigen Rechte-Baum: eine Zeile pro Top-Level-Bereich
(Geräte/Logs/Einstellungen) mit eigenem "Bereich anzeigen"-Kästchen,
darunter eingerückt die Unterpunkte mit ihren jeweiligen Einzelrechten.
readonly=true zeigt nur den aktuellen Stand (Admin/Systemgruppen bzw.
fehlendes groups.edit), sonst ist der Baum Teil eines <form>. #}
{# Rendert einen Bereich (Geräte/Logs/Einstellungen) als kompakte Tabelle:
Zeilen = Bereich selbst (nur die R-Spalte aktiv, das ist der Kill-Switch)
+ je eine Zeile pro Unterpunkt (Clients, Switche, ...). Spalten = die
kanonischen Rechte R/W/E/D nebeneinander (permission_row_types).
"Ändern" (E) deckt Löschen überall mit ab — es gibt nirgends ein eigenes
Löschen-Recht. "D" existiert nur bei Clients und bedeutet dort NICHT
Löschen, sondern PoE-Neustart über das Dashboard-Popup. Zellen ohne
passendes Recht für die jeweilige Zeile (z.B. "W" bei "Geräte" selbst,
oder "D" bei Switche/Zugangsdaten) werden als ausgegraute, nicht
anklickbare Checkbox dargestellt statt zu fehlen — dadurch bleiben alle
Spalten bündig untereinander. readonly=true zeigt nur den aktuellen Stand
(Admin/Systemgruppen bzw. fehlendes groups.edit). Die JS-Funktion
applyPermissionGating() sperrt Kind-Rechte, solange das Bereich-Lesen
(Kill-Switch) nicht gesetzt ist. #}
{% macro permission_table(group, checked_keys, readonly) %}
<div class="permission-area">
<div style="overflow-x:auto;">
<table class="permission-table">
<thead>
<tr>
<th>{{ group['label'] }}</th>
{% for row_key, row_letter, row_label in permission_row_types %}
<th title="{{ row_label }}">{{ row_letter }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td class="permission-row-label">{{ group['label'] }} (gesamt)</td>
{% for row_key, row_letter, row_label in permission_row_types %}
<td>
{% if row_key == 'view' %}
<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">
{% else %}
<input type="checkbox" disabled class="permission-cb-na" tabindex="-1">
{% endif %}
</td>
{% endfor %}
</tr>
{% 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 permission_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) %}
{% for group_key, group in permission_catalog.items() %}
<div class="permission-area">
<label class="check-row permission-area-toggle" {% if readonly %}style="cursor:default; color:var(--text-faint);"{% endif %}>
<input type="checkbox" name="permissions" value="{{ group['view_key'] }}"
{% if group['view_key'] in checked_keys %}checked{% endif %}
{% if readonly %}disabled{% endif %}>
<strong>{{ group['label'] }}</strong>
</label>
<div class="permission-area-children">
{% for child_key, child in group['children'].items() %}
<div>
<div class="permission-group-title">{{ child['label'] }}</div>
<div class="check-list">
{% for key, label in child['items'].items() %}
<label class="check-row" {% if readonly %}style="cursor:default; color:var(--text-faint);"{% endif %}>
<input type="checkbox" name="permissions" value="{{ key }}"
{% if key in checked_keys %}checked{% endif %}
{% if readonly %}disabled{% endif %}>
{{ label }}
</label>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{{ permission_table(group, checked_keys, readonly) }}
{% endfor %}
{% endmacro %}
@@ -47,7 +86,12 @@
Über Gruppen lassen sich Rechte für jeden Bereich der App granular vergeben.
Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken,
um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern.
„Admin“ und „Benutzer“ sind feste Systemgruppen und nicht veränderbar.
„Admin“ und „Benutzer“ sind feste Systemgruppen und nicht veränderbar. Die Zeile „... (gesamt)“
muss gelesen werden dürfen, bevor die Zeilen darunter greifen — ausgegraute Kästchen gelten für
die jeweilige Zeile nicht.
<strong>R</strong> = Lesen (Read) · <strong>W</strong> = Schreiben/Anlegen (Write) ·
<strong>E</strong> = Ändern inkl. Löschen (Edit) ·
<strong>D</strong> = Dashboard-Neustart (nur bei Clients; automatische Neustarts bei Ausfall bleiben davon unberührt)
</div>
</div>
{% if current_user.has_permission('groups.create') %}
@@ -112,7 +156,7 @@
<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.delete') and not g.is_default and not g.is_system %}
{% 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">
@@ -254,5 +298,28 @@ function toggleDetail(id) {
const chev = document.getElementById(id.replace("detail-", "chev-"));
if (chev) chev.style.transform = row.classList.contains("hidden") ? "" : "rotate(180deg)";
}
// Kind-Rechte einer Kategorie sind erst vergebbar, wenn das übergeordnete
// "Bereich anzeigen"-Recht (Kill-Switch, erste Spalte/L-Zeile) gesetzt ist —
// spiegelt serverseitig User.has_permission() (PERMISSION_PARENT_GROUP).
// Bearbeitbare Tabellen bekommen einen Live-Listener, Nur-Lese-Tabellen
// (Admin/Systemgruppen) bleiben unangetastet, deren Checkboxen sind ohnehin
// alle disabled.
function applyPermissionGating() {
document.querySelectorAll(".permission-area").forEach(function (area) {
const toggle = area.querySelector(".permission-area-toggle-cb");
if (!toggle || toggle.disabled) return;
const children = area.querySelectorAll(".permission-child-cb");
const sync = function () {
children.forEach(function (cb) {
cb.disabled = !toggle.checked;
if (!toggle.checked) cb.checked = false;
});
};
toggle.addEventListener("change", sync);
sync();
});
}
document.addEventListener("DOMContentLoaded", applyPermissionGating);
</script>
{% endblock %}
+1 -1
View File
@@ -5,7 +5,7 @@
{% endblock %}
{% set can_create = current_user.has_permission('switches.create') %}
{% set can_edit = current_user.has_permission('switches.edit') %}
{% set can_delete = current_user.has_permission('switches.delete') %}
{% set can_delete = current_user.has_permission('switches.edit') %}
{% block page_title %}Switche{% endblock %}
{% block page_sub %}<div class="topbar-sub">{{ switches|length }} Switche</div>{% endblock %}
+1 -1
View File
@@ -54,7 +54,7 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/><path d="M8.2 10.7l7.6-4.4M8.2 13.3l7.6 4.4"/></svg>
</button>
{% endif %}
{% if current_user.has_permission('users.delete') and may_touch_target %}
{% if current_user.has_permission('users.edit') and may_touch_target %}
<form method="post" data-confirm="Willst du „{{ u['username'] }}“ wirklich löschen?">
<button type="submit" name="delete_user" value="{{ u['id'] }}" 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>