from __future__ import annotations from flask import Blueprint, flash, g, redirect, render_template, request, url_for from flask.typing import ResponseReturnValue from app.auth import role_required from app.extensions import db from app.models import Account from app.services import ( ACCOUNT_ROLE_OPTIONS, DEFAULT_SHARE_LINK_EXPIRE_DAYS, build_share_link_url, create_share_link, get_account_or_none, get_audit_log_or_none, get_share_link_or_none, is_username_taken, list_accounts, list_action_types, list_audit_logs, list_share_links, parse_account_form, parse_password_reset_form, parse_share_link_create_form, revoke_share_link, serialize_account_snapshot, write_audit_log, ) admin_bp = Blueprint("admin", __name__, url_prefix="/admin") def _share_link_rows() -> list[dict[str, object]]: rows: list[dict[str, object]] = [] for link in list_share_links(): rows.append( { "link": link, "public_url": build_share_link_url(link.token), "status_label": "已撤销" if link.revoked_at else ("已过期" if link.is_expired() else "有效"), } ) return rows @admin_bp.get("/accounts") @role_required("admin") def accounts() -> ResponseReturnValue: return render_template( "admin/accounts.html", accounts=list_accounts(), role_options=ACCOUNT_ROLE_OPTIONS, ) @admin_bp.route("/accounts/new", methods=["GET", "POST"]) @role_required("admin") def create_account() -> ResponseReturnValue: form_data = { "username": "", "display_name": "", "role": "entry_only", "password": "", "confirm_password": "", } if request.method == "POST": form_data = { "username": request.form.get("username", "").strip(), "display_name": request.form.get("display_name", "").strip(), "role": request.form.get("role", "").strip(), "password": request.form.get("password", ""), "confirm_password": request.form.get("confirm_password", ""), } payload, errors = parse_account_form(dict(request.form), is_create=True) if is_username_taken(form_data["username"]): errors.append("账号名已存在,请换一个新的账号名。") if errors: for error in errors: flash(error, "error") return render_template( "admin/account_form.html", account=None, form_title="新增账号", submit_label="创建账号", cancel_url=url_for("admin.accounts"), role_options=ACCOUNT_ROLE_OPTIONS, form_data=form_data, is_create=True, ), 400 account = Account( username=str(payload["username"]), display_name=payload["display_name"] if isinstance(payload["display_name"], str) else None, role=str(payload["role"]), status="active", ) account.set_password(str(payload["password"])) current_account = g.current_account if isinstance(current_account, Account): account.created_by = current_account.id account.updated_by = current_account.id db.session.add(account) db.session.flush() write_audit_log( action_type="create_account", target_type="account", actor=current_account if isinstance(current_account, Account) else None, target_id=account.id, target_display_name=account.display_name or account.username, after_data=serialize_account_snapshot(account), commit=False, ) db.session.commit() flash(f"已创建账号 {account.username}。", "success") return redirect(url_for("admin.accounts")) return render_template( "admin/account_form.html", account=None, form_title="新增账号", submit_label="创建账号", cancel_url=url_for("admin.accounts"), role_options=ACCOUNT_ROLE_OPTIONS, form_data=form_data, is_create=True, ) @admin_bp.route("/accounts//edit", methods=["GET", "POST"]) @role_required("admin") def edit_account(account_id: int) -> ResponseReturnValue: account = get_account_or_none(account_id) if account is None: flash("未找到要编辑的账号。", "error") return redirect(url_for("admin.accounts")) form_data = { "username": account.username, "display_name": account.display_name or "", "role": account.role, } if request.method == "POST": form_data = { "username": account.username, "display_name": request.form.get("display_name", "").strip(), "role": request.form.get("role", "").strip(), } payload, errors = parse_account_form(dict(request.form), is_create=False) if errors: for error in errors: flash(error, "error") return render_template( "admin/account_form.html", account=account, form_title="编辑账号", submit_label="保存账号信息", cancel_url=url_for("admin.accounts"), role_options=ACCOUNT_ROLE_OPTIONS, form_data=form_data, is_create=False, ), 400 before_snapshot = serialize_account_snapshot(account) account.display_name = payload["display_name"] if isinstance(payload["display_name"], str) else None account.role = str(payload["role"]) current_account = g.current_account if isinstance(current_account, Account): account.updated_by = current_account.id after_snapshot = serialize_account_snapshot(account) if after_snapshot == before_snapshot: flash("没有检测到需要保存的账号变更。", "warning") db.session.rollback() return redirect(url_for("admin.edit_account", account_id=account.id)) write_audit_log( action_type="update_account", target_type="account", actor=current_account if isinstance(current_account, Account) else None, target_id=account.id, target_display_name=account.display_name or account.username, before_data=before_snapshot, after_data=after_snapshot, commit=False, ) db.session.commit() flash(f"已更新账号 {account.username}。", "success") return redirect(url_for("admin.accounts")) return render_template( "admin/account_form.html", account=account, form_title="编辑账号", submit_label="保存账号信息", cancel_url=url_for("admin.accounts"), role_options=ACCOUNT_ROLE_OPTIONS, form_data=form_data, is_create=False, ) @admin_bp.route("/accounts//reset-password", methods=["GET", "POST"]) @role_required("admin") def reset_account_password(account_id: int) -> ResponseReturnValue: account = get_account_or_none(account_id) if account is None: flash("未找到要重置密码的账号。", "error") return redirect(url_for("admin.accounts")) if request.method == "POST": password, errors = parse_password_reset_form(dict(request.form)) if errors: for error in errors: flash(error, "error") return render_template( "admin/account_reset_password.html", account=account, cancel_url=url_for("admin.accounts"), ), 400 before_snapshot = serialize_account_snapshot(account) account.set_password(str(password)) current_account = g.current_account if isinstance(current_account, Account): account.updated_by = current_account.id write_audit_log( action_type="reset_password", target_type="account", actor=current_account if isinstance(current_account, Account) else None, target_id=account.id, target_display_name=account.display_name or account.username, before_data=before_snapshot, after_data=serialize_account_snapshot(account), commit=False, ) db.session.commit() flash(f"已重置账号 {account.username} 的密码。", "success") return redirect(url_for("admin.accounts")) return render_template( "admin/account_reset_password.html", account=account, cancel_url=url_for("admin.accounts"), ) @admin_bp.post("/accounts//toggle-status") @role_required("admin") def toggle_account_status(account_id: int): account = get_account_or_none(account_id) if account is None: flash("未找到要变更状态的账号。", "error") return redirect(url_for("admin.accounts")) current_account = g.current_account if isinstance(current_account, Account) and current_account.id == account.id and account.status == "active": flash("不能停用当前正在使用的管理员账号。", "error") return redirect(url_for("admin.accounts")) before_snapshot = serialize_account_snapshot(account) account.status = "disabled" if account.status == "active" else "active" if isinstance(current_account, Account): account.updated_by = current_account.id write_audit_log( action_type="toggle_account_status", target_type="account", actor=current_account if isinstance(current_account, Account) else None, target_id=account.id, target_display_name=account.display_name or account.username, before_data=before_snapshot, after_data=serialize_account_snapshot(account), commit=False, ) db.session.commit() flash(f"已将账号 {account.username} 设为{'停用' if account.status == 'disabled' else '启用'}。", "success") return redirect(url_for("admin.accounts")) @admin_bp.get("/audit-logs") @role_required("admin") def audit_logs() -> ResponseReturnValue: actor_user_id = request.args.get("actor_user_id", type=int) action_type = request.args.get("action_type", "").strip() from_date = request.args.get("from_date", "").strip() to_date = request.args.get("to_date", "").strip() return render_template( "admin/audit_logs.html", logs=list_audit_logs( actor_user_id=actor_user_id, action_type=action_type, from_date=from_date, to_date=to_date, ), accounts=list_accounts(), action_types=list_action_types(), selected_actor_user_id=actor_user_id, selected_action_type=action_type, from_date=from_date, to_date=to_date, ) @admin_bp.get("/audit-logs/") @role_required("admin") def audit_log_detail(log_id: int) -> ResponseReturnValue: log = get_audit_log_or_none(log_id) if log is None: flash("未找到要查看的审计日志。", "error") return redirect(url_for("admin.audit_logs")) return render_template("admin/audit_log_detail.html", log=log) @admin_bp.route("/share-links", methods=["GET", "POST"]) @role_required("admin") def share_links() -> ResponseReturnValue: if request.method == "POST": payload, errors = parse_share_link_create_form(dict(request.form)) if errors: for error in errors: flash(error, "error") return render_template( "admin/share_links.html", link_rows=_share_link_rows(), default_expire_days=DEFAULT_SHARE_LINK_EXPIRE_DAYS, form_data={ "label": request.form.get("label", "").strip(), "expire_days": request.form.get("expire_days", str(DEFAULT_SHARE_LINK_EXPIRE_DAYS)).strip(), }, ), 400 current_account = g.current_account actor = current_account if isinstance(current_account, Account) else None label = payload.get("label") expire_days = payload.get("expire_days") link = create_share_link( created_by=actor.id if actor is not None else None, label=label if isinstance(label, str) else None, expire_days=expire_days if isinstance(expire_days, int) else DEFAULT_SHARE_LINK_EXPIRE_DAYS, ) write_audit_log( action_type="create_share_link", target_type="share_link", actor=actor, target_id=link.id, target_display_name=build_share_link_url(link.token), after_data={ "token": link.token, "label": link.label, "expires_at": link.expires_at.isoformat(), }, commit=False, ) db.session.commit() flash("已创建分享链接。", "success") return redirect(url_for("admin.share_links")) return render_template( "admin/share_links.html", link_rows=_share_link_rows(), default_expire_days=DEFAULT_SHARE_LINK_EXPIRE_DAYS, form_data={"label": "", "expire_days": str(DEFAULT_SHARE_LINK_EXPIRE_DAYS)}, ) @admin_bp.post("/share-links//revoke") @role_required("admin") def revoke_share_link_action(link_id: int) -> ResponseReturnValue: link = get_share_link_or_none(link_id) if link is None: flash("未找到要撤销的分享链接。", "error") return redirect(url_for("admin.share_links")) updated = revoke_share_link(link) if not updated: flash("该分享链接已是撤销状态。", "warning") return redirect(url_for("admin.share_links")) current_account = g.current_account actor = current_account if isinstance(current_account, Account) else None write_audit_log( action_type="revoke_share_link", target_type="share_link", actor=actor, target_id=link.id, target_display_name=build_share_link_url(link.token), after_data={"revoked_at": link.revoked_at.isoformat() if link.revoked_at else None}, commit=False, ) db.session.commit() flash("分享链接已撤销。", "success") return redirect(url_for("admin.share_links"))