diff --git a/bases/rsptx/book_server_api/routers/coach.py b/bases/rsptx/book_server_api/routers/coach.py index 4024a5c51..5431f2aaf 100644 --- a/bases/rsptx/book_server_api/routers/coach.py +++ b/bases/rsptx/book_server_api/routers/coach.py @@ -33,7 +33,7 @@ from .assessment import get_question_source, SelectQRequest # Import function for fetching api - comment out for DEV purposes -from rsptx.auth.session import auth_manager +from rsptx.auth.session import get_optional_user from rsptx.db.crud.crud import fetch_api_token from rsptx.db.crud.course import fetch_course from rsptx.db.crud.question import fetch_question @@ -202,7 +202,7 @@ async def get_question_html(request: Request, div_id: str): # @router.post("/ns/coach/parsons_scaffolding") @router.post("/parsons_scaffolding") async def parsons_scaffolding( - request: Request, course: Optional[str], user=Depends(auth_manager) + request: Request, course: Optional[str], user=Depends(get_optional_user) ): # Get `course` directly from the query string rslogger.warning(f"URL seen: {request.url}") @@ -240,6 +240,22 @@ async def parsons_scaffolding( else: parsonsexample_code = "LLM-example" + if user is None: + # Browsing-mode/anonymous visitors can get the pre-authored backup + # Parsons problem, but never the CodeTailor/LLM-personalized one -- + # that requires an account so we can call out to the LLM on their + # behalf and track usage. + if parsonsexample != "LLM-example" and parsonsexample_html: + return _build_static_parsons_response( + parsonsexample_code, parsons_attrs, personalization_level + ) + return JSONResponse( + content={ + "detail": "You need to be logged in to Runestone to access this resource" + }, + status_code=status.HTTP_401_UNAUTHORIZED, + ) + # Fetch API token api_token = None try: diff --git a/components/rsptx/auth/session.py b/components/rsptx/auth/session.py index ffa9a9097..c5ff6d65f 100644 --- a/components/rsptx/auth/session.py +++ b/components/rsptx/auth/session.py @@ -133,6 +133,19 @@ async def _load_user(user_id: str) -> AuthUserValidator: load_user = cast(Callable[[str], Awaitable[AuthUserValidator]], _load_user) +async def get_optional_user(request: Request) -> Optional[AuthUserValidator]: + """Return the authenticated user, or None for anonymous/browsing-mode visitors. + + Unlike ``Depends(auth_manager)``, this never raises -- it lets the route + itself decide what to do for logged-out users (e.g. serve a static/backup + resource instead of one that requires an account). + """ + try: + return await auth_manager(request) + except NotAuthenticatedException: + return None + + async def is_instructor( request: Request, user: Optional[AuthUserValidator] = None ) -> bool: