06/16 Phase 6 foundation: admin blueprint, audit log, user management

Adds the admin backend foundation: AuditLog + Setting models/migration,
an admin-only dashboard with KPI cards, and user management (search,
ban/suspend/activate, tier override, trust adjust) with audit logging
on every write action. Smoke suite extended to 64 checks.
This commit is contained in:
2026-06-16 11:45:00 -04:00
parent 62b04803ad
commit 2e9025fe55
18 changed files with 680 additions and 1 deletions
+127
View File
@@ -0,0 +1,127 @@
"""Admin backend: dashboard KPIs + user management. Admin-only."""
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import current_user
from flask_babel import gettext as _
from app.extensions import db
from app.models.user import User
from app.models.listing import Listing
from app.models.plan import Plan
from app.models.trust import TrustEvent
from app.models.audit import AuditLog
from app.models.enums import UserStatus, TrustEventType
from app.utils import admin_required
from app.services import admin_users as usvc
from app.services import admin_dashboard as dash
admin_bp = Blueprint("admin", __name__)
PER_PAGE = 25
@admin_bp.route("/admin")
@admin_required
def dashboard():
kpis = {
"active_listings": dash.active_listings_count(),
"new_users_7d": dash.new_users_count(7),
"new_users_30d": dash.new_users_count(30),
"mrr_cents": dash.mrr_cents(),
"revenue_30d_cents": dash.revenue_30d_cents(),
"flag_queue_depth": dash.flag_queue_depth(),
}
return render_template("admin/dashboard.html", kpis=kpis)
@admin_bp.route("/admin/users")
@admin_required
def users():
page = request.args.get("page", 1, type=int)
role = request.args.get("role", type=str) or None
status = request.args.get("status", type=str) or None
q = request.args.get("q", type=str) or None
pagination = usvc.search_query(role=role, status=status, q=q).paginate(
page=page, per_page=PER_PAGE, error_out=False)
return render_template("admin/users.html", pagination=pagination,
filters=request.args)
@admin_bp.route("/admin/users/<int:user_id>")
@admin_required
def user_detail(user_id):
user = User.query.get_or_404(user_id)
listings = (Listing.query.filter_by(user_id=user.id)
.order_by(Listing.created_at.desc()).limit(50).all())
trust_events = (user.trust_events.order_by(TrustEvent.created_at.desc())
.limit(50).all())
audit_history = (AuditLog.query.filter_by(target_type="user", target_id=user.id)
.order_by(AuditLog.created_at.desc()).limit(50).all())
plans = Plan.query.order_by(Plan.sort_order).all()
return render_template("admin/user_detail.html", user=user, listings=listings,
trust_events=trust_events, audit_history=audit_history,
plans=plans, TrustEventType=TrustEventType)
def _guard_self_action(user):
if user.id == current_user.id:
flash(_("You cannot perform this action on your own account."), "danger")
return True
return False
@admin_bp.route("/admin/users/<int:user_id>/ban", methods=["POST"])
@admin_required
def ban_user(user_id):
user = User.query.get_or_404(user_id)
if _guard_self_action(user):
return redirect(url_for("admin.user_detail", user_id=user.id))
usvc.set_status(user, UserStatus.banned, actor=current_user)
db.session.commit()
flash(_("User banned."), "warning")
return redirect(url_for("admin.user_detail", user_id=user.id))
@admin_bp.route("/admin/users/<int:user_id>/suspend", methods=["POST"])
@admin_required
def suspend_user(user_id):
user = User.query.get_or_404(user_id)
if _guard_self_action(user):
return redirect(url_for("admin.user_detail", user_id=user.id))
usvc.set_status(user, UserStatus.suspended, actor=current_user)
db.session.commit()
flash(_("User suspended."), "warning")
return redirect(url_for("admin.user_detail", user_id=user.id))
@admin_bp.route("/admin/users/<int:user_id>/activate", methods=["POST"])
@admin_required
def activate_user(user_id):
user = User.query.get_or_404(user_id)
usvc.set_status(user, UserStatus.active, actor=current_user)
db.session.commit()
flash(_("User activated."), "success")
return redirect(url_for("admin.user_detail", user_id=user.id))
@admin_bp.route("/admin/users/<int:user_id>/tier", methods=["POST"])
@admin_required
def set_tier(user_id):
user = User.query.get_or_404(user_id)
plan_id = request.form.get("plan_id", type=int)
plan = db.session.get(Plan, plan_id) if plan_id else None
usvc.set_tier(user, plan, actor=current_user)
db.session.commit()
flash(_("Tier updated."), "success")
return redirect(url_for("admin.user_detail", user_id=user.id))
@admin_bp.route("/admin/users/<int:user_id>/trust", methods=["POST"])
@admin_required
def trust_adjust(user_id):
user = User.query.get_or_404(user_id)
delta = request.form.get("delta", type=int) or 0
event_type = request.form.get("event_type", type=str)
usvc.adjust_trust(user, TrustEventType(event_type), delta, actor=current_user)
db.session.commit()
flash(_("Trust adjusted."), "success")
return redirect(url_for("admin.user_detail", user_id=user.id))