Zugangsdaten-Entitaet, Gruppen-Tabelle mit Aufklappansicht, Vor-/Nachname, Dashboard-Sektionen
- Zugangsdaten (SSH-Logins) als eigenstaendige, wiederverwendbare Entitaet statt direkt am Switch; inline Neuanlage beim Switch-Erstellen moeglich; automatische Migration bestehender Switch-Logins - Gruppen-Seite als Tabelle mit Aufklapp-Zeile fuer Rechte (Akkordeon), Admin als feste Systemzeile (Mitgliederverwaltung ueber is_admin), Standardgruppe 'Benutzer' mit allen Ansichtsrechten (devices.view, switches.view), automatische Zuordnung neuer/verwaister Benutzer - Users-Seite: Icon-Buttons statt Text, Bearbeiten+Passwortaenderung in einem Modal zusammengefuehrt, Gruppe/Admin-Zuweisung ueber eigenen Zuweisen-Button (wie Switch-Zuordnung bei Devices), Vor-/Nachname - Dashboard: einheitliche Kachelansicht mit/ohne Login, drei sortierte Abschnitte (Offline/Online/Deaktiviert), Kachel-Suchfilter, Bootstrap- artiges Grid (max. 6 Spalten), Aktivieren-Option im Popup fuer deaktivierte Geraete, Countdown serverseitig korrekt geseedet - Sidebar dauerhaft einklappbar (Desktop, persistent via localStorage) - Devices-Tabelle: Aktions-Buttons nebeneinander statt untereinander Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+330
-96
@@ -203,6 +203,48 @@ def _ensure_schema():
|
||||
if "is_default" not in existing_cols:
|
||||
conn.execute("ALTER TABLE groups ADD COLUMN is_default INTEGER DEFAULT 0")
|
||||
|
||||
# Migration: Vor-/Nachname für Benutzer nachrüsten.
|
||||
user_cols = {row["name"] for row in conn.execute("PRAGMA table_info(users)").fetchall()}
|
||||
if "first_name" not in user_cols:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN first_name TEXT")
|
||||
if "last_name" not in user_cols:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN last_name TEXT")
|
||||
|
||||
# Zugangsdaten (Credentials): eigenständige, wiederverwendbare
|
||||
# SSH-Logins für Switche, statt Username/Passwort direkt am Switch.
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
switch_cols = {row["name"] for row in conn.execute("PRAGMA table_info(switches)").fetchall()}
|
||||
if "credential_id" not in switch_cols:
|
||||
conn.execute("ALTER TABLE switches ADD COLUMN credential_id INTEGER")
|
||||
|
||||
# Migration: bestehende, direkt am Switch hinterlegte Zugangsdaten
|
||||
# (ältere DB-Version) in eigene Credentials-Datensätze überführen.
|
||||
if "username" in switch_cols and "password" in switch_cols:
|
||||
legacy_switches = conn.execute(
|
||||
"SELECT hostname, username, password FROM switches WHERE credential_id IS NULL"
|
||||
).fetchall()
|
||||
for sw in legacy_switches:
|
||||
base_name = f"{sw['hostname']} (migriert)"
|
||||
final_name, suffix = base_name, 1
|
||||
while conn.execute("SELECT 1 FROM credentials WHERE name=?", (final_name,)).fetchone():
|
||||
suffix += 1
|
||||
final_name = f"{base_name} {suffix}"
|
||||
cur = conn.execute(
|
||||
"INSERT INTO credentials (name, username, password) VALUES (?, ?, ?)",
|
||||
(final_name, sw["username"], sw["password"]),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE switches SET credential_id=? WHERE hostname=?",
|
||||
(cur.lastrowid, sw["hostname"]),
|
||||
)
|
||||
|
||||
# Standardgruppe 'Benutzer' sicherstellen. Die View-Rechte werden nur bei
|
||||
# der *erstmaligen* Erzeugung gesetzt — spätere Anpassungen durch einen
|
||||
# Admin (z.B. ein Recht wieder entziehen) bleiben so über Neustarts hinweg
|
||||
@@ -437,12 +479,12 @@ def get_device_status(devices):
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""
|
||||
Dashboard als Kachel-Ansicht. Ohne Login: nur aktive (nicht deaktivierte)
|
||||
Geräte, nur Online/Offline/Gesamt-Statistik, keine Interaktion.
|
||||
Eingeloggt: alle Geräte (inkl. deaktivierte), volle Statistik-Kacheln,
|
||||
Klick-Details und manueller PoE-Neustart. In beiden Fällen: erst alle
|
||||
Nicht-Online-Geräte (alphabetisch), danach alle Online-Geräte
|
||||
(alphabetisch).
|
||||
Dashboard als Kachel-Ansicht, in drei Abschnitten: Offline, Online,
|
||||
Deaktiviert (jeweils alphabetisch) — jeweils nur gerendert, wenn nicht
|
||||
leer. Ohne Login: keine deaktivierten Geräte, nur Online/Offline/Gesamt-
|
||||
Statistik, keine Interaktion. Eingeloggt: alle Geräte, volle
|
||||
Statistik-Kacheln, Klick-Details, manueller PoE-Neustart bzw. bei
|
||||
deaktivierten Geräten eine Aktivieren-Option.
|
||||
"""
|
||||
conn = get_db_connection()
|
||||
all_devices = conn.execute(
|
||||
@@ -452,26 +494,34 @@ def index():
|
||||
|
||||
interval = int(get_setting("interval", 5))
|
||||
status, last_seen, last_checked, last_run_at = get_device_status(all_devices)
|
||||
|
||||
is_authenticated = current_user.is_authenticated
|
||||
visible_devices = all_devices if is_authenticated else [d for d in all_devices if d["is_active"]]
|
||||
|
||||
def sort_key(d):
|
||||
is_online = bool(d["is_active"]) and status.get(d["mac"]) == "online"
|
||||
return (1 if is_online else 0, d["name"].lower())
|
||||
def by_name(devs):
|
||||
return sorted(devs, key=lambda d: d["name"].lower())
|
||||
|
||||
devices = sorted(visible_devices, key=sort_key)
|
||||
offline_devices = by_name(
|
||||
d for d in all_devices if d["is_active"] and status.get(d["mac"]) != "online"
|
||||
)
|
||||
online_devices = by_name(
|
||||
d for d in all_devices if d["is_active"] and status.get(d["mac"]) == "online"
|
||||
)
|
||||
disabled_devices = by_name(d for d in all_devices if not d["is_active"]) if is_authenticated else []
|
||||
|
||||
online = sum(1 for d in visible_devices if d["is_active"] and status.get(d["mac"]) == "online")
|
||||
offline = sum(1 for d in visible_devices if d["is_active"] and status.get(d["mac"]) != "online")
|
||||
stats = {"online": online, "offline": offline, "total": len(visible_devices)}
|
||||
visible_devices = offline_devices + online_devices + disabled_devices
|
||||
devices = offline_devices + online_devices # für Kompatibilität/Zähler
|
||||
|
||||
stats = {"online": len(online_devices), "offline": len(offline_devices), "total": len(devices)}
|
||||
if is_authenticated:
|
||||
stats["disabled"] = sum(1 for d in all_devices if not d["is_active"])
|
||||
stats["total"] = len(all_devices)
|
||||
stats["disabled"] = len(disabled_devices)
|
||||
|
||||
last_run_epoch_ms = int(last_run_at.timestamp() * 1000) if last_run_at else None
|
||||
|
||||
return render_template(
|
||||
"index.html", devices=devices, status=status, last_seen=last_seen,
|
||||
"index.html",
|
||||
offline_devices=offline_devices, online_devices=online_devices, disabled_devices=disabled_devices,
|
||||
device_count=len(visible_devices),
|
||||
status=status, last_seen=last_seen,
|
||||
last_checked=last_checked, interval=interval, stats=stats,
|
||||
last_run_epoch_ms=last_run_epoch_ms,
|
||||
)
|
||||
@@ -670,6 +720,34 @@ def toggle_device(mac):
|
||||
# Switches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _resolve_credential_choice(conn):
|
||||
"""
|
||||
Liest die Zugangsdaten-Auswahl aus einem Switch-Formular: entweder eine
|
||||
bestehende credential_id, oder (choice == "new") legt direkt aus dem
|
||||
Switch-Formular heraus neue Zugangsdaten an — damit man beim Anlegen
|
||||
eines Switches nicht zuerst zu "Zugangsdaten" wechseln muss.
|
||||
Gibt (credential_id, error_message) zurück; error_message ist None bei Erfolg.
|
||||
"""
|
||||
choice = request.form.get("credential_choice", "")
|
||||
if choice == "new":
|
||||
name = request.form.get("new_credential_name", "").strip()
|
||||
username = request.form.get("new_credential_username", "").strip()
|
||||
password = request.form.get("new_credential_password", "")
|
||||
if not (name and username and password):
|
||||
return None, "Für neue Zugangsdaten müssen Name, Username und Passwort ausgefüllt sein!"
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO credentials (name, username, password) VALUES (?, ?, ?)",
|
||||
(name, username, encrypt_password(password)),
|
||||
)
|
||||
return cur.lastrowid, None
|
||||
except sqlite3.IntegrityError:
|
||||
return None, "Es existieren bereits Zugangsdaten mit diesem Namen!"
|
||||
elif choice.isdigit():
|
||||
return int(choice), None
|
||||
return None, "Bitte Zugangsdaten auswählen oder neue anlegen."
|
||||
|
||||
|
||||
@app.route("/switches", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def switches():
|
||||
@@ -685,17 +763,19 @@ def switches():
|
||||
return redirect(url_for("switches"))
|
||||
hostname = request.form["hostname"]
|
||||
ip = request.form["ip"]
|
||||
username = request.form["username"]
|
||||
password = encrypt_password(request.form["password"])
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO switches (hostname, ip, username, password) VALUES (?, ?, ?, ?)",
|
||||
(hostname, ip, username, password),
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Switch {hostname} hinzugefügt.", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Hostname existiert bereits oder Eingabefehler!", "danger")
|
||||
credential_id, cred_error = _resolve_credential_choice(conn)
|
||||
if cred_error:
|
||||
flash(cred_error, "danger")
|
||||
else:
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO switches (hostname, ip, credential_id) VALUES (?, ?, ?)",
|
||||
(hostname, ip, credential_id),
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Switch {hostname} hinzugefügt.", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Hostname existiert bereits oder Eingabefehler!", "danger")
|
||||
|
||||
if request.method == "POST" and "edit_switch" in request.form:
|
||||
if not current_user.has_permission("switches.edit"):
|
||||
@@ -704,33 +784,35 @@ def switches():
|
||||
old_hostname = request.form["old_hostname"]
|
||||
hostname = request.form["hostname"]
|
||||
ip = request.form["ip"]
|
||||
username = request.form["username"]
|
||||
new_password = request.form.get("password")
|
||||
credential_id, cred_error = _resolve_credential_choice(conn)
|
||||
if cred_error:
|
||||
flash(cred_error, "danger")
|
||||
else:
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE switches SET hostname=?, ip=?, credential_id=? WHERE hostname=?",
|
||||
(hostname, ip, credential_id, old_hostname),
|
||||
)
|
||||
if hostname != old_hostname:
|
||||
conn.execute(
|
||||
"UPDATE devices SET switch_hostname=? WHERE switch_hostname=?",
|
||||
(hostname, old_hostname),
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Switch {hostname} aktualisiert.", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Hostname existiert bereits oder Eingabefehler!", "danger")
|
||||
|
||||
try:
|
||||
if new_password:
|
||||
conn.execute(
|
||||
"UPDATE switches SET hostname=?, ip=?, username=?, password=? WHERE hostname=?",
|
||||
(hostname, ip, username, encrypt_password(new_password), old_hostname),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE switches SET hostname=?, ip=?, username=? WHERE hostname=?",
|
||||
(hostname, ip, username, old_hostname),
|
||||
)
|
||||
if hostname != old_hostname:
|
||||
conn.execute(
|
||||
"UPDATE devices SET switch_hostname=? WHERE switch_hostname=?",
|
||||
(hostname, old_hostname),
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Switch {hostname} aktualisiert.", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Hostname existiert bereits oder Eingabefehler!", "danger")
|
||||
|
||||
switch_rows = conn.execute("SELECT hostname, ip, username FROM switches ORDER BY hostname ASC").fetchall()
|
||||
switch_rows = conn.execute("""
|
||||
SELECT switches.hostname, switches.ip, switches.credential_id,
|
||||
credentials.name AS credential_name, credentials.username AS credential_username
|
||||
FROM switches
|
||||
LEFT JOIN credentials ON credentials.id = switches.credential_id
|
||||
ORDER BY switches.hostname ASC
|
||||
""").fetchall()
|
||||
all_credentials = conn.execute("SELECT id, name, username FROM credentials ORDER BY name ASC").fetchall()
|
||||
conn.close()
|
||||
return render_template("switches.html", switches=switch_rows)
|
||||
return render_template("switches.html", switches=switch_rows, all_credentials=all_credentials)
|
||||
|
||||
|
||||
@app.route("/switches/delete/<hostname>", methods=["POST"])
|
||||
@@ -755,6 +837,94 @@ def delete_switch(hostname):
|
||||
return redirect(url_for("switches"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zugangsdaten (Credentials) — wiederverwendbare SSH-Logins für Switche,
|
||||
# damit nicht jeder Switch sein eigenes Login braucht. Gleiche Berechtigungen
|
||||
# wie Switch-Verwaltung (switches.*), da inhaltlich untrennbar davon.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/credentials", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def credentials():
|
||||
if not current_user.can_manage_switches:
|
||||
flash("Keine Berechtigung für die Zugangsdatenverwaltung.", "danger")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
if request.method == "POST" and "add_credential" in request.form:
|
||||
if not current_user.has_permission("switches.create"):
|
||||
flash("Keine Berechtigung, Zugangsdaten anzulegen.", "danger")
|
||||
return redirect(url_for("credentials"))
|
||||
name = request.form.get("name", "").strip()
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
if name and username and password:
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO credentials (name, username, password) VALUES (?, ?, ?)",
|
||||
(name, username, encrypt_password(password)),
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Zugangsdaten '{name}' angelegt.", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Es existieren bereits Zugangsdaten mit diesem Namen!", "danger")
|
||||
else:
|
||||
flash("Name, Username und Passwort müssen ausgefüllt sein!", "danger")
|
||||
|
||||
elif request.method == "POST" and "edit_credential" in request.form:
|
||||
if not current_user.has_permission("switches.edit"):
|
||||
flash("Keine Berechtigung, Zugangsdaten zu bearbeiten.", "danger")
|
||||
return redirect(url_for("credentials"))
|
||||
cred_id = request.form.get("credential_id")
|
||||
name = request.form.get("name", "").strip()
|
||||
username = request.form.get("username", "").strip()
|
||||
new_password = request.form.get("password", "")
|
||||
if name and username:
|
||||
try:
|
||||
if new_password:
|
||||
conn.execute(
|
||||
"UPDATE credentials SET name=?, username=?, password=? WHERE id=?",
|
||||
(name, username, encrypt_password(new_password), cred_id),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE credentials SET name=?, username=? WHERE id=?",
|
||||
(name, username, cred_id),
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Zugangsdaten '{name}' aktualisiert.", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Es existieren bereits Zugangsdaten mit diesem Namen!", "danger")
|
||||
else:
|
||||
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("switches.delete"):
|
||||
flash("Keine Berechtigung, Zugangsdaten zu löschen.", "danger")
|
||||
return redirect(url_for("credentials"))
|
||||
cred_id = request.form.get("delete_credential")
|
||||
used_by = conn.execute("SELECT hostname FROM switches WHERE credential_id=?", (cred_id,)).fetchall()
|
||||
if used_by:
|
||||
names = ", ".join(s["hostname"] for s in used_by)
|
||||
flash(f"Diese Zugangsdaten werden noch von folgenden Switchen verwendet: {names}", "danger")
|
||||
else:
|
||||
conn.execute("DELETE FROM credentials WHERE id=?", (cred_id,))
|
||||
conn.commit()
|
||||
flash("Zugangsdaten gelöscht.", "success")
|
||||
|
||||
credential_rows = conn.execute("""
|
||||
SELECT credentials.id, credentials.name, credentials.username,
|
||||
COUNT(switches.hostname) AS switch_count
|
||||
FROM credentials
|
||||
LEFT JOIN switches ON switches.credential_id = credentials.id
|
||||
GROUP BY credentials.id
|
||||
ORDER BY credentials.name ASC
|
||||
""").fetchall()
|
||||
conn.close()
|
||||
return render_template("credentials.html", credentials=credential_rows)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Browser-SSH-Terminal — Verbindungstest beim Anlegen/Bearbeiten von Switchen.
|
||||
#
|
||||
@@ -1063,25 +1233,27 @@ def users():
|
||||
if "add_user" in request.form:
|
||||
username = request.form["username"].strip()
|
||||
password = request.form["password"].strip()
|
||||
is_admin = int(request.form.get("is_admin", 0))
|
||||
first_name = request.form.get("first_name", "").strip() or None
|
||||
last_name = request.form.get("last_name", "").strip() or None
|
||||
# Die Gruppen-Auswahl entscheidet auch über die Rolle: Auswahl
|
||||
# "admin" macht den Benutzer zum Admin, jede andere Auswahl ist
|
||||
# eine normale Gruppe (oder keine).
|
||||
group_choice = request.form.get("group_id") or ""
|
||||
is_admin = 1 if group_choice == "admin" else 0
|
||||
|
||||
if username and password:
|
||||
pw_hash = bcrypt.generate_password_hash(password).decode("utf-8")
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||
(username, pw_hash, is_admin),
|
||||
"INSERT INTO users (username, password, is_admin, first_name, last_name) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(username, pw_hash, is_admin, first_name, last_name),
|
||||
)
|
||||
# Neue, nicht-admin Benutzer landen automatisch in der
|
||||
# Standardgruppe 'Benutzer' (alle Ansichtsrechte).
|
||||
if not is_admin:
|
||||
default_group = conn.execute(
|
||||
"SELECT id FROM groups WHERE is_default=1 LIMIT 1"
|
||||
).fetchone()
|
||||
if default_group:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)",
|
||||
(cur.lastrowid, default_group["id"]),
|
||||
)
|
||||
if not is_admin and group_choice:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)",
|
||||
(cur.lastrowid, group_choice),
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Benutzer '{username}' erfolgreich angelegt!", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
@@ -1089,29 +1261,56 @@ def users():
|
||||
else:
|
||||
flash("Username und Passwort dürfen nicht leer sein!", "danger")
|
||||
|
||||
elif "change_role" in request.form:
|
||||
elif "edit_user" in request.form:
|
||||
# Nur Stammdaten + optional Passwort — Gruppe/Rolle wird
|
||||
# ausschließlich über "Gruppe zuweisen" geändert (s.u.).
|
||||
user_id = request.form["user_id"]
|
||||
username = request.form.get("username", "").strip()
|
||||
is_admin = int(request.form.get("is_admin", 0))
|
||||
first_name = request.form.get("first_name", "").strip() or None
|
||||
last_name = request.form.get("last_name", "").strip() or None
|
||||
new_password = request.form.get("new_password", "").strip()
|
||||
if username:
|
||||
conn.execute(
|
||||
"UPDATE users SET username=?, is_admin=? WHERE id=?", (username, is_admin, user_id)
|
||||
)
|
||||
if new_password:
|
||||
pw_hash = bcrypt.generate_password_hash(new_password).decode("utf-8")
|
||||
conn.execute(
|
||||
"UPDATE users SET username=?, first_name=?, last_name=?, password=? WHERE id=?",
|
||||
(username, first_name, last_name, pw_hash, user_id),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE users SET username=?, first_name=?, last_name=? WHERE id=?",
|
||||
(username, first_name, last_name, user_id),
|
||||
)
|
||||
conn.commit()
|
||||
flash("Rolle und Username geändert!", "success")
|
||||
flash("Benutzer aktualisiert!", "success")
|
||||
else:
|
||||
flash("Username darf nicht leer sein!", "danger")
|
||||
|
||||
elif "change_password" in request.form:
|
||||
elif "assign_group" in request.form:
|
||||
user_id = request.form["user_id"]
|
||||
new_password = request.form.get("new_password", "").strip()
|
||||
if new_password:
|
||||
pw_hash = bcrypt.generate_password_hash(new_password).decode("utf-8")
|
||||
conn.execute("UPDATE users SET password=? WHERE id=?", (pw_hash, user_id))
|
||||
conn.commit()
|
||||
flash("Passwort erfolgreich geändert!", "success")
|
||||
choice = request.form.get("group_id") or ""
|
||||
|
||||
# Mindestens ein Admin muss bestehen bleiben.
|
||||
target = conn.execute("SELECT is_admin FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
if target and target["is_admin"] and choice != "admin":
|
||||
admin_count = conn.execute("SELECT COUNT(*) AS n FROM users WHERE is_admin=1").fetchone()["n"]
|
||||
if admin_count <= 1:
|
||||
flash("Es muss mindestens ein Admin bestehen bleiben.", "danger")
|
||||
conn.close()
|
||||
return redirect(url_for("users"))
|
||||
|
||||
conn.execute("DELETE FROM user_groups WHERE user_id=?", (user_id,))
|
||||
if choice == "admin":
|
||||
conn.execute("UPDATE users SET is_admin=1 WHERE id=?", (user_id,))
|
||||
else:
|
||||
flash("Passwort darf nicht leer sein!", "danger")
|
||||
conn.execute("UPDATE users SET is_admin=0 WHERE id=?", (user_id,))
|
||||
if choice:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)",
|
||||
(user_id, choice),
|
||||
)
|
||||
conn.commit()
|
||||
flash("Gruppe zugewiesen!", "success")
|
||||
|
||||
elif "delete_user" in request.form:
|
||||
user_id = request.form["delete_user"]
|
||||
@@ -1121,16 +1320,18 @@ def users():
|
||||
flash("Benutzer gelöscht!", "success")
|
||||
|
||||
users_list = conn.execute("""
|
||||
SELECT users.id, users.username, users.is_admin,
|
||||
GROUP_CONCAT(groups.name, ', ') AS group_names
|
||||
SELECT users.id, users.username, users.is_admin, users.first_name, users.last_name,
|
||||
GROUP_CONCAT(groups.name, ', ') AS group_names,
|
||||
(SELECT group_id FROM user_groups WHERE user_groups.user_id = users.id LIMIT 1) AS group_id
|
||||
FROM users
|
||||
LEFT JOIN user_groups ON user_groups.user_id = users.id
|
||||
LEFT JOIN groups ON groups.id = user_groups.group_id
|
||||
GROUP BY users.id
|
||||
ORDER BY users.username ASC
|
||||
""").fetchall()
|
||||
all_groups = conn.execute("SELECT id, name, is_default FROM groups ORDER BY is_default DESC, name ASC").fetchall()
|
||||
conn.close()
|
||||
return render_template("users.html", users=users_list)
|
||||
return render_template("users.html", users=users_list, all_groups=all_groups)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1163,31 +1364,62 @@ def groups():
|
||||
elif "save_group" in request.form:
|
||||
group_id = request.form.get("group_id")
|
||||
name = request.form.get("name", "").strip()
|
||||
selected_permissions = set(request.form.getlist("permissions")) & set(ALL_PERMISSION_KEYS)
|
||||
selected_members = {int(x) for x in request.form.getlist("members") if x.isdigit()}
|
||||
|
||||
if not name:
|
||||
flash("Gruppenname darf nicht leer sein!", "danger")
|
||||
conn.close()
|
||||
return redirect(url_for("groups"))
|
||||
|
||||
# Die Gruppenkarte (Name + Rechte) und das Mitglieder-Modal sind
|
||||
# zwei getrennte Formulare, damit das Speichern des einen nicht
|
||||
# versehentlich den Stand des anderen zurücksetzt. Die verstecken
|
||||
# *_submitted-Felder markieren, welcher Teil tatsächlich abgeschickt
|
||||
# wurde (leere Checkbox-Listen wären sonst nicht von "nichts
|
||||
# ausgewählt" zu unterscheiden).
|
||||
try:
|
||||
conn.execute("UPDATE groups SET name=? WHERE id=?", (name, group_id))
|
||||
conn.execute("DELETE FROM group_permissions WHERE group_id=?", (group_id,))
|
||||
conn.executemany(
|
||||
"INSERT INTO group_permissions (group_id, permission) VALUES (?, ?)",
|
||||
[(group_id, p) for p in selected_permissions],
|
||||
)
|
||||
conn.execute("DELETE FROM user_groups WHERE group_id=?", (group_id,))
|
||||
conn.executemany(
|
||||
"INSERT INTO user_groups (user_id, group_id) VALUES (?, ?)",
|
||||
[(uid, group_id) for uid in selected_members],
|
||||
)
|
||||
|
||||
if "permissions_submitted" in request.form:
|
||||
selected_permissions = set(request.form.getlist("permissions")) & set(ALL_PERMISSION_KEYS)
|
||||
conn.execute("DELETE FROM group_permissions WHERE group_id=?", (group_id,))
|
||||
conn.executemany(
|
||||
"INSERT INTO group_permissions (group_id, permission) VALUES (?, ?)",
|
||||
[(group_id, p) for p in selected_permissions],
|
||||
)
|
||||
|
||||
if "members_submitted" in request.form:
|
||||
selected_members = {int(x) for x in request.form.getlist("members") if x.isdigit()}
|
||||
conn.execute("DELETE FROM user_groups WHERE group_id=?", (group_id,))
|
||||
conn.executemany(
|
||||
"INSERT INTO user_groups (user_id, group_id) VALUES (?, ?)",
|
||||
[(uid, group_id) for uid in selected_members],
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
flash(f"Gruppe '{name}' aktualisiert.", "success")
|
||||
except sqlite3.IntegrityError:
|
||||
flash("Eine Gruppe mit diesem Namen existiert bereits!", "danger")
|
||||
|
||||
elif "assign_admins" in request.form:
|
||||
selected_admins = {int(x) for x in request.form.getlist("members") if x.isdigit()}
|
||||
if not selected_admins:
|
||||
flash("Es muss mindestens ein Admin bestehen bleiben.", "danger")
|
||||
else:
|
||||
conn.execute("UPDATE users SET is_admin=0")
|
||||
conn.executemany(
|
||||
"UPDATE users SET is_admin=1 WHERE id=?",
|
||||
[(uid,) for uid in selected_admins],
|
||||
)
|
||||
# Admins brauchen keine Gruppenrechte mehr (sie dürfen ohnehin
|
||||
# alles) — Mitgliedschaften aufräumen, damit sie nicht doppelt
|
||||
# in Admin- und z.B. Standardgruppe auftauchen.
|
||||
conn.executemany(
|
||||
"DELETE FROM user_groups WHERE user_id=?",
|
||||
[(uid,) for uid in selected_admins],
|
||||
)
|
||||
conn.commit()
|
||||
flash("Admin-Zuweisung aktualisiert.", "success")
|
||||
|
||||
elif "delete_group" in request.form:
|
||||
group_id = request.form.get("delete_group")
|
||||
target = conn.execute("SELECT name, is_default FROM groups WHERE id=?", (group_id,)).fetchone()
|
||||
@@ -1236,6 +1468,7 @@ def groups():
|
||||
"permissions": set(ALL_PERMISSION_KEYS),
|
||||
"member_names": [u["username"] for u in admin_rows],
|
||||
}
|
||||
all_users_all = conn.execute("SELECT id, username, is_admin FROM users ORDER BY username ASC").fetchall()
|
||||
|
||||
conn.close()
|
||||
return render_template(
|
||||
@@ -1243,6 +1476,7 @@ def groups():
|
||||
groups=groups_data,
|
||||
admin_virtual_group=admin_virtual_group,
|
||||
all_users=all_users,
|
||||
all_users_all=all_users_all,
|
||||
permission_catalog=PERMISSIONS,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user