diff --git a/.env.example b/.env.example index f9e9d6a..6200b0a 100644 --- a/.env.example +++ b/.env.example @@ -48,8 +48,6 @@ NGINX_HEALTH_URL=http://127.0.0.1/ # Empty means allow all. Use semicolon/comma/space-separated IPs or CIDRs, for example: # NGINX_ALLOWED_CLIENT_IPS=192.168.1.10;192.168.1.23;192.168.1.0/24 NGINX_ALLOWED_CLIENT_IPS= -LICENSE_PATH= - CORS_ORIGINS=http://127.0.0.1:5173,http://localhost CORS_ALLOW_CREDENTIALS=true CORS_STRICT_MODE=false diff --git a/backend/app/api.py b/backend/app/api.py index 88f10fa..d06788b 100644 --- a/backend/app/api.py +++ b/backend/app/api.py @@ -3,7 +3,7 @@ from __future__ import annotations from fastapi import APIRouter, Depends from .routers import include_all_routers -from .routers.dependencies import _require_auth, _require_license +from .routers.dependencies import _require_auth -router = APIRouter(dependencies=[Depends(_require_license), Depends(_require_auth)]) +router = APIRouter(dependencies=[Depends(_require_auth)]) include_all_routers(router) diff --git a/backend/app/config.py b/backend/app/config.py index 7b09f12..3a7b715 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -202,7 +202,6 @@ class Settings(BaseSettings): CORS_ALLOW_CREDENTIALS: bool = True CORS_STRICT_MODE: bool = False - LICENSE_PATH: str = "" INIT_ADMIN_USERNAME: str = "admin" INIT_ADMIN_PASSWORD: str = "" INIT_ADMIN_RESET_PASSWORD: bool = False @@ -1333,7 +1332,6 @@ def validate_runtime_config() -> dict[str, Any]: _check_path(label="PYTHON_PATH", value=settings.PYTHON_PATH, errors=errors, warnings=warnings, expect_file=True) _check_path(label="NGINX_PATH", value=settings.NGINX_PATH, errors=errors, warnings=warnings, required=True, expect_file=True) - _check_path(label="LICENSE_PATH", value=settings.LICENSE_PATH, errors=errors, warnings=warnings, expect_file=True) _check_path(label="IDL_EXECUTABLE", value=settings.IDL_EXECUTABLE, errors=errors, warnings=warnings, expect_file=True) _check_path(label="IDL_WORKBENCH_PATH", value=settings.IDL_WORKBENCH_PATH, errors=errors, warnings=warnings, expect_file=True) _check_path(label="GF3_GEO_DEM_PATH", value=settings.GF3_GEO_DEM_PATH, errors=errors, warnings=warnings, expect_file=True) diff --git a/backend/app/license_service.py b/backend/app/license_service.py deleted file mode 100644 index cec2421..0000000 --- a/backend/app/license_service.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -授权验证服务 — LIC2 方案 -======================== -- 公钥硬编码在源码中,无需 .env 配置 -- 无本地 state 文件,无时间防回退(去掉脆弱的本地状态) -- 授权文件格式:LIC2|| -- 私钥只在签发工具(license-issuer/)中,不随代码部署 - -私钥丢失处理: - 1. 在 license-issuer/ 运行 rotate-key --force 生成新密钥对 - 2. 将新公钥更新到本文件的 _PUBLIC_KEY_B64 常量 - 3. 重新部署后端,重新为客户签发授权文件 -""" - -import base64 -import hashlib -import json -import os -import subprocess -import time -import uuid -from datetime import datetime, timezone -from typing import Any, Dict, Optional - -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey -from cryptography.exceptions import InvalidSignature -from .config import settings - -# ── 公钥(硬编码,与 license-issuer/public_key.b64 对应) ───────────────────── -# 私钥丢失后,用新密钥对重新签发授权,并将此处更新为新公钥。 -_PUBLIC_KEY_B64 = "QOpR1c3bONDwOzrj3IVTogE1ZHIphpwxJY8nhWa09yw=" - -_APP_DIR = os.path.dirname(os.path.abspath(__file__)) -_BACKEND_DIR = os.path.dirname(_APP_DIR) -LICENSE_PATH_DEFAULT = os.path.join(_BACKEND_DIR, "license", "license.lic") -LICENSE_STATUS_CACHE_SECONDS = int(os.getenv("LICENSE_STATUS_CACHE_SECONDS", "30")) -_LICENSE_STATUS_CACHE: Dict[str, Any] = { - "path": None, - "mtime": None, - "checked_at": 0.0, - "payload": None, -} - - -def _now_utc() -> datetime: - return datetime.now(timezone.utc) - - -# ── 机器指纹 ────────────────────────────────────────────────────────────────── - -def _run_cmd(args: list) -> str: - try: - out = subprocess.check_output( - args, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, shell=False - ) - return out.decode("utf-8", errors="ignore").strip() - except Exception: - return "" - - -def _run_powershell(cmd: str) -> str: - return _run_cmd(["powershell", "-NoProfile", "-Command", cmd]) - - -def _pick_value(text: str) -> str: - lines = [line.strip() for line in text.splitlines() if line.strip()] - if len(lines) <= 1: - return "" - values = [v for v in lines[1:] if v and v.lower() not in ("serialnumber", "uuid")] - return values[0] if values else "" - - -def _get_machine_fingerprint() -> str: - uuid_text = _run_cmd(["wmic", "csproduct", "get", "uuid"]) - if not uuid_text: - uuid_text = _run_powershell("(Get-CimInstance Win32_ComputerSystemProduct).UUID") - disk_text = _run_cmd(["wmic", "diskdrive", "get", "serialnumber"]) - if not disk_text: - disk_text = _run_powershell( - "(Get-CimInstance Win32_DiskDrive | Select-Object -First 1 -ExpandProperty SerialNumber)" - ) - - uuid_val = _pick_value(uuid_text) - disk_val = _pick_value(disk_text) - mac_val = f"{uuid.getnode():012x}" - if not mac_val or mac_val == "000000000000": - mac_val = _run_powershell( - "(Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | Select-Object -First 1 -ExpandProperty MacAddress)" - ) or "" - - raw = "|".join([uuid_val, disk_val, mac_val]) - return hashlib.sha256(raw.encode("utf-8")).hexdigest() - - -# ── 授权验证 ────────────────────────────────────────────────────────────────── - -def _license_result(result: Dict[str, Any], *, use_cache: bool, path: str, mtime: Optional[float]) -> Dict[str, Any]: - if use_cache: - _LICENSE_STATUS_CACHE.update({ - "path": path, - "mtime": mtime, - "checked_at": time.monotonic(), - "payload": dict(result), - }) - return result - - -def check_license(license_path: Optional[str] = None) -> Dict[str, Any]: - use_cache = license_path is None - license_path = license_path or settings.LICENSE_PATH or LICENSE_PATH_DEFAULT - mtime = os.path.getmtime(license_path) if os.path.exists(license_path) else None - - if use_cache: - cached = _LICENSE_STATUS_CACHE.get("payload") - cache_age = time.monotonic() - float(_LICENSE_STATUS_CACHE.get("checked_at") or 0.0) - if ( - cached is not None - and _LICENSE_STATUS_CACHE.get("path") == license_path - and _LICENSE_STATUS_CACHE.get("mtime") == mtime - and cache_age <= LICENSE_STATUS_CACHE_SECONDS - ): - return dict(cached) - - if not os.path.exists(license_path): - return _license_result({"ok": False, "reason": "未找到授权文件"}, use_cache=use_cache, path=license_path, mtime=mtime) - - try: - blob = open(license_path, "rb").read().strip() - parts = blob.split(b"|", 2) - if len(parts) != 3 or parts[0] != b"LIC2": - return _license_result({"ok": False, "reason": "授权文件格式无效"}, use_cache=use_cache, path=license_path, mtime=mtime) - - _, sig_b64, payload_b64 = parts - pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(_PUBLIC_KEY_B64)) - pub.verify(base64.b64decode(sig_b64), payload_b64) - payload = json.loads(base64.b64decode(payload_b64)) - except InvalidSignature: - return _license_result({"ok": False, "reason": "授权文件签名无效(可能已被篡改)"}, use_cache=use_cache, path=license_path, mtime=mtime) - except Exception as e: - return _license_result({"ok": False, "reason": f"授权文件解析失败: {e}"}, use_cache=use_cache, path=license_path, mtime=mtime) - - fp_expected = payload.get("fingerprint") - fp_actual = _get_machine_fingerprint() - if not fp_expected or fp_expected != fp_actual: - return _license_result({"ok": False, "reason": "机器指纹不匹配"}, use_cache=use_cache, path=license_path, mtime=mtime) - - expires_at = payload.get("expires_at") - if not expires_at: - return _license_result({"ok": False, "reason": "授权文件缺少有效期"}, use_cache=use_cache, path=license_path, mtime=mtime) - try: - expires_dt = datetime.fromisoformat(expires_at) - except Exception: - return _license_result({"ok": False, "reason": "有效期格式错误"}, use_cache=use_cache, path=license_path, mtime=mtime) - - if _now_utc() > expires_dt: - return _license_result({"ok": False, "reason": "授权已过期"}, use_cache=use_cache, path=license_path, mtime=mtime) - - return _license_result({ - "ok": True, - "issued_to": payload.get("issued_to"), - "expires_at": expires_at, - "fingerprint": fp_actual, - "license_path": license_path, - }, use_cache=use_cache, path=license_path, mtime=mtime) diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py index ad4c795..fa1e940 100644 --- a/backend/app/routers/__init__.py +++ b/backend/app/routers/__init__.py @@ -15,7 +15,6 @@ from . import ( health, idl, landsar_lt1_production, - license, logs, monitor, ops_maintenance, @@ -42,7 +41,6 @@ def include_all_routers(router: APIRouter) -> None: """将所有子路由注册到给定的 APIRouter。""" router.include_router(health.router) router.include_router(auth.router) - router.include_router(license.router) router.include_router(tasks_runtime.router) router.include_router(workflow.router) router.include_router(task_batches.router) diff --git a/backend/app/routers/dependencies.py b/backend/app/routers/dependencies.py index 1add7ad..4d009c6 100644 --- a/backend/app/routers/dependencies.py +++ b/backend/app/routers/dependencies.py @@ -38,29 +38,17 @@ _TRUSTED_PROXY_IPS: frozenset[str] = frozenset( if ip.strip() ) from ..database import get_db -from ..license_service import check_license from ..models import AuthRateLimitORM, AuthUserORM, DinsarTaskBatchORM, DinsarTaskItemORM, PsTaskBatchORM, PsTaskItemORM # --------------------------------------------------------------------------- # Path classification constants # --------------------------------------------------------------------------- -_LICENSE_EXEMPT_PATHS = { - "/api/license/status", - "/api/license/upload", - "/api/license/refresh", - "/api/health", - "/api/auth/login", - "/api/auth/logout", - "/api/auth/me", -} - READ_ONLY_METHODS = {"GET", "HEAD", "OPTIONS"} READ_SAFE_POST_PATHS = { "/api/radar-data/search", } PUBLIC_AUTH_PATHS = { - "/api/license/status", "/api/health", "/api/auth/login", "/api/auth/logout", @@ -68,8 +56,6 @@ PUBLIC_AUTH_PATHS = { HIGH_RISK_WRITE_PATH_PREFIXES = ( "/api/auth/users", - "/api/license/upload", - "/api/license/refresh", "/api/workflow/runs", "/api/task-batches/", "/api/tools/", @@ -103,13 +89,6 @@ LOGIN_THROTTLE_CLEANUP_INTERVAL_SECONDS = read_int_env( _LOGIN_THROTTLE_LOCK = asyncio.Lock() _LOGIN_THROTTLE_LAST_CLEANUP_MONO = 0.0 -_LICENSE_UPLOAD_LOCK = asyncio.Lock() -MAX_LICENSE_UPLOAD_BYTES = read_int_env( - "MAX_LICENSE_UPLOAD_BYTES", - 1024 * 1024, - minimum=1024, - maximum=20 * 1024 * 1024, -) # --------------------------------------------------------------------------- # Statistics cache @@ -363,24 +342,6 @@ async def _add_operation_audit_log( ) -# --------------------------------------------------------------------------- -# License guard -# --------------------------------------------------------------------------- - - -def _require_license(request: Request): - """ - 授权校验:未授权时拒绝所有 API。 - 使用精确路径集合匹配,防止 endswith 绕过攻击。 - """ - path = (request.url.path or "").rstrip("/") or "/" - if path in _LICENSE_EXEMPT_PATHS: - return - result = check_license() - if not result.get("ok"): - raise HTTPException(status_code=403, detail=f"License required: {result.get('reason')}") - - # --------------------------------------------------------------------------- # Auth guards # --------------------------------------------------------------------------- diff --git a/backend/app/routers/license.py b/backend/app/routers/license.py deleted file mode 100644 index 59de747..0000000 --- a/backend/app/routers/license.py +++ /dev/null @@ -1,162 +0,0 @@ -from __future__ import annotations - -import os -import tempfile -from typing import Any, Dict, Optional - -from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile -from sqlalchemy.ext.asyncio import AsyncSession - -from ..config import read_int_env, settings -from ..database import get_db -from ..license_service import check_license -from ..models import AuthUserORM -from .dependencies import ( - _add_operation_audit_log, - _get_optional_session_user, - _require_admin, - _LICENSE_UPLOAD_LOCK, - MAX_LICENSE_UPLOAD_BYTES, -) -from ..auth_service import ROLE_ADMIN - -router = APIRouter() - - -def _serialize_license_status(raw_status: Dict[str, Any], include_details: bool) -> Dict[str, Any]: - public_payload = { - "ok": bool(raw_status.get("ok")), - "reason": raw_status.get("reason"), - "expires_at": raw_status.get("expires_at"), - "issued_to": raw_status.get("issued_to"), - } - if include_details: - return { - **public_payload, - "fingerprint": raw_status.get("fingerprint"), - "license_path": raw_status.get("license_path"), - } - return public_payload - - -@router.get("/license/status") -async def license_status(request: Request, db: AsyncSession = Depends(get_db)): - """ - 授权状态查询(无需授权)。 - 默认返回脱敏字段;管理员会话可看到额外调试字段。 - """ - status = check_license() - session_user = await _get_optional_session_user(request, db) - include_details = bool(session_user and session_user.role == ROLE_ADMIN) - return _serialize_license_status(status, include_details=include_details) - - -@router.post("/license/upload") -async def license_upload( - request: Request, - file: UploadFile = File(...), - db: AsyncSession = Depends(get_db), - admin_user: AuthUserORM = Depends(_require_admin), -): - """ - 上传授权文件(覆盖后立即生效)。 - """ - filename = (file.filename or "").strip() - if not filename.lower().endswith('.lic'): - raise HTTPException(status_code=400, detail='License file must end with .lic') - - license_path = settings.LICENSE_PATH - if not license_path: - license_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "license", "license.lic") - license_path = os.path.abspath(license_path) - - license_dir = os.path.dirname(license_path) - os.makedirs(license_dir, exist_ok=True) - - content = await file.read() - if not content: - raise HTTPException(status_code=400, detail="License file is empty") - if len(content) > MAX_LICENSE_UPLOAD_BYTES: - raise HTTPException( - status_code=400, - detail=f"License file too large (max {MAX_LICENSE_UPLOAD_BYTES} bytes)", - ) - - upload_status: Optional[Dict[str, Any]] = None - upload_error: Optional[str] = None - tmp_path: Optional[str] = None - async with _LICENSE_UPLOAD_LOCK: - try: - with tempfile.NamedTemporaryFile( - mode="wb", - delete=False, - dir=license_dir, - prefix="license_upload_", - suffix=".lic.tmp", - ) as temp_file: - temp_file.write(content) - temp_file.flush() - tmp_path = temp_file.name - - upload_status = check_license(license_path=tmp_path) - if not upload_status.get("ok"): - upload_error = upload_status.get("reason") or "license validation failed" - else: - os.replace(tmp_path, license_path) - tmp_path = None - upload_status = check_license() - if not upload_status.get("ok"): - upload_error = upload_status.get("reason") or "license validation failed after activation" - except Exception as exc: - upload_error = str(exc) - finally: - if tmp_path and os.path.exists(tmp_path): - try: - os.remove(tmp_path) - except OSError: - pass - - if upload_error: - await _add_operation_audit_log( - db, - request=request, - action="license_upload_failed", - user=admin_user, - resource="license/upload", - detail={"filename": filename, "reason": upload_error}, - ) - await db.commit() - raise HTTPException(status_code=400, detail=f"License invalid: {upload_error}") - - await _add_operation_audit_log( - db, - request=request, - action="license_uploaded", - user=admin_user, - resource="license/upload", - detail={"filename": filename, "size": len(content)}, - ) - await db.commit() - return {"message": "License uploaded", "status": upload_status} - - -@router.post("/license/refresh") -async def license_refresh( - request: Request, - db: AsyncSession = Depends(get_db), - admin_user: AuthUserORM = Depends(_require_admin), -): - """ - 刷新授权状态。 - """ - status = check_license() - await _add_operation_audit_log( - db, - request=request, - action="license_refreshed", - user=admin_user, - resource="license/refresh", - detail={"license_ok": bool(status.get("ok")), "reason": status.get("reason")}, - ) - await db.commit() - return status diff --git a/docs/APP_LICENSE_REMOVAL_AUDIT_20260702.md b/docs/APP_LICENSE_REMOVAL_AUDIT_20260702.md new file mode 100644 index 0000000..27bb5c1 --- /dev/null +++ b/docs/APP_LICENSE_REMOVAL_AUDIT_20260702.md @@ -0,0 +1,32 @@ +# Application License Removal Audit + +Date: 2026-07-02 + +## Decision + +The application-level offline license gate has been removed. The system now relies on login sessions, role checks, audit logging, deployment network controls, and operation-level guards. LandSAR vendor license configuration is not part of this removal and remains required for LandSAR production runtimes. + +## Removed Scope + +- Global FastAPI license dependency and `/api/license/*` routes. +- Backend LIC2 verification service and bundled license issuer tools. +- Frontend license status store fields, license overlay, login-page license warning, and header license chip. +- Deployment example variable `LICENSE_PATH`. +- Audit-log display labels for removed license upload/refresh actions. + +## Preserved Scope + +- User authentication and session cookie flow. +- Admin versus read-only write protection. +- High-risk write audit logging. +- LandSAR runtime license settings such as `LANDSAR_LICENSE_MODE`, `LANDSAR_LICENSE_HOST`, and `LANDSAR_LICENSE_PORT`. + +## Operational Result + +Startup and API availability no longer depend on a local `.lic` file or issuer-generated public/private key material. Removing the gate also removes a misleading security boundary: application access control must be handled by account roles, network exposure, and deployment policy rather than a local offline license file. + +## Follow-up Checks + +- Keep `.env` and initial admin credentials out of deployable artifacts. +- Keep LandSAR license server settings documented as vendor runtime requirements, not application authorization. +- Continue using the operations maintenance panel for failed task cleanup and audit review. diff --git a/docs/FRONTEND_PRODUCTION_UI_REFINEMENT.md b/docs/FRONTEND_PRODUCTION_UI_REFINEMENT.md index 3a4a1c7..181d162 100644 --- a/docs/FRONTEND_PRODUCTION_UI_REFINEMENT.md +++ b/docs/FRONTEND_PRODUCTION_UI_REFINEMENT.md @@ -45,7 +45,7 @@ ## 2026-06-21 页眉与生产面板补充 - 页眉移除 DB、Worker、IDL、Ollama、Nginx 等运行状态灯,系统健康状态集中放在“运行维护”模块。 -- 页眉改为单位 logo、单位名称、系统名称、授权摘要、任务摘要和用户操作。 +- 页眉改为单位 logo、单位名称、系统名称、任务摘要和用户操作。 - 单位名、系统名、页眉标语和 logo URL 支持通过 `VITE_APP_*` 环境变量配置,默认使用 `frontend/src/logo.jpg`。 - 新增 `PRODUCT.md`,记录系统面向科研工程单位的产品定位:科研、专业、克制,同时要求严谨、稳定、工程化。 - SBAS 生产面板中高曝光的 Runtime Status、Task queue、Workflow 说明文案改为中文表达,保留必要英文术语。 diff --git a/frontend/src/App.css b/frontend/src/App.css index 8fbe323..1f16332 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -3802,29 +3802,6 @@ input[type="checkbox"] { min-width: 0; } -.status-license-chip { - display: inline-flex; - align-items: center; - padding: 3px 7px; - border-radius: 999px; - border: 1px solid var(--color-border); - background: var(--color-panel-muted); - color: var(--color-text-secondary); - line-height: 1.2; -} - -.status-license-chip.ok { - border-color: rgba(22, 163, 74, 0.24); - background: rgba(22, 163, 74, 0.08); - color: #166534; -} - -.status-license-chip.fail { - border-color: rgba(220, 38, 38, 0.24); - background: rgba(220, 38, 38, 0.08); - color: #991b1b; -} - .status-actions { display: flex; justify-content: flex-end; @@ -3891,12 +3868,6 @@ input[type="checkbox"] { white-space: nowrap; } -.status-license { - font-size: 0.75em; - color: var(--color-text-muted); - white-space: nowrap; -} - .status-task-bar { width: 120px; height: 6px; @@ -6194,12 +6165,15 @@ input[type="checkbox"] { .production-workspace-shell .dinsar-production-shell, .production-workspace-shell .dinsar-products-page { + width: 100%; max-width: none; margin-left: 0; margin-right: 0; + box-sizing: border-box; } -.production-workspace-shell .dinsar-production-shell { +.production-workspace-shell .dinsar-production-shell, +.production-workspace-shell .dinsar-products-page { padding: 0; } @@ -7090,7 +7064,7 @@ input[type="checkbox"] { } .dinsar-products-catalog-section .dinsar-catalog-workspace { - grid-template-columns: minmax(360px, 420px) minmax(0, 1fr); + grid-template-columns: minmax(380px, 0.36fr) minmax(0, 1fr); } .sbas-products-page { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5486f21..369871b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -184,22 +184,12 @@ function App() { // --- Zustand stores --- const { currentUser, setCurrentUser, authChecked, setAuthChecked, - licenseStatus, setLicenseStatus, licenseLoading, setLicenseLoading, - licenseUploadStatus, setLicenseUploadStatus, licenseFileName, setLicenseFileName, healthStatus, setHealthStatus, healthLoading, setHealthLoading, healthError, setHealthError, } = useAuthStore(useShallow((state) => ({ currentUser: state.currentUser, setCurrentUser: state.setCurrentUser, authChecked: state.authChecked, setAuthChecked: state.setAuthChecked, - licenseStatus: state.licenseStatus, - setLicenseStatus: state.setLicenseStatus, - licenseLoading: state.licenseLoading, - setLicenseLoading: state.setLicenseLoading, - licenseUploadStatus: state.licenseUploadStatus, - setLicenseUploadStatus: state.setLicenseUploadStatus, - licenseFileName: state.licenseFileName, - setLicenseFileName: state.setLicenseFileName, healthStatus: state.healthStatus, setHealthStatus: state.setHealthStatus, healthLoading: state.healthLoading, @@ -405,13 +395,12 @@ function App() { const floodPairPreviewLayerRef = useRef(null); const floodVectorLayersRef = useRef({}); const mapRegionLayerRef = useRef(null); - const prevLicenseOkRef = useRef(false); + const prevAppReadyRef = useRef(false); const initializeAppDataRef = useRef(null); const addLogRef = useRef(addLog); const handleTaskCompletionRef = useRef(null); const updateLayerTooltipRef = useRef(null); const radarSearchRequestSeqRef = useRef(0); - const licenseFileRef = useRef(null); const foundPairsRef = useRef(foundPairs); const hazardLayersRef = useRef({}); const dinsarResultLayersRef = useRef({}); @@ -712,14 +701,10 @@ function App() { const { handleLoginSuccess, handleLogout, - fetchLicenseStatus, fetchHealthStatus, - handleLicenseUpload, } = useAppAuthLifecycle({ - ensureCanOperate, clearRadarSearchResults, radarSearchRequestSeqRef, - prevLicenseOkRef, aoeLayerRef, activeLayersRef, radarPreviewLayersRef, @@ -727,13 +712,9 @@ function App() { setCurrentUser, setAuthChecked, setPendingTaskIds, - setLicenseLoading, - setLicenseStatus, setHealthLoading, setHealthError, setHealthStatus, - setLicenseFileName, - setLicenseUploadStatus, setAoiLayer, setAllData, setRadarPagination, @@ -838,14 +819,14 @@ function App() { }, [baseLayerKey, currentUser?.id]); useEffect(() => { - if (licenseLoading || !authChecked) return; - const isOk = !!licenseStatus?.ok && !!currentUser; - const prevOk = prevLicenseOkRef.current; - if (isOk && !prevOk) { + if (!authChecked) return; + const isReady = !!currentUser; + const wasReady = prevAppReadyRef.current; + if (isReady && !wasReady) { initializeAppDataRef.current?.(); } - prevLicenseOkRef.current = isOk; - }, [licenseLoading, licenseStatus?.ok, authChecked, currentUser]); + prevAppReadyRef.current = isReady; + }, [authChecked, currentUser]); const { fetchDinsarResults, @@ -917,7 +898,6 @@ function App() { handleCancelActiveTasks, } = useGlobalTaskControl({ currentUser, - licenseOk: !!licenseStatus?.ok, activeTasks, setActiveTasks, setRuntimeSummary, @@ -2002,7 +1982,6 @@ function App() { const selectedPairsCount = foundPairs.filter(p => p.isSelected).length; const hasEnoughRadarScenesForPlanning = radarImagingDates.length >= 2; - const licenseOk = !!licenseStatus?.ok; const avgTaskProgress = activeTasks.length ? Math.round(activeTasks.reduce((sum, t) => sum + (t.progress || 0), 0) / activeTasks.length) : 0; @@ -2118,7 +2097,6 @@ function App() { activeTasks={activeTasks} avgTaskProgress={avgTaskProgress} runtimeSummary={runtimeSummary} - licenseStatus={licenseStatus} healthStatus={healthStatus} healthLoading={healthLoading} healthError={healthError} @@ -2136,7 +2114,6 @@ function App() { currentUser={currentUser} language={language} apiEndpoint={apiClient.defaults.baseURL} - licenseOk={licenseOk} foundPairs={foundPairs} dinsarTotal={dinsarPagination.total} selectedPairsCount={selectedPairsCount} @@ -2192,14 +2169,7 @@ function App() { onPairingAoiModeChange={handlePairingAoiModeChange} onPairingProvinceChange={handlePairingProvinceChange} onPairingCityChange={handlePairingCityChange} - licenseLoading={licenseLoading} - licenseStatus={licenseStatus} isAdmin={isAdmin} - licenseFileRef={licenseFileRef} - onUploadFile={handleLicenseUpload} - onRefreshLicenseStatus={fetchLicenseStatus} - licenseFileName={licenseFileName} - licenseUploadStatus={licenseUploadStatus} activeTasks={activeTasks} runtimeSummary={runtimeSummary} showCancelTask={showCancelTask} diff --git a/frontend/src/AuditLogPanel.jsx b/frontend/src/AuditLogPanel.jsx index e23e198..e7a1538 100644 --- a/frontend/src/AuditLogPanel.jsx +++ b/frontend/src/AuditLogPanel.jsx @@ -12,9 +12,6 @@ const ACTION_LABELS = { logout: '退出登录', user_created: '创建用户', user_updated: '更新用户', - license_uploaded: '上传授权文件', - license_upload_failed: '上传授权失败', - license_refreshed: '刷新授权状态', task_queued: '任务入队', batch_created: '创建任务批次', batch_marked_complete: '批次标记完成', diff --git a/frontend/src/LoginPage.jsx b/frontend/src/LoginPage.jsx index 21a88b6..0ccb4b1 100644 --- a/frontend/src/LoginPage.jsx +++ b/frontend/src/LoginPage.jsx @@ -27,21 +27,8 @@ const LoginPage = ({ onLoginSuccess }) => { const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); - const [licenseStatus, setLicenseStatus] = useState(null); const [activeSlide, setActiveSlide] = useState(0); - useEffect(() => { - const checkLicense = async () => { - try { - const response = await apiClient.get('/license/status'); - setLicenseStatus(response.data || null); - } catch { - setLicenseStatus({ ok: false, reason: '无法获取授权状态' }); - } - }; - checkLicense(); - }, []); - useEffect(() => { const timer = window.setInterval(() => { setActiveSlide((current) => (current + 1) % loginSlides.length); @@ -85,12 +72,6 @@ const LoginPage = ({ onLoginSuccess }) => {

内网受控访问,请使用管理员分配的账号登录。

- {licenseStatus && !licenseStatus.ok && ( -
- 当前授权状态异常:{licenseStatus.reason || '未授权'} -
- )} -