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:
92
app/main.py
92
app/main.py
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user