modified: web/app.py

modified:   web/blueprints/auth.py
	modified:   web/blueprints/group_admin.py
	modified:   web/blueprints/site_admin.py
	new file:   web/limiter.py
	modified:   web/panel_db.py
	modified:   web/requirements.txt
	new file:   web/templates/429.html
	new file:   web/templates/admin/audit_log.html
	modified:   web/templates/admin/base.html
This commit is contained in:
simon
2026-04-14 13:02:41 +02:00
parent 452d50e5b5
commit 3b78f5dfb1
10 changed files with 564 additions and 35 deletions

View File

@@ -9,6 +9,7 @@ from config import Config
from mailer import send_mail, build_invite_email, force_https_url
import panel_db as db
from roles import GROUP_MANAGEMENT_ROLES, GROUP_ROLE_OPTIONS, GROUP_ROLE_SET, role_label
from limiter import limiter
site_admin = Blueprint("site_admin", __name__, url_prefix="/admin")
@@ -52,6 +53,7 @@ def dashboard():
@site_admin.route("/mail", methods=["GET", "POST"])
@admin_required
@limiter.limit("20 per hour", methods=["POST"])
def mail_settings():
settings = db.get_site_mail_settings()
error = None
@@ -101,6 +103,10 @@ def mail_settings():
"Your SMTP settings were verified successfully and have been saved.",
)
db.set_site_mail_settings(host, port, username, password, from_email, from_name, use_tls)
db.log_audit_event(
session["user_id"], session["username"], "mail.settings_saved",
ip_address=request.remote_addr,
)
flash("Mail settings saved and verified.", "success")
return redirect(url_for("site_admin.mail_settings"))
except Exception as exc:
@@ -123,6 +129,10 @@ def mail_settings():
@admin_required
def mail_settings_delete():
db.delete_site_mail_settings()
db.log_audit_event(
session["user_id"], session["username"], "mail.settings_deleted",
ip_address=request.remote_addr,
)
flash("Mail settings removed.", "success")
return redirect(url_for("site_admin.mail_settings"))
@@ -151,7 +161,13 @@ def group_new():
elif db.get_group_by_name(name):
flash("A group with that name already exists.", "danger")
else:
db.create_group(name, desc)
gid = db.create_group(name, desc)
db.log_audit_event(
session["user_id"], session["username"], "group.created",
entity_type="group", entity_id=gid,
details={"name": name},
ip_address=request.remote_addr,
)
flash(f"Group '{name}' created.", "success")
return redirect(url_for("site_admin.groups"))
return render_template("admin/group_edit.html", group=None)
@@ -171,6 +187,12 @@ def group_edit(group_id):
flash("Group name must not be empty.", "danger")
else:
db.update_group(group_id, name, desc)
db.log_audit_event(
session["user_id"], session["username"], "group.updated",
entity_type="group", entity_id=group_id,
details={"name": name},
ip_address=request.remote_addr,
)
flash("Group updated.", "success")
return redirect(url_for("site_admin.groups"))
return render_template("admin/group_edit.html", group=group)
@@ -179,7 +201,14 @@ def group_edit(group_id):
@site_admin.route("/groups/<int:group_id>/delete", methods=["POST"])
@admin_required
def group_delete(group_id):
group = db.get_group_by_id(group_id)
db.delete_group(group_id)
db.log_audit_event(
session["user_id"], session["username"], "group.deleted",
entity_type="group", entity_id=group_id,
details={"name": group["name"] if group else None},
ip_address=request.remote_addr,
)
flash("Group deleted.", "success")
return redirect(url_for("site_admin.groups"))
@@ -211,6 +240,13 @@ def group_member_add(group_id):
return redirect(url_for("site_admin.group_members", group_id=group_id))
if user_id:
db.add_group_member(user_id, group_id, role)
target = db.get_user_by_id(user_id)
db.log_audit_event(
session["user_id"], session["username"], "member.added",
entity_type="user", entity_id=user_id,
details={"target": target["username"] if target else str(user_id), "role": role},
group_id=group_id, ip_address=request.remote_addr,
)
flash("Member added.", "success")
return redirect(url_for("site_admin.group_members", group_id=group_id))
@@ -218,7 +254,14 @@ def group_member_add(group_id):
@site_admin.route("/groups/<int:group_id>/members/<int:user_id>/remove", methods=["POST"])
@admin_required
def group_member_remove(group_id, user_id):
target = db.get_user_by_id(user_id)
db.remove_group_member(user_id, group_id)
db.log_audit_event(
session["user_id"], session["username"], "member.removed",
entity_type="user", entity_id=user_id,
details={"target": target["username"] if target else str(user_id)},
group_id=group_id, ip_address=request.remote_addr,
)
flash("Member removed.", "success")
return redirect(url_for("site_admin.group_members", group_id=group_id))
@@ -234,7 +277,15 @@ def group_member_set_role(group_id, user_id):
flash("Invalid role selected.", "danger")
return redirect(url_for("site_admin.group_members", group_id=group_id))
perms = member["permissions"] if isinstance(member["permissions"], dict) else (_json.loads(member["permissions"]) if member["permissions"] else {})
old_role = member.get("role")
db.update_member(user_id, group_id, new_role, perms)
target = db.get_user_by_id(user_id)
db.log_audit_event(
session["user_id"], session["username"], "member.role_changed",
entity_type="user", entity_id=user_id,
details={"target": target["username"] if target else str(user_id), "old_role": old_role, "new_role": new_role},
group_id=group_id, ip_address=request.remote_addr,
)
flash(f"Role changed to '{new_role}'.", "success")
return redirect(url_for("site_admin.group_members", group_id=group_id))
@@ -507,6 +558,12 @@ def user_edit(user_id):
if new_password:
db.change_password(user_id, new_password)
flash("Password changed.", "info")
db.log_audit_event(
session["user_id"], session["username"], "user.updated",
entity_type="user", entity_id=user_id,
details={"username": username, "is_site_admin": is_site_admin},
ip_address=request.remote_addr,
)
flash("User updated.", "success")
return redirect(url_for("site_admin.users"))
return render_template("admin/user_edit.html", user=user)
@@ -518,7 +575,14 @@ def user_delete(user_id):
if user_id == session.get("user_id"):
flash("You cannot delete yourself.", "danger")
else:
target = db.get_user_by_id(user_id)
db.delete_user(user_id)
db.log_audit_event(
session["user_id"], session["username"], "user.deleted",
entity_type="user", entity_id=user_id,
details={"username": target["username"] if target else str(user_id)},
ip_address=request.remote_addr,
)
flash("User deleted.", "success")
return redirect(url_for("site_admin.users"))
@@ -568,3 +632,41 @@ def stop_view():
session.pop("permissions", None)
session.pop("admin_viewing", None)
return redirect(url_for("site_admin.dashboard"))
# ──────────────────────────────────────────────────────────────
# Audit-Log
# ──────────────────────────────────────────────────────────────
@site_admin.route("/audit")
@admin_required
def audit_log():
page = request.args.get("page", 1, type=int)
action_f = request.args.get("action", "").strip() or None
group_f = request.args.get("group_id", None, type=int)
actor_f = request.args.get("actor", "").strip() or None
per_page = 50
rows, total = db.get_audit_log(
page=page, per_page=per_page,
action_filter=action_f,
group_id_filter=group_f,
actor_filter=actor_f,
)
total_pages = max(1, (total + per_page - 1) // per_page)
all_groups = db.list_all_groups() or []
actions = db.get_audit_log_distinct_actions()
return render_template(
"admin/audit_log.html",
rows=rows,
total=total,
page=page,
total_pages=total_pages,
per_page=per_page,
action_filter=action_f or "",
group_filter=group_f,
actor_filter=actor_f or "",
all_groups=all_groups,
actions=actions,
)