160 lines
7.2 KiB
Python
160 lines
7.2 KiB
Python
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 persist_env_value, require_permission, 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,
|
|
PUBLISH_MAX_FILES,
|
|
PUBLISH_MAX_REQUEST_BYTES,
|
|
RELEASE_MAIN_EXECUTABLE,
|
|
TARGET_ARCH,
|
|
TARGET_PLATFORM,
|
|
default_client_config_values,
|
|
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
|
|
|
|
|
|
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 ""
|
|
|
|
|
|
def normalize_install_root(value: str, fallback: str = "..") -> str:
|
|
normalized = str(value or fallback).strip().replace("\\", "/")
|
|
if normalized in ("", "."):
|
|
return "."
|
|
if normalized == "..":
|
|
return ".."
|
|
raise HTTPException(status_code=400, detail="install_root 目前仅支持 . 或 ..")
|
|
|
|
|
|
@router.post("/admin/token/change")
|
|
def admin_change_token(body: ChangeAdminTokenRequest, auth=Depends(require_permission("admin:manage"))):
|
|
new_token = body.new_token.strip()
|
|
if len(new_token) < 8:
|
|
raise HTTPException(status_code=400, detail="新兼容令牌至少需要 8 个字符")
|
|
if len(new_token) > 128:
|
|
raise HTTPException(status_code=400, detail="新兼容令牌不能超过 128 个字符")
|
|
try:
|
|
persist_env_value(ENV_FILE_PATH, "ADMIN_TOKEN", new_token)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"令牌已通过校验,但写入 .env 失败: {exc}")
|
|
set_admin_token(new_token)
|
|
os.environ["ADMIN_TOKEN"] = new_token
|
|
return {"code": 0, "msg": f"兼容管理员令牌已更新,并已写入 {ENV_FILE_PATH}"}
|
|
|
|
|
|
@router.get("/admin/runtime-config")
|
|
def admin_runtime_config(request: Request, auth=Depends(require_permission("config:view"))):
|
|
return {
|
|
"release_main_executable": RELEASE_MAIN_EXECUTABLE,
|
|
"target_platform": TARGET_PLATFORM,
|
|
"target_arch": TARGET_ARCH,
|
|
"publish_max_request_bytes": PUBLISH_MAX_REQUEST_BYTES,
|
|
"publish_max_files": PUBLISH_MAX_FILES,
|
|
"publish_max_fields": PUBLISH_MAX_FIELDS,
|
|
"api_base_url": public_api_base_url(request),
|
|
"default_client_config": default_client_config_values(request),
|
|
}
|
|
|
|
|
|
@router.post("/admin/client-config/generate")
|
|
def admin_generate_client_config(body: ClientConfigGenerateRequest, request: Request, auth=Depends(require_permission("config:view"))):
|
|
app_id = body.app_id.strip()
|
|
channel = body.channel.strip() or "stable"
|
|
current_version = body.current_version.strip() or "1.0.0"
|
|
client_protocol = max(1, int(body.client_protocol or 3))
|
|
api_base_url = (body.api_base_url.strip() or public_api_base_url(request)).rstrip("/")
|
|
if not app_id:
|
|
raise HTTPException(status_code=400, detail="请先选择或填写 App ID")
|
|
if not validate_channel_code(channel):
|
|
raise HTTPException(status_code=400, detail="渠道代码无效")
|
|
if not api_base_url.startswith(("http://", "https://")):
|
|
raise HTTPException(status_code=400, detail="api_base_url 必须以 http:// 或 https:// 开头")
|
|
|
|
app_row = app_channel_repository.get_app(app_id)
|
|
if not app_row:
|
|
raise HTTPException(status_code=404, detail="应用不存在,请先在应用管理中创建")
|
|
channel_row = app_channel_repository.get_channel(app_id, channel)
|
|
if not channel_row:
|
|
raise HTTPException(status_code=400, detail={"error": "channel_not_found", "msg": f"渠道 {channel} 不存在"})
|
|
|
|
defaults = default_client_config_values(request)
|
|
install_root = normalize_install_root(body.install_root, defaults["install_root"])
|
|
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"],
|
|
"channel": channel,
|
|
"current_version": current_version,
|
|
"client_protocol": str(client_protocol),
|
|
"launch_token": defaults["launch_token"],
|
|
"license_key": license_key,
|
|
"client_token": defaults["client_token"],
|
|
"request_timeout_ms": defaults["request_timeout_ms"],
|
|
"temp_folder": defaults["temp_folder"],
|
|
"device_id": "",
|
|
"install_root": install_root,
|
|
"main_executable": main_executable,
|
|
"launcher_executable": defaults["launcher_executable"],
|
|
"updater_executable": defaults["updater_executable"],
|
|
"bootstrap_executable": defaults["bootstrap_executable"],
|
|
"health_check_timeout_ms": defaults["health_check_timeout_ms"],
|
|
"platform": defaults["platform"],
|
|
"arch": defaults["arch"],
|
|
}
|
|
crash_test_config = crash_report_test_values(api_base_url)
|
|
server_config = {
|
|
"api_base_url": api_base_url,
|
|
}
|
|
return {
|
|
"config": config,
|
|
"json_text": json.dumps(config, ensure_ascii=False, indent=2),
|
|
"license_warning": license_warning,
|
|
"server_config": server_config,
|
|
"server_config_text": json.dumps(server_config, ensure_ascii=False, indent=2),
|
|
"crash_test_config": crash_test_config,
|
|
"crash_test_text": crash_report_test_text(api_base_url),
|
|
}
|