feat: accept CLIENT_API_KEY on /license page.
Users can paste the key from the seller instead of editing license.env; it persists in license_state.json. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -51,8 +51,11 @@ def install_license_middleware(app: FastAPI) -> None:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
code = (data.get("code") if isinstance(data, dict) else "") or ""
|
||||
return redeem_code(str(code))
|
||||
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():
|
||||
@@ -65,7 +68,8 @@ def install_license_middleware(app: FastAPI) -> None:
|
||||
if request.method == "POST":
|
||||
form = await request.form()
|
||||
code = str(form.get("code") or "").strip()
|
||||
result = redeem_code(code)
|
||||
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:
|
||||
|
||||
@@ -44,7 +44,8 @@ def install_license_gate(app: Flask) -> None:
|
||||
def _license_redeem_api():
|
||||
data = request.get_json(silent=True) or {}
|
||||
code = (data.get("code") or request.form.get("code") or "").strip()
|
||||
return jsonify(redeem_code(code))
|
||||
ckey = (data.get("client_api_key") or request.form.get("client_api_key") or "").strip()
|
||||
return jsonify(redeem_code(code, client_api_key=ckey or None))
|
||||
|
||||
@app.post("/api/license/validate")
|
||||
def _license_validate_api():
|
||||
@@ -56,7 +57,8 @@ def install_license_gate(app: Flask) -> None:
|
||||
err = ""
|
||||
if request.method == "POST":
|
||||
code = (request.form.get("code") or "").strip()
|
||||
result = redeem_code(code)
|
||||
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:
|
||||
|
||||
+54
-12
@@ -63,7 +63,25 @@ def api_base_url() -> str:
|
||||
|
||||
def client_key() -> str:
|
||||
_load_license_env()
|
||||
return (os.getenv("LICENSE_CLIENT_KEY") or "").strip()
|
||||
env_key = (os.getenv("LICENSE_CLIENT_KEY") or "").strip()
|
||||
if env_key:
|
||||
return env_key
|
||||
with _lock:
|
||||
st = _read_state()
|
||||
return (st.get("client_api_key") or "").strip()
|
||||
|
||||
|
||||
def set_client_api_key(key: str) -> dict[str, Any]:
|
||||
"""保存用户在授权页填写的 CLIENT_API_KEY(写入 license_state.json)。"""
|
||||
key = (key or "").strip()
|
||||
if len(key) < 8:
|
||||
return {"ok": False, "message": "CLIENT_API_KEY 太短"}
|
||||
with _lock:
|
||||
st = _read_state()
|
||||
st["client_api_key"] = key
|
||||
_write_state(st)
|
||||
os.environ["LICENSE_CLIENT_KEY"] = key
|
||||
return {"ok": True, "message": "CLIENT_API_KEY 已保存"}
|
||||
|
||||
|
||||
def offline_grace_hours() -> float:
|
||||
@@ -162,7 +180,7 @@ def _parse_expires_at(value: str | None) -> float | None:
|
||||
def _http_json(method: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
key = client_key()
|
||||
if not key:
|
||||
return {"ok": False, "message": "未配置 LICENSE_CLIENT_KEY。请在仓库根目录复制 license.env.example 为 license.env,填入与授权站相同的 CLIENT_API_KEY,然后重启服务。"}
|
||||
return {"ok": False, "message": "未配置 CLIENT_API_KEY。请在软件授权页填写卖家提供的密钥,或在 license.env 中设置 LICENSE_CLIENT_KEY。"}
|
||||
url = f"{api_base_url()}{path}"
|
||||
payload = json.dumps(body).encode("utf-8")
|
||||
req = Request(
|
||||
@@ -192,10 +210,19 @@ def _http_json(method: str, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"ok": False, "message": "授权服务返回非 JSON", "network_error": True}
|
||||
|
||||
|
||||
def redeem_code(code: str) -> dict[str, Any]:
|
||||
def redeem_code(code: str, client_api_key: str | None = None) -> dict[str, Any]:
|
||||
code = (code or "").strip().upper().replace(" ", "")
|
||||
if len(code) < 8:
|
||||
return {"ok": False, "message": "激活码无效"}
|
||||
if client_api_key is not None and str(client_api_key).strip():
|
||||
saved = set_client_api_key(str(client_api_key))
|
||||
if not saved.get("ok"):
|
||||
return saved
|
||||
if not client_key():
|
||||
return {
|
||||
"ok": False,
|
||||
"message": "请先填写 CLIENT_API_KEY(向卖家索取,与授权站后台「系统设置」中的密钥一致)",
|
||||
}
|
||||
device_id = get_device_id()
|
||||
result = _http_json("POST", "/v1/redeem", {"device_id": device_id, "code": code})
|
||||
if not result.get("ok"):
|
||||
@@ -206,15 +233,18 @@ def redeem_code(code: str) -> dict[str, Any]:
|
||||
}
|
||||
now = _now_ts()
|
||||
with _lock:
|
||||
state = {
|
||||
"device_id": device_id,
|
||||
"subscription_id": result.get("subscription_id"),
|
||||
"plan": result.get("plan"),
|
||||
"expires_at": result.get("expires_at"),
|
||||
"last_ok_at": now,
|
||||
"last_validate_at": now,
|
||||
"last_reason": "",
|
||||
}
|
||||
state = _read_state()
|
||||
state.update(
|
||||
{
|
||||
"device_id": device_id,
|
||||
"subscription_id": result.get("subscription_id"),
|
||||
"plan": result.get("plan"),
|
||||
"expires_at": result.get("expires_at"),
|
||||
"last_ok_at": now,
|
||||
"last_validate_at": now,
|
||||
"last_reason": "",
|
||||
}
|
||||
)
|
||||
_write_state(state)
|
||||
return {
|
||||
"ok": True,
|
||||
@@ -294,6 +324,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
|
||||
with _lock:
|
||||
state = _read_state()
|
||||
device_id = get_device_id()
|
||||
key = client_key()
|
||||
key_mask = (key[:4] + "…" + key[-4:]) if len(key) >= 12 else (("已配置" if key else ""))
|
||||
sub_id = (state.get("subscription_id") or "").strip()
|
||||
if not sub_id:
|
||||
return {
|
||||
@@ -303,6 +335,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
|
||||
"message": "尚未激活",
|
||||
"device_id": device_id,
|
||||
"api_url": api_base_url(),
|
||||
"has_client_key": bool(key),
|
||||
"client_key_masked": key_mask,
|
||||
}
|
||||
|
||||
exp_ts = _parse_expires_at(state.get("expires_at"))
|
||||
@@ -318,6 +352,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
|
||||
"device_id": device_id,
|
||||
"subscription_id": sub_id,
|
||||
"api_url": api_base_url(),
|
||||
"has_client_key": bool(key),
|
||||
"client_key_masked": key_mask,
|
||||
}
|
||||
|
||||
reason = (state.get("last_reason") or "").strip()
|
||||
@@ -332,6 +368,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
|
||||
"device_id": device_id,
|
||||
"subscription_id": sub_id,
|
||||
"api_url": api_base_url(),
|
||||
"has_client_key": bool(key),
|
||||
"client_key_masked": key_mask,
|
||||
}
|
||||
|
||||
last_ok = float(state.get("last_ok_at") or 0)
|
||||
@@ -352,6 +390,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
|
||||
"device_id": device_id,
|
||||
"subscription_id": sub_id,
|
||||
"api_url": api_base_url(),
|
||||
"has_client_key": bool(key),
|
||||
"client_key_masked": key_mask,
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -365,6 +405,8 @@ def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]:
|
||||
"subscription_id": sub_id,
|
||||
"last_ok_at": last_ok or None,
|
||||
"api_url": api_base_url(),
|
||||
"has_client_key": bool(key),
|
||||
"client_key_masked": key_mask,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
font-family: ui-monospace, Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
input[type=text] {
|
||||
input[type=text], input[type=password] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
@@ -67,12 +67,13 @@
|
||||
.err { color: #ff7b8a; margin: 10px 0; font-size: 0.9rem; }
|
||||
.meta { margin-top: 16px; font-size: 0.85rem; color: #9aa0b4; line-height: 1.6; }
|
||||
a { color: #7ec8ff; }
|
||||
.hint { color: #6a7088; font-size: 0.78rem; margin-top: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>软件授权</h1>
|
||||
<p class="muted">复制下方设备 ID,联系微信 <strong>{{ wechat }}</strong> 购买激活码后粘贴兑换。授权站:{{ api_url }}</p>
|
||||
<p class="muted">复制设备 ID,联系微信 <strong>{{ wechat }}</strong> 获取 <strong>CLIENT_API_KEY</strong> 与激活码。授权站:{{ api_url }}</p>
|
||||
|
||||
{% if message %}<p class="ok">{{ message }}</p>{% endif %}
|
||||
{% if error %}<p class="err">{{ error }}</p>{% endif %}
|
||||
@@ -83,6 +84,11 @@
|
||||
</div>
|
||||
|
||||
<form method="post" action="/license">
|
||||
<div class="row">
|
||||
<label>CLIENT_API_KEY</label>
|
||||
<input type="text" name="client_api_key" placeholder="{% if status.has_client_key %}已保存 {{ status.client_key_masked }},可留空或填写新密钥{% else %}粘贴卖家提供的 CLIENT_API_KEY{% endif %}" autocomplete="off" {% if not status.has_client_key %}required{% endif %}>
|
||||
<div class="hint">与授权站后台「系统设置」中的密钥一致,本地保存后下次可留空。</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>激活码</label>
|
||||
<input type="text" name="code" placeholder="粘贴激活码" autocomplete="off" required>
|
||||
|
||||
Reference in New Issue
Block a user