feat: consent records page, tab persistence, and snippet copy fix (#9)
feat: consent records list endpoint and top-level admin page
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy import select
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.db import get_db
|
||||
@@ -11,6 +11,7 @@ from src.models.site import Site
|
||||
from src.schemas.auth import CurrentUser
|
||||
from src.schemas.consent import (
|
||||
ConsentRecordCreate,
|
||||
ConsentRecordListResponse,
|
||||
ConsentRecordResponse,
|
||||
ConsentVerifyResponse,
|
||||
)
|
||||
@@ -86,6 +87,63 @@ async def _load_record_for_org(
|
||||
return record
|
||||
|
||||
|
||||
@router.get("/", response_model=ConsentRecordListResponse)
|
||||
async def list_consent_records(
|
||||
site_id: uuid.UUID = Query(..., description="Filter by site"),
|
||||
visitor_id: str | None = Query(None, description="Filter by visitor ID (exact match)"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
current_user: CurrentUser = Depends(require_role("owner", "admin", "editor", "viewer")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> dict:
|
||||
"""List consent records for a site, with optional visitor_id filter.
|
||||
|
||||
Tenant-isolated — the site must belong to the caller's organisation.
|
||||
Returns newest records first.
|
||||
"""
|
||||
# Verify site belongs to the caller's org.
|
||||
site = (
|
||||
await db.execute(
|
||||
select(Site).where(
|
||||
Site.id == site_id,
|
||||
Site.organisation_id == current_user.organisation_id,
|
||||
Site.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if site is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found")
|
||||
|
||||
base = select(ConsentRecord).where(ConsentRecord.site_id == site_id)
|
||||
count_base = (
|
||||
select(func.count()).select_from(ConsentRecord).where(ConsentRecord.site_id == site_id)
|
||||
)
|
||||
|
||||
if visitor_id:
|
||||
base = base.where(ConsentRecord.visitor_id == visitor_id)
|
||||
count_base = count_base.where(ConsentRecord.visitor_id == visitor_id)
|
||||
|
||||
total = await db.scalar(count_base) or 0
|
||||
items = (
|
||||
(
|
||||
await db.execute(
|
||||
base.order_by(ConsentRecord.consented_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"items": list(items),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{consent_id}", response_model=ConsentRecordResponse)
|
||||
async def get_consent(
|
||||
consent_id: uuid.UUID,
|
||||
|
||||
@@ -50,6 +50,15 @@ class ConsentRecordResponse(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ConsentRecordListResponse(BaseModel):
|
||||
"""Paginated list of consent records."""
|
||||
|
||||
items: list[ConsentRecordResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class ConsentVerifyResponse(BaseModel):
|
||||
"""Audit proof that a consent record exists."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user