Files
crypto_monitor_user/lib/license/fastapi_gate.py
T
2026-07-17 18:38:05 +08:00

133 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 _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("/assets/") or path.startswith("/static/"):
return True
if path.startswith("/favicon"):
return True
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():
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):
# 已授权:默认不可再进授权页(续费/换机:/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":
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"):
return RedirectResponse(url="/", status_code=302)
err = result.get("message") or "激活失败"
status = get_license_status()
html = _TEMPLATE_PATH.read_text(encoding="utf-8")
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 "未授权")
)
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 _license_public_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)