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), }