add banbanmini backend

This commit is contained in:
HycJack
2026-03-24 15:04:36 +08:00
parent 0d0f995dc2
commit 7510ca6df1
197 changed files with 13008 additions and 0 deletions

250
talkingq-url/api/auth.py Normal file
View File

@@ -0,0 +1,250 @@
from fastapi import Depends, HTTPException, status, Request, APIRouter, Path, Query
from fastapi.security import APIKeyHeader
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any, Tuple
from services.device_auth_manager import device_auth_manager
from utils.logger import session_logger
from config import settings
import re
router = APIRouter(prefix="/api/auth", tags=["Auth"])
device_id_header = APIKeyHeader(name="X-Device-ID", auto_error=False)
device_serial_header = APIKeyHeader(name="X-Device-Serial", auto_error=False)
admin_api_key_header = APIKeyHeader(name="X-Admin-API-Key", auto_error=False)
client_api_key_header = APIKeyHeader(name="X-Client-Key", auto_error=False)
async def verify_device(
device_id: str = Depends(device_id_header),
serial_number: str = Depends(device_serial_header)
):
"""验证设备ID和序列号"""
if not device_id or not serial_number:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少设备认证信息",
headers={"WWW-Authenticate": "DeviceAuth"},
)
is_valid = await device_auth_manager.authenticate_device(device_id, serial_number)
if not is_valid:
session_logger.warning("system", "auth", f"设备 {device_id} 认证失败")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="设备认证失败",
headers={"WWW-Authenticate": "DeviceAuth"},
)
return device_id
async def api_auth(
device_id: str = Depends(verify_device)
):
"""API认证依赖项只使用设备ID和序列号认证"""
return device_id
async def admin_auth(api_key: str = Depends(admin_api_key_header)) -> bool:
"""验证管理员API密钥"""
if not api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少管理员API密钥",
headers={"WWW-Authenticate": "AdminAuth"},
)
return api_key == settings.admin_api_key
async def client_auth(client_api_key: str = Depends(client_api_key_header)) -> bool:
"""验证小程序客户端API密钥"""
if not client_api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少客户端API密钥",
headers={"WWW-Authenticate": "ClientAuth"},
)
if client_api_key != settings.client_api_key:
session_logger.warning("system", "auth", "客户端API密钥验证失败")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="客户端API密钥无效",
headers={"WWW-Authenticate": "ClientAuth"},
)
return True
async def admin_or_api_auth(
device_id: str = Path(...),
admin_api_key: str = Depends(admin_api_key_header),
authenticated_device_id: str = Depends(api_auth)
) -> Tuple[str, bool]:
"""允许管理员或设备本身访问,返回(认证设备ID, 是否管理员)"""
is_admin = admin_api_key == settings.admin_api_key if admin_api_key else False
if not is_admin and authenticated_device_id != device_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="没有权限查询其他设备",
headers={"WWW-Authenticate": "DeviceAuth"},
)
return authenticated_device_id, is_admin
class DeviceRegistrationRequest(BaseModel):
device_id: str = Field(..., min_length=16, max_length=64, description="设备ID基于ESP32的MAC地址生成")
serial_number: str = Field(..., min_length=18, max_length=64, description="序列号,包含批次前缀和唯一码")
batch_id: Optional[str] = Field(None, description="批次ID使用YYYYMMDD格式")
is_active: bool = Field(True, description="设备激活状态")
class DeviceRegistrationResponse(BaseModel):
status: str
device_id: str
serial_number: str
batch_id: Optional[str] = None
is_active: bool
@router.post("/register-device", response_model=DeviceRegistrationResponse)
async def register_device(
request: DeviceRegistrationRequest,
admin_authenticated: bool = Depends(admin_auth)
):
"""注册新设备到认证白名单"""
batch_id = request.batch_id
if not batch_id and len(request.serial_number) >= 8:
batch_id = request.serial_number[:8]
success = await device_auth_manager.register_device(
request.device_id,
request.serial_number,
batch_id,
request.is_active
)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="设备注册失败",
)
return {
"status": "success",
"device_id": request.device_id,
"serial_number": request.serial_number,
"batch_id": batch_id,
"is_active": request.is_active
}
class BatchDeviceRegistrationRequest(BaseModel):
devices: List[DeviceRegistrationRequest]
class BatchDeviceRegistrationResponse(BaseModel):
status: str
registered_count: int
failed_count: int
details: List[Dict[str, Any]]
@router.post("/register-devices-batch", response_model=BatchDeviceRegistrationResponse)
async def register_devices_batch(
request: BatchDeviceRegistrationRequest,
admin_authenticated: bool = Depends(admin_auth)
):
"""批量注册设备到认证白名单"""
results = []
success_count = 0
failed_count = 0
for device in request.devices:
batch_id = device.batch_id
if not batch_id and len(device.serial_number) >= 8:
batch_id = device.serial_number[:8]
success = await device_auth_manager.register_device(
device.device_id,
device.serial_number,
batch_id,
device.is_active
)
if success:
success_count += 1
results.append({
"status": "success",
"device_id": device.device_id,
"serial_number": device.serial_number,
"batch_id": batch_id
})
else:
failed_count += 1
results.append({
"status": "failed",
"device_id": device.device_id,
"serial_number": device.serial_number
})
return {
"status": "completed",
"registered_count": success_count,
"failed_count": failed_count,
"details": results
}
@router.get("/verify-device/{device_id}")
async def verify_device(
device_id: str = Path(...),
serial_number: str = None,
auth_result: Tuple[str, bool] = Depends(admin_or_api_auth)
):
"""验证设备是否已正确注册 (管理员或设备自身可使用)"""
authenticated_device_id, is_admin = auth_result
if is_admin and serial_number:
is_valid = await device_auth_manager.authenticate_device(device_id, serial_number)
elif not is_admin:
is_valid = True
else:
is_valid = None
device_info = await device_auth_manager.get_device_info(device_id)
return {
"device_id": device_id,
"is_valid": is_valid,
"device_info": device_info
}
class SerialQueryResponse(BaseModel):
device_id: str
serial_number: str
@router.get("/query-serial", response_model=SerialQueryResponse)
async def query_serial_by_mac(
mac_address: str = Query(..., description="设备MAC地址"),
client_authenticated: bool = Depends(client_auth)
):
"""通过MAC地址查询设备ID和序列号"""
mac_pattern = re.compile(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$')
if not mac_pattern.match(mac_address):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="MAC地址格式无效应为XX:XX:XX:XX:XX:XX或XX-XX-XX-XX-XX-XX格式"
)
normalized_mac = mac_address.replace(":", "").replace("-", "").upper()
expected_device_id = f"TalkingQ_{normalized_mac}"
device_info = await device_auth_manager.get_device_info(expected_device_id)
if not device_info or not device_info.get("is_active", False):
session_logger.warning("system", "auth", f"未找到MAC地址{mac_address}对应的设备或设备未激活")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="未找到对应的设备信息或设备未激活"
)
return {
"device_id": expected_device_id,
"serial_number": device_info["serial_number"]
}
@router.get("/verify")
async def verify_auth(device_id: str = Depends(api_auth)):
"""验证设备认证状态"""
return {"status": "authenticated", "device_id": device_id}