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:
@@ -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))
|
||||
Reference in New Issue
Block a user