Files
socks-manager/auth.py
T
Your Name 0bd9bcac12 fix: 修复暗色主题UI对比度、端口占用死循环及多项Web表单bug
后端修复:
- engine/instances.py: Socks5Server.start() 端口占用时死循环, 增加超时和异常透传
- engine/instances.py: import time 移至顶层
- engine/instances.py: sync_instances/start_instance 容忍单实例启动失败
- run.py: 启动时 sync_instances 失败不退出进程
- web/routes.py: instances start/restart/edit 路由捕获 OSError, flash 友好提示
- api/v1.py: instances start/restart/update API 返回 JSON 错误而非 500 崩溃
- web/routes.py: toggle_user/ban_user 支持 hidden 字段语义, 缺字段时切换状态
- web/routes.py: inject_nav 增加独立 nav.dashboard 和 page_name, 修复双 active bug
- auth.py: login 路由添加 logging import, 修 NameError

前端重构:
- templates/base.html: 全局对比度提升(--text #f1f5f9, --accent #a5b4fc), 标题色显式声明, alert/modal/progress/table 完整暗色样式
- templates/base.html: main-content padding-top 修复 navbar 遮挡内容
- templates/base.html: 侧边栏 active 高亮 (白字 + elevated bg + 左边框)
- templates/base.html: 顶栏动态显示当前页面名
- templates/*.html: 统一 page-header 类, 所有图标按钮添加 title tooltip
- templates/users.html: toggle/ban 表单添加 hidden enabled/ban 字段
- templates/connections.html: fmtBytes 改为 Jinja macro, 时间格式化
- templates/dashboard.html: 时间格式化为 YYYY-MM-DD HH:MM:SS, chart canvas 高度调整
- templates/logs.html: 时间格式化为可读格式
- templates/stats.html: 提升 chart 颜色对比度
- templates/system.html: 使用 time.localtime() 显示本地时间
2026-07-16 23:01:39 +08:00

135 lines
5.3 KiB
Python

"""登录认证。"""
import os
import hashlib
import logging
import secrets
from functools import wraps
from flask import Blueprint, request, session, redirect, url_for, current_app
log = logging.getLogger("socks.auth")
bp = Blueprint("auth", __name__)
def _get_admin_creds():
_ENV = os.environ.get("SM_ENV_PATH", os.path.join(os.path.dirname(__file__), ".env"))
pwd = os.environ.get("SM_ADMIN_PASSWORD")
user = os.environ.get("SM_ADMIN_USER", "admin")
if not pwd and os.path.exists(_ENV):
try:
for line in open(_ENV):
line = line.strip()
if line and not line.startswith("#"):
k, _, v = line.partition("=")
if k.strip() == "SM_ADMIN_PASSWORD":
pwd = v.strip()
elif k.strip() == "SM_ADMIN_USER":
user = v.strip()
except Exception:
pass
return (user, pwd or "")
def get_or_set_password():
"""启动时检查密码,如果为空则设置默认值并打印提示。"""
import os
u, p = _get_admin_creds()
if not p or not p.strip():
default_pwd = "admin123"
os.environ["SM_ADMIN_PASSWORD"] = default_pwd
os.environ["SM_ADMIN_USER"] = u
_ENV = os.environ.get("SM_ENV_PATH", os.path.join(os.path.dirname(__file__), ".env"))
try:
with open(_ENV, "w") as f:
f.write(f"SM_ADMIN_USER={u}\nSM_ADMIN_PASSWORD={default_pwd}\n")
print(f"⚠️ 密码未设置,已使用默认密码: {default_pwd}")
print(f" 配置文件: {_ENV}")
print(f" 请立即修改密码!")
except Exception as e:
print(f"⚠️ 密码未设置,默认: {default_pwd} (无法写入 .env: {e})")
return (u, default_pwd)
return (u, p)
def get_admin_password_hash():
"""获取管理员密码哈希(用于安全比较)。"""
_, p = _get_admin_creds()
return hashlib.sha256(p.encode("utf-8")).hexdigest()
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not session.get("logged_in"):
if request.path.startswith("/api/"):
return {"error": "unauthorized"}, 401
return redirect(url_for("auth.login", next=request.url))
return f(*args, **kwargs)
return decorated
@bp.route("/login", methods=["GET", "POST"])
def login():
if session.get("logged_in"):
return redirect(url_for("web.index"))
if request.method == "POST":
u, p = _get_admin_creds()
form_user = request.form.get("username", "").strip()
form_pwd = request.form.get("password", "")
# 防爆破检查
client_ip = request.remote_addr or "unknown"
from services.user_service import UserService
us = UserService()
blocked, remaining = us.check_fail2ban(client_ip)
if blocked:
log.warning("防爆破已阻断来自 %s 的登录尝试", client_ip)
return {"error": "登录过于频繁,请稍后再试"}, 429
# 使用常量时间比较防止时序攻击
if form_user == u and secrets.compare_digest(form_pwd, p):
us.record_auth_attempt(client_ip, success=True)
session.permanent = True
session["logged_in"] = True
session["user"] = u
return redirect(request.args.get("next") or url_for("web.index"))
us.record_auth_attempt(client_ip, success=False)
return {"error": "用户名或密码错误"}, 401
return _render_login()
@bp.route("/logout")
def logout():
session.clear()
return redirect(url_for("auth.login"))
def _render_login():
return """<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>登录 · SOCKS Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body{background:#0b0e14;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}
.card{background:#161922;border:1px solid #374151;border-radius:.75rem}
.form-control{background:#1a1d2b;border:1px solid #4b5563;color:#e2e8f0;border-radius:.5rem}
.form-control:focus{border-color:#818cf8;box-shadow:0 0 0 3px rgba(129,140,248,.15);background:#1a1d2b;color:#e2e8f0}
.btn-primary{background:#818cf8;border-color:#818cf8;border-radius:.5rem}
.btn-primary:hover{background:#6366f1;border-color:#6366f1}
h4{color:#818cf8;font-weight:700}
.form-label{color:#cbd5e0 !important;font-size:13px;font-weight:500}
.card .text-muted{color:#9ca3af !important}
</style></head>
<body class="d-flex align-items-center justify-content-center" style="min-height:100vh">
<div class="card shadow-lg p-5" style="width:380px">
<h4 class="text-center mb-2">SOCKS Manager</h4>
<p class="text-center text-muted small mb-4">SOCKS5 代理管理平台</p>
<form method="post">
<div class="mb-3"><label class="form-label small">用户名</label>
<input name="username" class="form-control" value="admin" autofocus required></div>
<div class="mb-4"><label class="form-label small">密码</label>
<input name="password" type="password" class="form-control" required></div>
<button type="submit" class="btn btn-primary w-100">登 录</button>
</form>
</div></body></html>"""