163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
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
|