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}