Add batch management APIs, API security, rate limiting, and optimizations

- Batch device CRUD: POST /api/devices/batch (create 500), PUT /api/devices/batch (update 500),
  POST /api/devices/batch-delete (delete 100) with WHERE IN bulk queries
- Batch command: POST /api/commands/batch with model_validator mutual exclusion
- API key auth (X-API-Key header, secrets.compare_digest timing-safe)
- Rate limiting via SlowAPIMiddleware (60/min default, 30/min writes)
- Real client IP extraction (X-Forwarded-For / CF-Connecting-IP)
- Global exception handler (no stack trace leaks, passes HTTPException through)
- CORS with auto-disable credentials on wildcard origins
- Schema validation: IMEI pattern, lat/lon ranges, Literal enums, MAC/UUID patterns
- Heartbeats router, per-ID endpoints for locations/attendance/bluetooth
- Input dedup in batch create, result ordering preserved
- Baidu reverse geocoding, Gaode map tiles with WGS84→GCJ02 conversion
- Device detail panel with feature toggles and command controls
- Side panel for location/beacon pages with auto-select active device

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
2026-03-20 09:18:43 +00:00
parent 1bdbe4fa19
commit 7d6040af41
23 changed files with 1564 additions and 294 deletions

View File

@@ -1,19 +1,31 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
from app.database import init_db, async_session
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.middleware import SlowAPIMiddleware
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from app.database import init_db, async_session, engine
from app.tcp_server import tcp_manager
from app.config import settings
from app.routers import devices, locations, alarms, attendance, commands, bluetooth, beacons
from app.routers import devices, locations, alarms, attendance, commands, bluetooth, beacons, heartbeats
from app.dependencies import verify_api_key
import asyncio
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Rate limiter (shared instance via app.extensions for router access)
from app.extensions import limiter
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
@@ -40,7 +52,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="KKS Badge Management System / KKS工牌管理系统",
description="""
## KKS P240 & P241 蓝牙工牌管理后台
## KKS P240 & P241 蓝牙工牌管理后台 API
### 功能模块 / Features:
- **设备管理 / Device Management** - 设备注册、状态监控
@@ -50,6 +62,10 @@ app = FastAPI(
- **指令管理 / Commands** - 远程指令下发与留言
- **蓝牙数据 / Bluetooth** - 蓝牙打卡与定位数据
- **信标管理 / Beacons** - 蓝牙信标注册与位置配置
- **心跳数据 / Heartbeats** - 设备心跳记录查询
### 认证 / Authentication:
设置 `API_KEY` 环境变量后,所有 `/api/` 请求需携带 `X-API-Key` 请求头。
### 通讯协议 / Protocol:
- TCP端口: {tcp_port} (设备连接)
@@ -61,14 +77,48 @@ app = FastAPI(
lifespan=lifespan,
)
# Include routers
app.include_router(devices.router)
app.include_router(locations.router)
app.include_router(alarms.router)
app.include_router(attendance.router)
app.include_router(commands.router)
app.include_router(bluetooth.router)
app.include_router(beacons.router)
# Rate limiter — SlowAPIMiddleware applies default_limits to all routes
app.state.limiter = limiter
app.add_middleware(SlowAPIMiddleware)
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# CORS
_origins = [o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=_origins,
allow_credentials="*" not in _origins,
allow_methods=["*"],
allow_headers=["*"],
)
# Global exception handler — prevent stack trace leaks
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
# Let FastAPI handle its own exceptions
if isinstance(exc, (StarletteHTTPException, RequestValidationError)):
raise exc
logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
return JSONResponse(
status_code=500,
content={"code": 500, "message": "Internal server error", "data": None},
)
# Include API routers (all under /api/ prefix, protected by API key if configured)
_api_deps = [verify_api_key] if settings.API_KEY else []
app.include_router(devices.router, dependencies=[*_api_deps])
app.include_router(locations.router, dependencies=[*_api_deps])
app.include_router(alarms.router, dependencies=[*_api_deps])
app.include_router(attendance.router, dependencies=[*_api_deps])
app.include_router(commands.router, dependencies=[*_api_deps])
app.include_router(bluetooth.router, dependencies=[*_api_deps])
app.include_router(beacons.router, dependencies=[*_api_deps])
app.include_router(heartbeats.router, dependencies=[*_api_deps])
_STATIC_DIR = Path(__file__).parent / "static"
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
@@ -90,11 +140,25 @@ async def root():
"redoc": "/redoc",
"admin": "/admin",
"tcp_port": settings.TCP_PORT,
"auth_enabled": settings.API_KEY is not None,
}
@app.get("/health", tags=["Root"])
async def health():
"""Health check with database connectivity test."""
db_ok = False
try:
from sqlalchemy import text
async with engine.connect() as conn:
await conn.execute(text("SELECT 1"))
db_ok = True
except Exception:
logger.warning("Health check: database unreachable")
status = "healthy" if db_ok else "degraded"
return {
"status": "healthy",
"status": status,
"database": "ok" if db_ok else "error",
"connected_devices": len(tcp_manager.connections),
}