aa8af6c2e0
Flask + SQLite + Bootstrap5 暗色主题 - 仪表盘: 在线/离线统计 + 最近代理 + 操作日志 - 代理 CRUD: 增删改 + 启用/禁用 + 单/批量健康检测(SOCKS5) - 分组管理: 创建/删除/筛选 - 操作日志: 分页查询 - RESTful API + Web 界面 - 管理员登录认证 (SM_ADMIN_PASSWORD 环境变量)
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
|
from datetime import datetime, timezone
|
|
from auth import login_required
|
|
from models import Proxy, Group, Log, db
|
|
from health import check_proxy, run_health_checks
|
|
|
|
bp = Blueprint("main", __name__)
|
|
|
|
|
|
def _log(level, action, target=None, detail=None):
|
|
db.session.add(Log(level=level, action=action, target=target, detail=detail))
|
|
db.session.commit()
|
|
|
|
|
|
# ── Dashboard ──────────────────────────────────────────────────
|
|
@bp.route("/")
|
|
@bp.route("/dashboard")
|
|
@login_required
|
|
def dashboard():
|
|
total = Proxy.query.count()
|
|
online = Proxy.query.filter_by(status="online", enabled=True).count()
|
|
offline = Proxy.query.filter_by(status="offline").count()
|
|
unknown = Proxy.query.filter_by(status="unknown").count()
|
|
groups = Group.query.count()
|
|
recent = Log.query.order_by(Log.timestamp.desc()).limit(20).all()
|
|
proxies = Proxy.query.filter_by(enabled=True).order_by(Proxy.updated_at.desc()).limit(10).all()
|
|
return render_template("dashboard.html", total=total, online=online, offline=offline,
|
|
unknown=unknown, groups=groups, recent=recent, proxies=proxies)
|
|
|
|
|
|
# ── Proxy list ─────────────────────────────────────────────────
|
|
@bp.route("/proxies")
|
|
@login_required
|
|
def proxy_list():
|
|
g = request.args.get("group")
|
|
query = Proxy.query.order_by(Proxy.updated_at.desc())
|
|
if g:
|
|
query = query.filter_by(group_id=g)
|
|
groups = Group.query.all()
|
|
return render_template("proxies.html", proxies=query.all(), groups=groups, active_group=g)
|
|
|
|
|
|
@bp.route("/proxies/<int:pid>", methods=["DELETE"])
|
|
@login_required
|
|
def delete_proxy(pid):
|
|
p = Proxy.query.get_or_404(pid)
|
|
_log("WARN", "delete", p.name, f"删除代理 {p.host}:{p.port}")
|
|
db.session.delete(p)
|
|
db.session.commit()
|
|
return redirect(url_for("main.proxy_list"))
|
|
|
|
|
|
@bp.route("/proxies/toggle/<int:pid>")
|
|
@login_required
|
|
def toggle_proxy(pid):
|
|
p = Proxy.query.get_or_404(pid)
|
|
p.enabled = not p.enabled
|
|
db.session.commit()
|
|
_log("INFO", "toggle", p.name, f"{'启用' if p.enabled else '禁用'}")
|
|
return redirect(url_for("main.proxy_list"))
|
|
|
|
|
|
# ── Add / Edit ─────────────────────────────────────────────────
|
|
@bp.route("/proxies/add", methods=["GET", "POST"])
|
|
@login_required
|
|
def add_proxy():
|
|
if request.method == "POST":
|
|
p = Proxy(
|
|
name=request.form["name"],
|
|
host=request.form["host"],
|
|
port=int(request.form["port"]),
|
|
username=request.form.get("username"),
|
|
password=request.form.get("password"),
|
|
group_id=int(request.form["group_id"]) if request.form.get("group_id") else None,
|
|
notes=request.form.get("notes"),
|
|
)
|
|
db.session.add(p)
|
|
db.session.commit()
|
|
_log("INFO", "add", p.name, f"添加代理 {p.host}:{p.port}")
|
|
return redirect(url_for("main.proxy_list"))
|
|
groups = Group.query.all()
|
|
return render_template("proxy_form.html", p=None, groups=groups, action="add")
|
|
|
|
|
|
@bp.route("/proxies/<int:pid>/edit", methods=["GET", "POST"])
|
|
@login_required
|
|
def edit_proxy(pid):
|
|
p = Proxy.query.get_or_404(pid)
|
|
if request.method == "POST":
|
|
p.name = request.form["name"]
|
|
p.host = request.form["host"]
|
|
p.port = int(request.form["port"])
|
|
p.username = request.form.get("username")
|
|
p.password = request.form.get("password")
|
|
p.group_id = int(request.form["group_id"]) if request.form.get("group_id") else None
|
|
p.notes = request.form.get("notes")
|
|
db.session.commit()
|
|
_log("INFO", "update", p.name, f"更新代理 {p.host}:{p.port}")
|
|
return redirect(url_for("main.proxy_list"))
|
|
groups = Group.query.all()
|
|
return render_template("proxy_form.html", p=p, groups=groups, action="edit")
|
|
|
|
|
|
# ── Group ──────────────────────────────────────────────────────
|
|
@bp.route("/groups")
|
|
@login_required
|
|
def group_list():
|
|
groups = Group.query.all()
|
|
return render_template("groups.html", groups=groups)
|
|
|
|
|
|
@bp.route("/groups/add", methods=["POST"])
|
|
@login_required
|
|
def add_group():
|
|
name = request.form["name"]
|
|
if Group.query.filter_by(name=name).first():
|
|
return jsonify({"error": "分组已存在"}), 400
|
|
g = Group(name=name, color=request.form.get("color", "#6366f1"),
|
|
description=request.form.get("description"))
|
|
db.session.add(g)
|
|
db.session.commit()
|
|
_log("INFO", "add", name, "添加分组")
|
|
return jsonify({"id": g.id, "name": g.name})
|
|
|
|
|
|
@bp.route("/groups/<int:gid>", methods=["DELETE"])
|
|
@login_required
|
|
def delete_group(gid):
|
|
g = Group.query.get_or_404(gid)
|
|
if g.proxies:
|
|
return jsonify({"error": "分组下仍有代理,请先移动"}), 400
|
|
_log("WARN", "delete", g.name, "删除分组")
|
|
db.session.delete(g)
|
|
db.session.commit()
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
# ── Logs ───────────────────────────────────────────────────────
|
|
@bp.route("/logs")
|
|
@login_required
|
|
def logs():
|
|
page = int(request.args.get("page", 1))
|
|
ps = db.paginate(Log.query.order_by(Log.timestamp.desc()), page=page, per_page=50)
|
|
return render_template("logs.html", pages=ps,
|
|
page_start=max(1, ps.page - 2),
|
|
page_end=min(ps.pages, ps.page + 2))
|
|
|
|
|
|
@bp.route("/health/check-all")
|
|
@login_required
|
|
def health_check_all():
|
|
results = run_health_checks()
|
|
return jsonify(results)
|