Review
Magic-link login return-to for Silo co-op pickups
Silo Collective members kept dropping off the weekly bulk-food pickup flow when the emailed magic link dumped them on the home dashboard. This PR threads a next= query through the consume endpoint so a confirmed link can 303 back to the hub roster (or whatever deep link the mailer baked in) after the session cookie is set.
FastAPITier 2securityauthredirect
Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.
app/routers/magic_auth.py+18-8
| 22 | 22 | from fastapi import APIRouter, HTTPException, Query | |
| 23 | 23 | from fastapi.responses import RedirectResponse | |
| 24 | 24 | ||
| 25 | 25 | from app.auth.session import attach_member_session | |
| 26 | 26 | from app.auth.tokens import consume_magic_token | |
| 27 | 27 | ||
| 28 | 28 | router = APIRouter(prefix="/auth", tags=["auth"]) | |
| 29 | 29 | ||
| 30 | - | @router.get("/magic") | |
| 31 | - | async def magic_login(token: str): | |
| 32 | - | member = consume_magic_token(token) | |
| 33 | - | if member is None: | |
| 34 | - | raise HTTPException(status_code=400, detail="invalid or expired link") | |
| 35 | - | response = RedirectResponse(url="/dashboard", status_code=303) | |
| 36 | - | attach_member_session(response, member) | |
| 37 | - | return response | |
| 30 | + | def safe_return_path(next: str) -> str: | |
| 31 | + | # only same-site relative paths — blocks https://evil.example | |
| 32 | + | if next.startswith("/"): | |
| 33 | + | return next | |
| 34 | + | return "/dashboard" | |
| 35 | + | ||
| 36 | + | @router.get("/magic") | |
| 37 | + | async def magic_login( | |
| 38 | + | token: str, | |
| 39 | + | next: str = Query("/dashboard", description="post-login destination"), | |
| 40 | + | ): | |
| 41 | + | member = consume_magic_token(token) | |
| 42 | + | if member is None: | |
| 43 | + | raise HTTPException(status_code=400, detail="invalid or expired link") | |
| 44 | + | dest = safe_return_path(next) | |
| 45 | + | response = RedirectResponse(url=dest, status_code=303) | |
| 46 | + | attach_member_session(response, member) | |
| 47 | + | return response |