A misbehaving or clock-broken device could submit period_end <= period_start, polluting the camera_records table with zero-length or inverted windows that corrupt downstream hourly analytics. Add a Pydantic model_validator so the request is rejected at the API boundary instead of silently persisting bad ranges. Found via adversarial review (run 2026-05-01-191359, both reviewers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.4 KiB
Python
76 lines
2.4 KiB
Python
# server/camera_endpoint.py
|
|
# Add these models and endpoint to the server's main.py alongside the existing BLE endpoints.
|
|
# Requires: camera_records table (see migrations/004_camera_records.sql)
|
|
#
|
|
# IMPORTANT: Before deploying, verify the HMAC message format in verify_device_hmac
|
|
# matches what the firmware computes:
|
|
# HMAC-SHA256(secret, f"{device_id}:{timestamp}:{sha256_hex(body)}")
|
|
# Headers expected: X-Device-Id, X-Timestamp, X-HMAC-Signature
|
|
|
|
import sqlite3
|
|
from typing import List
|
|
|
|
from fastapi import Depends
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
class CameraRecord(BaseModel):
|
|
period_start: int
|
|
period_end: int
|
|
entries: int = Field(ge=0)
|
|
exits: int = Field(ge=0)
|
|
|
|
@model_validator(mode="after")
|
|
def _period_order(self):
|
|
if self.period_end <= self.period_start:
|
|
raise ValueError("period_end must be strictly greater than period_start")
|
|
return self
|
|
|
|
|
|
class CameraEventsRequest(BaseModel):
|
|
location_id: str
|
|
records: List[CameraRecord]
|
|
|
|
|
|
class CameraEventsResponse(BaseModel):
|
|
status: str
|
|
accepted: int
|
|
|
|
|
|
# Add this endpoint to your FastAPI app (alongside receive_batch_events):
|
|
#
|
|
# @app.post("/api/v1/camera/events/batch", response_model=CameraEventsResponse)
|
|
# async def receive_camera_events(
|
|
# batch: CameraEventsRequest,
|
|
# device_id: str = Depends(verify_device_hmac),
|
|
# db: sqlite3.Connection = Depends(get_db),
|
|
# ):
|
|
def receive_camera_events_impl(
|
|
batch: CameraEventsRequest,
|
|
device_id: str,
|
|
db: sqlite3.Connection,
|
|
) -> CameraEventsResponse:
|
|
"""Receive hourly camera entry/exit records; idempotent on (device_id, period_start)."""
|
|
cursor = db.cursor()
|
|
accepted = 0
|
|
for record in batch.records:
|
|
try:
|
|
cursor.execute(
|
|
"""INSERT INTO camera_records
|
|
(device_id, location_id, period_start, period_end, entries, exits)
|
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
(
|
|
device_id,
|
|
batch.location_id,
|
|
record.period_start,
|
|
record.period_end,
|
|
record.entries,
|
|
record.exits,
|
|
),
|
|
)
|
|
accepted += 1
|
|
except sqlite3.IntegrityError:
|
|
pass # duplicate (device_id, period_start) — idempotent
|
|
db.commit()
|
|
return CameraEventsResponse(status="ok", accepted=accepted)
|