"""FastAPI 中控许可中间件与 /license 页。""" from __future__ import annotations import os from pathlib import Path from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from lib.license.license_lib import ( get_device_id, get_license_status, is_license_valid, redeem_code, validate_license, ) _TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html" def _allowed_path(path: str) -> bool: if path in ( "/license", "/api/license/status", "/api/license/redeem", "/api/license/validate", "/health", ): return True if path.startswith("/assets/") or path.startswith("/static/"): return True if path.startswith("/favicon"): return True return False def install_license_middleware(app: FastAPI) -> None: @app.get("/health") async def _license_health(): st = get_license_status(skip_remote=True) return {"ok": True, "license_valid": bool(st.get("valid")), "service": "manual_trading_hub"} @app.get("/api/license/status") async def _license_status_api(): return get_license_status() @app.post("/api/license/redeem") async def _license_redeem_api(request: Request): try: data = await request.json() except Exception: data = {} if not isinstance(data, dict): data = {} code = str(data.get("code") or "").strip() ckey = str(data.get("client_api_key") or "").strip() return redeem_code(code, client_api_key=ckey or None) @app.post("/api/license/validate") async def _license_validate_api(): return validate_license(force=True) @app.api_route("/license", methods=["GET", "POST"]) async def _license_page(request: Request): msg = "" err = "" if request.method == "POST": form = await request.form() code = str(form.get("code") or "").strip() 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 "激活失败" 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 "")) .replace("{{ wechat }}", "dekun03") .replace("{{ message }}", msg) .replace("{{ error }}", err) .replace("{{ status_message }}", str(status.get("message") or "")) .replace("{{ expires_at }}", str(status.get("expires_at") or "—")) .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 filled = Template(html).render( device_id=get_device_id(), status=status, message=msg, error=err, api_url=status.get("api_url") or "", wechat="dekun03", ) return HTMLResponse(filled) @app.middleware("http") async def _license_http_middleware(request: Request, call_next): 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): return await call_next(request) if is_license_valid(): return await call_next(request) if path.startswith("/api/"): return JSONResponse( {"ok": False, "error": "license_required", "message": "请先激活许可"}, status_code=403, ) return RedirectResponse(url="/license", status_code=302)