From bc00671382d45159c36973d69059ecdb73840c34 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 17 Jul 2026 18:38:05 +0800 Subject: [PATCH] fix: hide /license after activation; keep gate before activate. EOF Co-authored-by: Cursor --- docs/license.md | 5 +++-- lib/license/fastapi_gate.py | 27 ++++++++++++++++++++------- lib/license/flask_gate.py | 25 +++++++++++++++++++------ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/docs/license.md b/docs/license.md index fc77b84..9bfaca1 100644 --- a/docs/license.md +++ b/docs/license.md @@ -5,10 +5,11 @@ ## 激活流程 1. 部署并启动本系统(三所或中控任一页面)。 -2. 浏览器打开 **`/license`**(未授权时会自动跳转)。 +2. **未授权时**访问任意业务页会强制跳到 **`/license`**;已授权后默认无法再打开该页(会跳回首页)。 3. 复制页面上的 **设备 ID**。 4. 联系微信 **dekun03** 购买(备注设备 ID),按约定支付 **USDT** 后获得 **激活码 + 客户密钥**(每张码一把密钥)。 -5. 在 `/license` 填写 **客户密钥** 与激活码并兑换(密钥写入本地 `data/license_state.json`;续费一般可留空)。 +5. 在 `/license` 填写 **客户密钥** 与激活码并兑换(密钥写入本地 `data/license_state.json`;续费一般可留空)。激活成功后自动进入系统。 +6. **续费**(设备仍在授权期内):打开 **`/license?renew=1`** 兑换续费码。换机在新设备上直接进 `/license` 即可(新设备尚未授权)。 ## 定价(整机授权) diff --git a/lib/license/fastapi_gate.py b/lib/license/fastapi_gate.py index d372d35..d616597 100644 --- a/lib/license/fastapi_gate.py +++ b/lib/license/fastapi_gate.py @@ -19,7 +19,8 @@ from lib.license.license_lib import ( _TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html" -def _allowed_path(path: str) -> bool: +def _license_public_path(path: str) -> bool: + """未授权时仍可访问的路径(授权页 / 接口 / 静态资源)。""" if path in ( "/license", "/api/license/status", @@ -35,6 +36,17 @@ def _allowed_path(path: str) -> bool: return False +def _license_manage_requested(request: Request) -> bool: + """已授权时默认禁止进入 /license;续费/换机用 ?renew=1。""" + q = request.query_params + return (q.get("renew") or q.get("manage") or "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + + def install_license_middleware(app: FastAPI) -> None: @app.get("/health") async def _license_health(): @@ -63,6 +75,10 @@ def install_license_middleware(app: FastAPI) -> None: @app.api_route("/license", methods=["GET", "POST"]) async def _license_page(request: Request): + # 已授权:默认不可再进授权页(续费/换机:/license?renew=1) + if is_license_valid() and request.method == "GET" and not _license_manage_requested(request): + return RedirectResponse(url="/", status_code=302) + msg = "" err = "" if request.method == "POST": @@ -71,12 +87,10 @@ def install_license_middleware(app: FastAPI) -> None: ckey = str(form.get("client_api_key") or "").strip() result = redeem_code(code, client_api_key=ckey or None) if result.get("ok"): - msg = result.get("message") or "激活成功" - else: - err = result.get("message") or "激活失败" + return RedirectResponse(url="/", status_code=302) + err = result.get("message") or "激活失败" status = get_license_status() html = _TEMPLATE_PATH.read_text(encoding="utf-8") - # 简单替换,避免 Jinja 依赖差异 filled = ( html.replace("{{ device_id }}", get_device_id()) .replace("{{ api_url }}", str(status.get("api_url") or "")) @@ -88,7 +102,6 @@ def install_license_middleware(app: FastAPI) -> None: .replace("{{ plan }}", str(status.get("plan") or "—")) .replace("{{ valid_text }}", "已授权" if status.get("valid") else "未授权") ) - # Flask 模板用 Jinja;FastAPI 路径用占位符版本 if "{%" in filled or "{{" in filled: from jinja2 import Template @@ -107,7 +120,7 @@ def install_license_middleware(app: FastAPI) -> None: if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"): return await call_next(request) path = request.url.path or "/" - if _allowed_path(path): + if _license_public_path(path): return await call_next(request) if is_license_valid(): return await call_next(request) diff --git a/lib/license/flask_gate.py b/lib/license/flask_gate.py index b4cbbcf..fc957d5 100644 --- a/lib/license/flask_gate.py +++ b/lib/license/flask_gate.py @@ -18,7 +18,8 @@ from lib.license.license_lib import ( _TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html" -def _allowed_path(path: str) -> bool: +def _license_public_path(path: str) -> bool: + """未授权时仍可访问的路径(授权页 / 接口 / 静态资源)。""" if path in ("/license", "/api/license/status", "/api/license/redeem", "/api/license/validate", "/health"): return True if path.startswith("/static/"): @@ -28,6 +29,16 @@ def _allowed_path(path: str) -> bool: return False +def _license_manage_requested() -> bool: + """已授权时默认禁止进入 /license;续费/换机用 ?renew=1。""" + return (request.args.get("renew") or request.args.get("manage") or "").strip() in ( + "1", + "true", + "yes", + "on", + ) + + def install_license_gate(app: Flask) -> None: """注册 /license 与 before_request 门禁。三所 Flask 共用。""" @@ -53,6 +64,10 @@ def install_license_gate(app: Flask) -> None: @app.route("/license", methods=["GET", "POST"]) def _license_page(): + # 已授权:默认不可再进授权页(续费/换机:/license?renew=1) + if is_license_valid() and request.method == "GET" and not _license_manage_requested(): + return redirect("/") + msg = "" err = "" if request.method == "POST": @@ -60,9 +75,8 @@ def install_license_gate(app: Flask) -> None: ckey = (request.form.get("client_api_key") or "").strip() result = redeem_code(code, client_api_key=ckey or None) if result.get("ok"): - msg = result.get("message") or "激活成功" - else: - err = result.get("message") or "激活失败" + return redirect("/") + err = result.get("message") or "激活失败" status = get_license_status() html = _TEMPLATE_PATH.read_text(encoding="utf-8") return render_template_string( @@ -80,9 +94,8 @@ def install_license_gate(app: Flask) -> None: if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"): return None path = request.path or "/" - if _allowed_path(path): + if _license_public_path(path): return None - # hub bridge 内部调用:仍要求已授权(整机许可) if is_license_valid(): return None if path.startswith("/api/"):