aboutsummaryrefslogtreecommitdiff
path: root/jb/views/auth.py
diff options
context:
space:
mode:
authorstuppie2026-08-31 16:52:15 -0600
committerstuppie2026-08-31 16:52:15 -0600
commit3f8d178d38686c1a5d71d94ebccdb61701469e95 (patch)
tree435707789cc0d1eb3c8670e4ab4bbae11d773285 /jb/views/auth.py
parent12f6fee851b68e86af658dfa17e4a0daed457dd1 (diff)
downloadamt-jb-3f8d178d38686c1a5d71d94ebccdb61701469e95.tar.gz
amt-jb-3f8d178d38686c1a5d71d94ebccdb61701469e95.zip
working on magic token and session token auth
Diffstat (limited to 'jb/views/auth.py')
-rw-r--r--jb/views/auth.py77
1 files changed, 77 insertions, 0 deletions
diff --git a/jb/views/auth.py b/jb/views/auth.py
new file mode 100644
index 0000000..0591294
--- /dev/null
+++ b/jb/views/auth.py
@@ -0,0 +1,77 @@
+"""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="/")