"""Redis-backed magic-link authentication. The user service is deliberately not coupled to this module. Once that service has resolved an email address to its stable user identifier, call ``create_magic_token`` and put the returned token in the emailed login URL. """ from typing import Annotated from fastapi import APIRouter, Depends, Response, status from fastapi.responses import HTMLResponse from jb.api.auth import ( SESSION_COOKIE_NAME, create_session, get_authenticated_user, ) from jb.api.magic_token import consume_magic_token from jb.config import settings from jb.models.auth import ( MagicLinkExchangeRequest, AuthenticatedUser, email_to_product_user_id, ) from jb.settings import BASE_HTML auth_router = APIRouter(prefix="/auth", tags=["Auth"]) @auth_router.get("/magic-link/", response_class=HTMLResponse, include_in_schema=False) def magic_link_landing_page() -> HTMLResponse: """Serve the SPA without redeeming the token; email prefetches are harmless.""" return HTMLResponse( BASE_HTML, headers={ "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Robots-Tag": "noindex, nofollow", }, ) @auth_router.post("/magic-link/exchange", status_code=status.HTTP_204_NO_CONTENT) def exchange_magic_link(body: MagicLinkExchangeRequest, response: Response) -> None: """Exchange a magic link only after its landing page makes an explicit POST.""" user_email = consume_magic_token(body.token) product_user_id = email_to_product_user_id(user_email) # todo: hit thl to make sure this user exists session_token = create_session(product_user_id) response.set_cookie( key=SESSION_COOKIE_NAME, value=session_token, max_age=settings.session_token_ttl_seconds, httponly=True, secure=not settings.debug, samesite="lax", path="/", ) @auth_router.get("/session", response_model=AuthenticatedUser) def get_session( user: Annotated[AuthenticatedUser, Depends(get_authenticated_user)], ) -> AuthenticatedUser: return user @auth_router.delete("/session", status_code=status.HTTP_204_NO_CONTENT) def delete_session( response: Response, ) -> None: # Logout is idempotent so clients can always discard their local token. # JWT sessions are stateless, so logout discards the browser cookie. A token # copied elsewhere remains valid until its short, configured expiration. response.delete_cookie(key=SESSION_COOKIE_NAME, path="/")