"""Student routes (single-session deployment)."""
from __future__ import annotations
from pathlib import Path
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Request, Response, WebSocket
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from app import auth
from app.config import Settings
from app.models import JoinRequest
from app.room import RoomManager
def router(settings: Settings, rooms: RoomManager) -> APIRouter:
api = APIRouter()
def resolve_sid(sid: str | None) -> str:
return sid if sid else (rooms.canonical_sid or settings.default_session_id)
@api.get("/")
async def student_entry(sid: str | None = None):
target_sid = resolve_sid(sid)
if not await rooms.session_exists(target_sid):
return HTMLResponse(
""
""
"
Quiz unavailable"
""
"
Ask your instructor for the link
"
"
This quiz link is missing or no longer valid.
"
"
",
status_code=404,
)
if not sid:
# Canonicalise the URL so QR codes, share links, and bookmarks
# all converge on the same sid-bearing form.
return RedirectResponse(url=f"/?sid={target_sid}", status_code=302)
return FileResponse(Path("static/student.html"))
@api.get("/api/session/{sid}")
async def session_metadata(sid: str):
if not await rooms.session_exists(sid):
raise HTTPException(status_code=404, detail="Session not found")
session = await rooms.get_session(sid)
return {
"sid": sid,
"title": session["title"],
"state": session["state"],
"current_question_idx": session["current_question_idx"],
"time_limit_default": (await rooms.get_pool_for_session(sid))["time_limit_default"],
}
@api.post("/api/session/{sid}/join")
async def join_session(sid: str, body: JoinRequest, response: Response):
if not await rooms.session_exists(sid):
raise HTTPException(status_code=404, detail="Session not found")
student_id = body.student_id.strip()
name = body.name.strip()
cookie_id = str(uuid4())
cookie_value = auth.sign_student(settings, sid, student_id, name, cookie_id)
await rooms.add_participant(sid, student_id, name, cookie_id)
auth.set_student_cookie(settings, response, cookie_value)
return {"ok": True, "cookie_id": cookie_id}
@api.get("/api/session/{sid}/me")
async def me(sid: str, request: Request):
identity = auth.get_student_identity(settings, request, sid)
if not identity:
raise HTTPException(status_code=401, detail="Student cookie required")
return await rooms.me(sid, identity["student_id"])
@api.get("/api/session/{sid}/stats")
async def stats(sid: str, request: Request, question_idx: int | None = None):
if not await rooms.session_exists(sid):
raise HTTPException(status_code=404, detail="Session not found")
identity = auth.get_student_identity(settings, request, sid)
return await rooms.stats(sid, question_idx, identity["student_id"] if identity else None)
@api.websocket("/ws/student/{sid}")
async def student_socket(websocket: WebSocket, sid: str):
identity = auth.get_student_identity_ws(settings, websocket, sid)
if not identity or not await rooms.session_exists(sid):
await websocket.close(code=4001)
return
await rooms.student_ws(websocket, sid, identity)
return api