Files

144 lines
5.5 KiB
Python

"""
app/admin/system_users/routes.py
System user management: create, edit, deactivate, force password reset.
All actions logged to AuditLog. Superadmin only.
"""
import logging
import secrets
from datetime import datetime, timezone, timedelta
from flask import (
Blueprint, render_template, redirect, url_for,
flash, request,
)
from flask_login import login_required, current_user
from app.extensions import db, bcrypt, mail
from app.models.platform import SystemUser
from app.admin.utils import superadmin_required, log_admin_action, model_to_dict
logger = logging.getLogger(__name__)
system_users_bp = Blueprint("system_users", __name__, url_prefix="/system-users")
_AUDIT_FIELDS = ["email", "name", "role", "is_active"]
@system_users_bp.route("/")
@login_required
@superadmin_required
def index():
users = SystemUser.query.order_by(SystemUser.created_at.desc()).all()
return render_template("admin/system_users/index.html", users=users)
@system_users_bp.route("/new", methods=["GET", "POST"])
@login_required
@superadmin_required
def create():
if request.method == "POST":
email = request.form.get("email", "").strip().lower()
name = request.form.get("name", "").strip()
role = request.form.get("role", "superadmin").strip()
password = request.form.get("password", "")
confirm = request.form.get("confirm_password", "")
from app.forms import validate_password_strength
error = validate_password_strength(password, confirm)
if error:
return render_template("admin/system_users/form.html", error=error, mode="create")
if SystemUser.query.filter_by(email=email).first():
return render_template(
"admin/system_users/form.html",
error="A user with that email already exists.", mode="create",
)
user = SystemUser(
email=email, name=name, role=role,
password_hash=bcrypt.generate_password_hash(password).decode("utf-8"),
is_active=True,
)
db.session.add(user)
db.session.flush()
log_admin_action("system_user.create", "system_user", user.id,
after=model_to_dict(user, _AUDIT_FIELDS))
db.session.commit()
logger.info("System user created: id=%s email=%s", user.id, email)
flash(f"System user \'{email}\' created.", "success")
return redirect(url_for("system_users.index"))
return render_template("admin/system_users/form.html", mode="create")
@system_users_bp.route("/<int:user_id>/edit", methods=["GET", "POST"])
@login_required
@superadmin_required
def edit(user_id):
user = SystemUser.query.get_or_404(user_id)
before = model_to_dict(user, _AUDIT_FIELDS)
if request.method == "POST":
user.name = request.form.get("name", user.name).strip()
user.role = request.form.get("role", user.role).strip()
user.is_active = request.form.get("is_active") == "1"
log_admin_action("system_user.edit", "system_user", user.id,
before=before, after=model_to_dict(user, _AUDIT_FIELDS))
db.session.commit()
logger.info("System user edited: id=%s", user.id)
flash(f"User \'{user.email}\' updated.", "success")
return redirect(url_for("system_users.index"))
return render_template("admin/system_users/form.html", mode="edit", user=user)
@system_users_bp.route("/<int:user_id>/toggle-active", methods=["POST"])
@login_required
@superadmin_required
def toggle_active(user_id):
user = SystemUser.query.get_or_404(user_id)
if user.id == current_user.id:
flash("You cannot deactivate your own account.", "danger")
return redirect(url_for("system_users.index"))
before = model_to_dict(user, _AUDIT_FIELDS)
user.is_active = not user.is_active
action = "system_user.activate" if user.is_active else "system_user.deactivate"
log_admin_action(action, "system_user", user.id,
before=before, after=model_to_dict(user, _AUDIT_FIELDS))
db.session.commit()
status = "activated" if user.is_active else "deactivated"
logger.info("System user %s: id=%s", status, user.id)
flash(f"User \'{user.email}\' {status}.", "success")
return redirect(url_for("system_users.index"))
@system_users_bp.route("/<int:user_id>/force-password-reset", methods=["POST"])
@login_required
@superadmin_required
def force_password_reset(user_id):
user = SystemUser.query.get_or_404(user_id)
token = secrets.token_urlsafe(32)
user.password_reset_token = bcrypt.generate_password_hash(token).decode("utf-8")
user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
user.failed_login_attempts = 0
user.locked_until = None
log_admin_action("system_user.force_password_reset", "system_user", user.id)
db.session.commit()
reset_url = url_for("admin_auth.password_reset_confirm", token=token, _external=True)
try:
from flask_mail import Message
msg = Message(
subject="Admin Portal — Password Reset by Administrator",
recipients=[user.email],
body=f"A password reset has been initiated.\n\nReset link (24 hours):\n\n{reset_url}",
)
mail.send(msg)
flash(f"Password reset email sent to \'{user.email}\'.", "success")
except Exception as exc:
logger.error("Force reset email failed: %s", exc)
flash(f"Token generated but email failed. Reset URL: {reset_url}", "warning")
return redirect(url_for("system_users.index"))