feat: 支持 Linux 客户端配置与发布校验
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from app.core.config import ENV_FILE_PATH
|
||||
from app.core.security import admin_auth, persist_env_value, set_admin_token
|
||||
from app.repositories import app_channel_repository
|
||||
from app.core.security import admin_auth, persist_env_value, set_admin_token, token_digest
|
||||
from app.repositories import app_channel_repository, license_repository
|
||||
from app.schemas.admin_config import ChangeAdminTokenRequest, ClientConfigGenerateRequest
|
||||
from app.services.admin_config_service import (
|
||||
PUBLISH_MAX_FIELDS,
|
||||
@@ -18,6 +19,7 @@ from app.services.admin_config_service import (
|
||||
crash_report_test_values,
|
||||
crash_report_test_text,
|
||||
normalize_executable_name,
|
||||
platform_executable_path,
|
||||
public_api_base_url,
|
||||
)
|
||||
from app.services.common_service import validate_channel_code
|
||||
@@ -26,6 +28,31 @@ from app.services.common_service import validate_channel_code
|
||||
router = APIRouter(tags=["admin-config"])
|
||||
|
||||
|
||||
def client_config_license_warning(license_key: str, app_id: str, channel: str) -> str:
|
||||
if not license_key:
|
||||
return ""
|
||||
license_row = license_repository.get_license_by_hash_with_usage(token_digest(license_key))
|
||||
if not license_row:
|
||||
return "当前 License 不存在或已删除,生成的客户端配置未写入 License,请创建或申请新的 License。"
|
||||
if license_row["app_id"] != app_id or license_row["channel_code"] != channel:
|
||||
return "当前 License 不属于所选应用或渠道,生成的客户端配置未写入 License,请选择匹配的 License。"
|
||||
if license_row["status"] != "active":
|
||||
return "当前 License 已禁用,生成的客户端配置未写入 License,请启用该 License 或创建新的 License。"
|
||||
try:
|
||||
expiry = datetime.fromisoformat(str(license_row["valid_until"]).replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
expiry = datetime.now(timezone.utc)
|
||||
if expiry.tzinfo is None:
|
||||
expiry = expiry.replace(tzinfo=timezone.utc)
|
||||
if expiry <= datetime.now(timezone.utc):
|
||||
return "当前 License 已过期,生成的客户端配置未写入 License,请创建或申请新的 License。"
|
||||
used_devices = int(license_row["used_devices"] or 0)
|
||||
max_devices = int(license_row["max_devices"] or 0)
|
||||
if used_devices >= max_devices:
|
||||
return f"当前 License 已绑定 {used_devices}/{max_devices} 台设备,已达到上限。生成的客户端配置未写入 License,请创建或申请新的 License。"
|
||||
return ""
|
||||
|
||||
|
||||
@router.get("/admin/auth/check")
|
||||
def admin_auth_check(auth=Depends(admin_auth)):
|
||||
return {"code": 0, "msg": "管理员令牌有效"}
|
||||
@@ -83,7 +110,14 @@ def admin_generate_client_config(body: ClientConfigGenerateRequest, request: Req
|
||||
raise HTTPException(status_code=400, detail={"error": "channel_not_found", "msg": f"渠道 {channel} 不存在"})
|
||||
|
||||
defaults = default_client_config_values(request)
|
||||
main_executable = normalize_executable_name(body.main_executable) or defaults["main_executable"]
|
||||
main_executable = platform_executable_path(
|
||||
normalize_executable_name(body.main_executable),
|
||||
defaults["main_executable"],
|
||||
)
|
||||
license_key = body.license_key.strip()
|
||||
license_warning = client_config_license_warning(license_key, app_id, channel)
|
||||
if license_warning:
|
||||
license_key = ""
|
||||
config = {
|
||||
"app_id": app_id,
|
||||
"app_name": app_row["app_name"],
|
||||
@@ -91,7 +125,7 @@ def admin_generate_client_config(body: ClientConfigGenerateRequest, request: Req
|
||||
"current_version": current_version,
|
||||
"client_protocol": str(client_protocol),
|
||||
"launch_token": defaults["launch_token"],
|
||||
"license_key": body.license_key.strip(),
|
||||
"license_key": license_key,
|
||||
"api_base_url": api_base_url,
|
||||
"client_token": defaults["client_token"],
|
||||
"request_timeout_ms": defaults["request_timeout_ms"],
|
||||
@@ -110,6 +144,7 @@ def admin_generate_client_config(body: ClientConfigGenerateRequest, request: Req
|
||||
return {
|
||||
"config": config,
|
||||
"json_text": json.dumps(config, ensure_ascii=False, indent=2),
|
||||
"license_warning": license_warning,
|
||||
"crash_test_config": crash_test_config,
|
||||
"crash_test_text": crash_report_test_text(api_base_url),
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ from app.services.common_service import is_executable_path
|
||||
from app.services.signing_service import canonical_manifest_bytes, sign_manifest, sign_policy
|
||||
|
||||
|
||||
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows")
|
||||
TARGET_ARCH = os.getenv("TARGET_ARCH", "x64")
|
||||
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows").strip().lower() or "windows"
|
||||
TARGET_ARCH = os.getenv("TARGET_ARCH", "x64").strip() or "x64"
|
||||
SIGNING_KEY_ID = os.getenv("SIGNING_KEY_ID", "manifest-key-v1")
|
||||
|
||||
router = APIRouter(tags=["admin-version"])
|
||||
|
||||
@@ -17,8 +17,8 @@ from app.services.common_service import is_executable_path, policy_row_to_dict,
|
||||
from app.services.signing_service import canonical_manifest_bytes, sign_device_identity, sign_manifest, sign_policy
|
||||
|
||||
|
||||
TARGET_PLATFORM = __import__("os").getenv("TARGET_PLATFORM", "windows")
|
||||
TARGET_ARCH = __import__("os").getenv("TARGET_ARCH", "x64")
|
||||
TARGET_PLATFORM = __import__("os").getenv("TARGET_PLATFORM", "windows").strip().lower() or "windows"
|
||||
TARGET_ARCH = __import__("os").getenv("TARGET_ARCH", "x64").strip() or "x64"
|
||||
SIGNING_KEY_ID = __import__("os").getenv("SIGNING_KEY_ID", "manifest-key-v1")
|
||||
|
||||
router = APIRouter(tags=["client-update"])
|
||||
|
||||
@@ -51,6 +51,17 @@ def list_licenses(app_id: str = "", include_deleted: bool = False) -> list:
|
||||
return rows
|
||||
|
||||
|
||||
def get_license_by_hash_with_usage(license_key_hash: str):
|
||||
conn = db.get_conn()
|
||||
row = conn.execute(
|
||||
"""SELECT l.*,(SELECT COUNT(*) FROM license_devices d WHERE d.license_id=l.license_id) used_devices
|
||||
FROM licenses l WHERE l.license_key_hash=? AND l.status<>'deleted'""",
|
||||
(license_key_hash,),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return row
|
||||
|
||||
|
||||
def set_license_status(license_id: str, status: str) -> int:
|
||||
conn = db.get_conn()
|
||||
cur = conn.execute(
|
||||
|
||||
@@ -7,9 +7,30 @@ from fastapi import HTTPException
|
||||
|
||||
PUBLIC_API_BASE_URL = os.getenv("PUBLIC_API_BASE_URL", "").strip().rstrip("/")
|
||||
VALID_CLIENT_TOKEN = os.getenv("CLIENT_API_TOKEN")
|
||||
RELEASE_MAIN_EXECUTABLE = os.getenv("RELEASE_MAIN_EXECUTABLE", "MainApp.exe").strip().replace(chr(92), "/").strip("/")
|
||||
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows")
|
||||
TARGET_ARCH = os.getenv("TARGET_ARCH", "x64")
|
||||
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows").strip().lower() or "windows"
|
||||
TARGET_ARCH = os.getenv("TARGET_ARCH", "x64").strip() or "x64"
|
||||
|
||||
|
||||
def platform_executable_path(value: str, fallback_base_name: str) -> str:
|
||||
path = str(value or "").strip().replace(chr(92), "/").strip("/")
|
||||
if not path:
|
||||
path = fallback_base_name
|
||||
parts = path.split("/")
|
||||
leaf = parts[-1]
|
||||
if TARGET_PLATFORM == "windows":
|
||||
if "." not in leaf:
|
||||
leaf += ".exe"
|
||||
elif TARGET_PLATFORM == "linux":
|
||||
if leaf.casefold().endswith(".exe"):
|
||||
leaf = leaf[:-4]
|
||||
parts[-1] = leaf
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
RELEASE_MAIN_EXECUTABLE = platform_executable_path(
|
||||
os.getenv("RELEASE_MAIN_EXECUTABLE", ""),
|
||||
"bin/SimCAE",
|
||||
)
|
||||
PUBLISH_MAX_REQUEST_BYTES = int(os.getenv("PUBLISH_MAX_REQUEST_MB", "4096")) * 1024 * 1024
|
||||
PUBLISH_MAX_FILES = int(os.getenv("PUBLISH_MAX_FILES", "20000"))
|
||||
PUBLISH_MAX_FIELDS = int(os.getenv("PUBLISH_MAX_FIELDS", str(PUBLISH_MAX_FILES + 100)))
|
||||
@@ -31,7 +52,7 @@ def default_client_main_executable() -> str:
|
||||
path = RELEASE_MAIN_EXECUTABLE.strip().replace(chr(92), "/").strip("/")
|
||||
if path.casefold().startswith("bin/"):
|
||||
path = path[4:]
|
||||
return path or "SimCAE.exe"
|
||||
return platform_executable_path(path, "SimCAE")
|
||||
|
||||
|
||||
def default_client_config_values(request: Request) -> dict:
|
||||
@@ -42,10 +63,13 @@ def default_client_config_values(request: Request) -> dict:
|
||||
"request_timeout_ms": os.getenv("CLIENT_REQUEST_TIMEOUT_MS", "5000"),
|
||||
"temp_folder": os.getenv("CLIENT_TEMP_FOLDER", "update_temp"),
|
||||
"install_root": os.getenv("CLIENT_INSTALL_ROOT", ".."),
|
||||
"main_executable": os.getenv("CLIENT_MAIN_EXECUTABLE", default_client_main_executable()),
|
||||
"launcher_executable": os.getenv("CLIENT_LAUNCHER_EXECUTABLE", "Launcher.exe"),
|
||||
"updater_executable": os.getenv("CLIENT_UPDATER_EXECUTABLE", "Updater.exe"),
|
||||
"bootstrap_executable": os.getenv("CLIENT_BOOTSTRAP_EXECUTABLE", "Bootstrap.exe"),
|
||||
"main_executable": platform_executable_path(
|
||||
os.getenv("CLIENT_MAIN_EXECUTABLE", ""),
|
||||
default_client_main_executable(),
|
||||
),
|
||||
"launcher_executable": platform_executable_path(os.getenv("CLIENT_LAUNCHER_EXECUTABLE", ""), "Launcher"),
|
||||
"updater_executable": platform_executable_path(os.getenv("CLIENT_UPDATER_EXECUTABLE", ""), "Updater"),
|
||||
"bootstrap_executable": platform_executable_path(os.getenv("CLIENT_BOOTSTRAP_EXECUTABLE", ""), "Bootstrap"),
|
||||
"health_check_timeout_ms": os.getenv("CLIENT_HEALTH_CHECK_TIMEOUT_MS", "15000"),
|
||||
"platform": TARGET_PLATFORM,
|
||||
"arch": TARGET_ARCH,
|
||||
|
||||
@@ -18,7 +18,15 @@ from app.repositories import publish_repository
|
||||
from app.services.common_service import require_channel
|
||||
|
||||
|
||||
RELEASE_MAIN_EXECUTABLE = os.getenv("RELEASE_MAIN_EXECUTABLE", "MainApp.exe").strip().replace(chr(92), "/").strip("/")
|
||||
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows").strip().lower() or "windows"
|
||||
DEFAULT_RELEASE_MAIN_EXECUTABLE = "bin/SimCAE" if TARGET_PLATFORM == "linux" else "bin/SimCAE.exe"
|
||||
RELEASE_MAIN_EXECUTABLE = os.getenv("RELEASE_MAIN_EXECUTABLE", DEFAULT_RELEASE_MAIN_EXECUTABLE).strip().replace(chr(92), "/").strip("/")
|
||||
if TARGET_PLATFORM == "linux" and RELEASE_MAIN_EXECUTABLE.casefold().endswith(".exe"):
|
||||
RELEASE_MAIN_EXECUTABLE = RELEASE_MAIN_EXECUTABLE[:-4]
|
||||
elif TARGET_PLATFORM == "windows":
|
||||
leaf = RELEASE_MAIN_EXECUTABLE.rsplit("/", 1)[-1]
|
||||
if "." not in leaf:
|
||||
RELEASE_MAIN_EXECUTABLE += ".exe"
|
||||
UPLOAD_SPOOL_DIR = configured_path("UPLOAD_SPOOL_DIR", "upload_spool")
|
||||
UPLOAD_SPOOL_DIR.mkdir(parents=True, exist_ok=True)
|
||||
tempfile.tempdir = str(UPLOAD_SPOOL_DIR)
|
||||
|
||||
Reference in New Issue
Block a user