---
estimated_steps: 48
estimated_files: 3
skills_used: []
---

# T02: Create consent schemas, router, and wire into main.py

Build the consent API: Pydantic schemas for request/response, a new router with 5 endpoints (list, get, update, history, admin summary), ownership verification, and registration in main.py.

## Steps

1. Read `backend/schemas.py` to see existing Pydantic v2 patterns (`ConfigDict(from_attributes=True)`, request vs response models).
2. Add consent schemas to `backend/schemas.py`:
   - `VideoConsentUpdate(BaseModel)`: `kb_inclusion: bool | None = None`, `training_usage: bool | None = None`, `public_display: bool | None = None` (partial update — only non-None fields are changed)
   - `VideoConsentRead(BaseModel)`: all consent fields + `source_video_id`, `video_filename`, `creator_id`, `updated_at`, `model_config = ConfigDict(from_attributes=True)`
   - `ConsentAuditEntry(BaseModel)`: `version`, `field_name`, `old_value`, `new_value`, `changed_by`, `created_at`, `model_config = ConfigDict(from_attributes=True)`
   - `ConsentListResponse(BaseModel)`: `items: list[VideoConsentRead]`, `total: int`
   - `ConsentSummary(BaseModel)`: per-flag counts
3. Create `backend/routers/consent.py`:
   - `APIRouter(prefix="/consent", tags=["consent"])`
   - Helper `_verify_video_ownership(video_id, user, session)` — loads SourceVideo, checks `user.creator_id` matches `video.creator_id`. Raises 403 if user has no `creator_id` or doesn't own video. Admin role (checked via `user.role == UserRole.admin`) bypasses the ownership check.
   - `GET /consent/videos` — list consent for current creator's videos, paginated (offset/limit). Join with SourceVideo to get filename.
   - `GET /consent/videos/{video_id}` — single video consent status
   - `PUT /consent/videos/{video_id}` — upsert consent: create `VideoConsent` if not exists, update changed fields, create `ConsentAuditLog` entry for each changed field with incrementing version number. Use `select ... for update` or compute max version from existing audit entries.
   - `GET /consent/videos/{video_id}/history` — audit trail ordered by version
   - `GET /consent/admin/summary` — admin-only aggregate stats, guarded by `require_role(UserRole.admin)`
   - Add `chrysopedia.consent` logger, log on PUT: `logger.info("Consent updated", extra={"video_id": ..., "fields_changed": ...})`
4. Register router in `backend/main.py`: `from routers import consent` and `app.include_router(consent.router, prefix="/api/v1")`
5. Verify: start the app and confirm `/docs` shows the consent endpoints (or just verify the import chain works).

## Must-Haves

- [ ] Partial update schema (only non-None fields trigger changes)
- [ ] Ownership verification helper with admin bypass
- [ ] PUT creates audit log entries per changed field with incrementing versions
- [ ] GET /consent/videos returns video filename alongside consent flags
- [ ] All endpoints require authentication via `get_current_user`
- [ ] Users without `creator_id` get 403 (not 500)
- [ ] Admin summary endpoint requires admin role
- [ ] Router registered in main.py

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| PostgreSQL | SQLAlchemy raises, FastAPI returns 500 | Connection pool timeout → 500 | N/A (ORM handles) |
| Auth (JWT) | 401 Unauthorized | N/A (local decode) | 401 (invalid token) |

## Negative Tests

- **Malformed inputs**: non-UUID video_id in path → 422. Empty PUT body (no fields) → no changes, no audit entries.
- **Error paths**: unauthenticated → 401, no creator_id → 403, wrong creator → 403, nonexistent video → 404
- **Boundary conditions**: first PUT for a video creates VideoConsent + audit entries. Repeated PUT with same values → no new audit entries.

## Inputs

- `backend/models.py` — VideoConsent, ConsentAuditLog, ConsentField models from T01
- `backend/schemas.py` — existing schema patterns
- `backend/routers/auth.py` — auth router pattern to follow
- `backend/auth.py` — get_current_user, require_role dependencies
- `backend/main.py` — router registration pattern

## Expected Output

- `backend/schemas.py` — updated with consent schemas
- `backend/routers/consent.py` — new consent router
- `backend/main.py` — updated with consent router registration

## Inputs

- `backend/models.py`
- `backend/schemas.py`
- `backend/routers/auth.py`
- `backend/auth.py`
- `backend/main.py`

## Expected Output

- `backend/schemas.py`
- `backend/routers/consent.py`
- `backend/main.py`

## Verification

cd backend && python -c "from routers.consent import router; print(f'{len(router.routes)} routes OK')"
