Review

Email magic-link password reset for TackRoom members

Stable boarders kept failing the SMS OTP reset path on spotty barn Wi‑Fi. Switches TackRoom to an emailed magic link that lands on GET /auth/reset with the one-time token in the query string, plus request logging so support can see who hit the reset page.

FastAPITier 2securityauthlogging

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

app/routers/password_reset.py+8-5
2828from fastapi import APIRouter, Depends, HTTPException, Request
2929from fastapi.responses import HTMLResponse, RedirectResponse
3030
3131from app.auth.tokens import issue_reset_token, consume_reset_token
3232from app.mail import send_member_email
3333from app.logging import access_log
3434
3535router = APIRouter(prefix="/auth", tags=["auth"])
3636
3737@router.post("/forgot")
3838async def forgot_password(email: str):
3939 token = issue_reset_token(email, ttl_hours=24)
40- send_member_email(email, template="reset_otp", code=token[:6])
40+ # magic link works offline at the barn; no SMS required
41+ link = f"https://members.tackroom.app/auth/reset?token={token}"
42+ send_member_email(email, template="reset_link", link=link)
4143 return {"ok": True}
4244
43-@router.post("/reset")
44-async def reset_password(email: str, code: str, new_password: str):
45- if not consume_reset_token(email, code):
46- raise HTTPException(status_code=400, detail="invalid code")
45+@router.get("/reset")
46+async def reset_password_page(request: Request, token: str):
47+ access_log.info("reset_page hit url=%s", str(request.url))
48+ # token stays in the query for the form POST; analytics on page for funnel
49+ return HTMLResponse(RESET_FORM_HTML.format(token=token))
4750
4851@router.post("/reset/confirm")
4952async def confirm_reset(token: str, new_password: str):
5053 member = consume_reset_token(token)