Compare commits
2 Commits
7a65b1dcfe
...
80447813c0
| Author | SHA1 | Date | |
|---|---|---|---|
| 80447813c0 | |||
| 77416a8382 |
@ -1 +1 @@
|
|||||||
v20260109-05-fix-parsers-route-import
|
v20260109-06-user-management-edit-roles
|
||||||
|
|||||||
@ -586,6 +586,15 @@ def settings():
|
|||||||
news_admin_stats = {}
|
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(
|
return render_template(
|
||||||
"main/settings.html",
|
"main/settings.html",
|
||||||
settings=settings,
|
settings=settings,
|
||||||
@ -594,7 +603,8 @@ def settings():
|
|||||||
free_disk_warning=free_disk_warning,
|
free_disk_warning=free_disk_warning,
|
||||||
has_client_secret=has_client_secret,
|
has_client_secret=has_client_secret,
|
||||||
tz_options=tz_options,
|
tz_options=tz_options,
|
||||||
users=User.query.order_by(User.username.asc()).all(),
|
users=users,
|
||||||
|
admin_users_count=admin_users_count,
|
||||||
section=section,
|
section=section,
|
||||||
news_admin_items=news_admin_items,
|
news_admin_items=news_admin_items,
|
||||||
news_admin_stats=news_admin_stats,
|
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"))
|
return redirect(url_for("main.settings", section="users"))
|
||||||
|
|
||||||
|
|
||||||
|
@main_bp.route("/settings/users/<int:user_id>/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/<int:user_id>/delete", methods=["POST"])
|
@main_bp.route("/settings/users/<int:user_id>/delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin")
|
@roles_required("admin")
|
||||||
@ -922,8 +979,13 @@ def settings_users_delete(user_id: int):
|
|||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
|
|
||||||
# Prevent deleting the last admin user
|
# Prevent deleting the last admin user
|
||||||
if user.role == "admin":
|
if "admin" in user.roles:
|
||||||
admin_count = User.query.filter_by(role="admin").count()
|
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:
|
if admin_count <= 1:
|
||||||
flash("Cannot delete the last admin account.", "danger")
|
flash("Cannot delete the last admin account.", "danger")
|
||||||
return redirect(url_for("main.settings", section="general"))
|
return redirect(url_for("main.settings", section="general"))
|
||||||
|
|||||||
@ -160,8 +160,30 @@
|
|||||||
{% if users %}
|
{% if users %}
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
|
{% set is_last_admin = ('admin' in user.roles and (admin_users_count or 0) <= 1) %}
|
||||||
<td>{{ user.username }}</td>
|
<td>{{ user.username }}</td>
|
||||||
<td>{{ (user.role or '')|replace(',', ', ') }}</td>
|
<td>
|
||||||
|
<form method="post" action="{{ url_for('main.settings_users_update_roles', user_id=user.id) }}" class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
|
<div class="form-check form-check-inline m-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="role_admin_{{ user.id }}" name="roles" value="admin" {% if 'admin' in user.roles %}checked{% endif %} {% if is_last_admin %}disabled title="Cannot remove admin from the last admin account"{% endif %} />
|
||||||
|
<label class="form-check-label" for="role_admin_{{ user.id }}">Admin</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline m-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="role_operator_{{ user.id }}" name="roles" value="operator" {% if 'operator' in user.roles %}checked{% endif %} />
|
||||||
|
<label class="form-check-label" for="role_operator_{{ user.id }}">Operator</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline m-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="role_reporter_{{ user.id }}" name="roles" value="reporter" {% if 'reporter' in user.roles %}checked{% endif %} />
|
||||||
|
<label class="form-check-label" for="role_reporter_{{ user.id }}">Reporter</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline m-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="role_viewer_{{ user.id }}" name="roles" value="viewer" {% if 'viewer' in user.roles %}checked{% endif %} />
|
||||||
|
<label class="form-check-label" for="role_viewer_{{ user.id }}">Viewer</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
<div class="text-muted small mt-1">Current: {{ (user.role or '')|replace(',', ', ') }}</div>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<form method="post" action="{{ url_for('main.settings_users_reset_password', user_id=user.id) }}" class="d-inline">
|
<form method="post" action="{{ url_for('main.settings_users_reset_password', user_id=user.id) }}" class="d-inline">
|
||||||
@ -170,7 +192,6 @@
|
|||||||
<button type="submit" class="btn btn-outline-secondary">Reset</button>
|
<button type="submit" class="btn btn-outline-secondary">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% set is_last_admin = (user.role == 'admin' and (users | selectattr('role', 'equalto', 'admin') | list | length) <= 1) %}
|
|
||||||
<form method="post" action="{{ url_for('main.settings_users_delete', user_id=user.id) }}" class="d-inline">
|
<form method="post" action="{{ url_for('main.settings_users_delete', user_id=user.id) }}" class="d-inline">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger" {% if is_last_admin %}disabled title="Cannot delete the last admin account"{% endif %}>Delete</button>
|
<button type="submit" class="btn btn-sm btn-outline-danger" {% if is_last_admin %}disabled title="Cannot delete the last admin account"{% endif %}>Delete</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -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.
|
- 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.
|
- 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
|
## 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.
|
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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user