"""Read-only external data API (`/api/v1`). A deliberately simple, API-key authenticated surface for Power BI (and any other external reporting tool). It is intentionally separate from the cookie/JWT session model used by the operator frontend: external tools cannot hold a browser session, so they present a single static key instead. Authentication: send the key either as an ``X-API-Key`` request header or an ``api_key`` query-string parameter (Power BI's Web connector supports both). The key is configured via the ``POWERBI_API_KEY`` environment variable; when it is blank the whole API is disabled and every request returns 503. """ from __future__ import annotations import secrets from datetime import date from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from sqlalchemy import select from sqlalchemy.orm import Session from app.core.config import settings from app.core.security_logging import log_security_event from app.db.session import get_db from app.models.throughput import ProductionThroughput from app.services.throughput_service import serialize_entry router = APIRouter(prefix="/api/v1", tags=["public-v1"]) _API_KEY_HEADER = "X-API-Key" def require_powerbi_api_key(request: Request) -> str: """Authorize an external request via the static Power BI API key. Returns the tenant the caller may read. Raises 503 when the API is not configured, or 401 when the key is missing/incorrect. """ configured = settings.powerbi_api_key if not configured: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="The data API is not configured.", ) presented = request.headers.get(_API_KEY_HEADER) or request.query_params.get("api_key") or "" # Constant-time comparison so the endpoint does not leak key length/contents # through response timing. if not presented or not secrets.compare_digest(presented, configured): log_security_event("authz.denied", role="powerbi", reason="invalid_api_key") raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing API key") return settings.powerbi_tenant_id @router.get("/throughput") def list_throughput( date_from: date | None = Query(default=None, description="Only entries on/after this production date (YYYY-MM-DD)."), date_to: date | None = Query(default=None, description="Only entries on/before this production date (YYYY-MM-DD)."), limit: int = Query(default=5000, ge=1, le=50000), tenant_id: str = Depends(require_powerbi_api_key), db: Session = Depends(get_db), ): """Flat list of production throughput entries for Power BI. One row per packing run, oldest first so incremental refreshes append naturally. Each row carries the same fields the operator UI shows (date, product, bag size, quantity, calculated kg, QA flags, staff, notes). """ stmt = select(ProductionThroughput).where(ProductionThroughput.tenant_id == tenant_id) if date_from is not None: stmt = stmt.where(ProductionThroughput.production_date >= date_from) if date_to is not None: stmt = stmt.where(ProductionThroughput.production_date <= date_to) stmt = stmt.order_by(ProductionThroughput.production_date.asc(), ProductionThroughput.id.asc()).limit(limit) return [serialize_entry(entry) for entry in db.scalars(stmt).all()]