From 77416a83827c665cd1be93a87dbdf20721346d97 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Fri, 9 Jan 2026 12:43:58 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-09 12:43:58) --- .last-branch | 2 +- .../src/backend/app/main/routes_settings.py | 68 ++++++++++++++++++- .../src/templates/main/settings.html | 25 ++++++- docs/changelog.md | 9 +++ 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/.last-branch b/.last-branch index 68001ac..393b1d3 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260109-05-fix-parsers-route-import +v20260109-06-user-management-edit-roles diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 9f45d26..a8dffd0 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -586,6 +586,15 @@ def settings(): news_admin_stats = {} + users = User.query.order_by(User.username.asc()).all() + + # Count users that have 'admin' among their assigned roles (comma-separated storage) + admin_users_count = 0 + try: + admin_users_count = sum(1 for u in users if "admin" in (getattr(u, "roles", None) or [])) + except Exception: + admin_users_count = 0 + return render_template( "main/settings.html", settings=settings, @@ -594,7 +603,8 @@ def settings(): free_disk_warning=free_disk_warning, has_client_secret=has_client_secret, tz_options=tz_options, - users=User.query.order_by(User.username.asc()).all(), + users=users, + admin_users_count=admin_users_count, section=section, news_admin_items=news_admin_items, news_admin_stats=news_admin_stats, @@ -915,6 +925,53 @@ def settings_users_reset_password(user_id: int): return redirect(url_for("main.settings", section="users")) +@main_bp.route("/settings/users//roles", methods=["POST"]) +@login_required +@roles_required("admin") +def settings_users_update_roles(user_id: int): + user = User.query.get_or_404(user_id) + + roles = [r.strip() for r in request.form.getlist("roles") if (r or "").strip()] + roles = list(dict.fromkeys(roles)) + if not roles: + roles = ["viewer"] + + # Prevent removing the last remaining admin role + removing_admin = ("admin" in user.roles) and ("admin" not in roles) + if removing_admin: + try: + all_users = User.query.all() + admin_count = sum(1 for u in all_users if "admin" in (getattr(u, "roles", None) or [])) + except Exception: + admin_count = 0 + + if admin_count <= 1: + flash("Cannot remove admin role from the last admin account.", "danger") + return redirect(url_for("main.settings", section="users")) + + old_roles = ",".join(user.roles) + new_roles = ",".join(roles) + user.role = new_roles + + try: + db.session.commit() + flash(f"Roles for '{user.username}' have been updated.", "success") + _log_admin_event("user_update_roles", f"User '{user.username}' roles changed from '{old_roles}' to '{new_roles}'.") + + # If the updated user is currently logged in, make sure the active role stays valid. + try: + if getattr(current_user, "id", None) == user.id: + current_user.set_active_role(user.roles[0]) + except Exception: + pass + except Exception as exc: + db.session.rollback() + print(f"[settings-users] Failed to update roles: {exc}") + flash("Failed to update roles.", "danger") + + return redirect(url_for("main.settings", section="users")) + + @main_bp.route("/settings/users//delete", methods=["POST"]) @login_required @roles_required("admin") @@ -922,8 +979,13 @@ def settings_users_delete(user_id: int): user = User.query.get_or_404(user_id) # Prevent deleting the last admin user - if user.role == "admin": - admin_count = User.query.filter_by(role="admin").count() + if "admin" in user.roles: + try: + all_users = User.query.all() + admin_count = sum(1 for u in all_users if "admin" in (getattr(u, "roles", None) or [])) + except Exception: + admin_count = 0 + if admin_count <= 1: flash("Cannot delete the last admin account.", "danger") return redirect(url_for("main.settings", section="general")) diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index d033ed5..bdc5fbe 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -160,8 +160,30 @@ {% if users %} {% for user in users %} + {% set is_last_admin = ('admin' in user.roles and (admin_users_count or 0) <= 1) %} {{ user.username }} - {{ (user.role or '')|replace(',', ', ') }} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
Current: {{ (user.role or '')|replace(',', ', ') }}
+
@@ -170,7 +192,6 @@
- {% set is_last_admin = (user.role == 'admin' and (users | selectattr('role', 'equalto', 'admin') | list | length) <= 1) %}
diff --git a/docs/changelog.md b/docs/changelog.md index a2e28c9..6fd414a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -37,6 +37,15 @@ - Fixed parsers page route startup crash by replacing an invalid absolute import with a package-relative import for the parser registry. - Prevented Gunicorn worker boot failure (Bad Gateway) caused by "No module named 'app'" during application initialization. +--- + +## v20260109-06-user-management-edit-roles + +- Added support to edit user roles directly in the User Management interface. +- Implemented backend logic to update existing user role assignments without requiring user deletion. +- Updated validation to ensure role changes are applied immediately and consistently. +- Ensured role updates are reflected correctly in permissions and access control. + ================================================================================================================================================ ## v0.1.19 This release delivers a broad set of improvements focused on reliability, transparency, and operational control across mail processing, administrative auditing, and Run Checks workflows. The changes aim to make message handling more robust, provide better insight for administrators, and give operators clearer and more flexible control when reviewing backup runs.