feat(exam): /api/exam template CRUD router (as-user RLS, E1 fix)
S4-5: new routers/exam/ package mounted at /api/exam (R5.1/E5, not under
/database/). Template CRUD with hybrid persistence (R5.2):
- POST/GET/GET{id}/PUT{id}/DELETE{id} /templates + PATCH /questions/{qid}
- Calls Supabase AS THE USER via SupabaseAnonClient.for_user (E1 fix), so the
RLS in 72-exam-marker.sql is enforced; no service-role for user-facing ops.
- Institute resolved/validated via the user_institute_ids() SECURITY DEFINER
RPC (institute_memberships is deny-all as-user per E4); client-supplied
institute_id is validated, never trusted (R5.5).
- Ownership pre-checked before writes (E2); out-of-scope ids read back as 404
under RLS (IDOR-safe). Soft-delete archives, never hard-deletes.
- PUT full-replace preserves client UUIDs as Neo4j join keys (spec §2).
- eb_exams.exam_code denormalised via a documented service-role catalogue
lookup (eb_exams is shared reference data, deny-all as-user per E4).
Unit tests cover auth, CRUD, ownership/IDOR, institute validation, soft-delete.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6ce6272a1e
commit
f52c3267ca
@@ -40,6 +40,7 @@ from routers.transcribe.canvas_events import router as canvas_events_router
|
||||
from routers.transcribe.keywords import router as keywords_router
|
||||
from routers.me.bootstrap_router import router as me_bootstrap_router
|
||||
from routers import tlsync_token as tlsync_token_router
|
||||
from routers.exam import router as exam_router
|
||||
|
||||
def register_routes(app: FastAPI):
|
||||
logger.info("Starting to register routes...")
|
||||
@@ -134,6 +135,9 @@ def register_routes(app: FastAPI):
|
||||
# TLSync auth token route
|
||||
app.include_router(tlsync_token_router.router, prefix="/api/tlsync", tags=["TLSync"])
|
||||
|
||||
# Exam-marker Routes (as-user Supabase, RLS-enforced; spec §4)
|
||||
app.include_router(exam_router, prefix="/api/exam", tags=["Exam"])
|
||||
|
||||
# Transcription Routes (CIS Phase 1)
|
||||
app.include_router(sessions_router, prefix="/transcribe", tags=["Transcription Sessions"])
|
||||
app.include_router(canvas_events_router, prefix="/transcribe", tags=["Transcription Canvas Events"])
|
||||
|
||||
Reference in New Issue
Block a user