chore: initialize insar management system v2
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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
|
||||
Reference in New Issue
Block a user