feat: add dual-auth dependency for jersey and checkout routes (#255) #258
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/basketball-api!258
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "255-dual-auth-jersey-checkout"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add a reusable
get_parent_dual_authdependency that resolves a Parent using either a?token=registration token (email-link flow) or a Keycloak Bearer JWT with email lookup (SPA flow). This unblocks self-service jersey ordering from the authenticated SPA without breaking existing email-link flows.Changes
src/basketball_api/auth.py— Added_get_optional_user(non-erroring Bearer resolver) andget_parent_dual_authdependency that tries registration token first, falls back to Keycloak JWT email match. Eagerly loadsParent.playersviajoinedload.src/basketball_api/routes/jersey.py— Updatedjersey_player_infoandjersey_checkoutto useget_parent_dual_authinstead of inlineParent.registration_tokenlookup. Removed unusedjoinedloadimport.src/basketball_api/routes/checkout.py— Updatedcreate_checkout_sessionto useget_parent_dual_authinstead of inline token lookup. Removed unusedjoinedloadimport.tests/test_dual_auth.py— 13 new tests covering: valid token path, invalid token 404, valid JWT path, JWT email-no-match 404, no-auth 401, and integration tests for jersey and checkout routes with both auth methods.Test Plan
pytest tests/ -x -q— 702 tests pass (689 existing + 13 new)pytest tests/ -k "dual_auth or jersey or checkout"— all dual-auth, jersey, and checkout tests passregister.pyandtryouts.pyhave zero diff (confirmed viagit diff)ruff formatandruff checkcleanReview Checklist
register.pyandtryouts.pyuntouched (zero blast radius)Parent.playersrelationship viajoinedloadauth.pyRelated Notes
Related
forgejo_admin/westside-landing#196QA Review -- PR #258
Summary
Adds
get_parent_dual_authdependency inauth.pythat resolves a Parent via either registration token or Keycloak JWT email lookup. Updates jersey and checkout routes to use it. 13 new tests. 702 total pass.Findings
Architecture (PASS)
_get_optional_user+get_parent_dual_authproperly uses FastAPI DI chain._get_optional_useris testable viaapp.dependency_overrides, which the tests demonstrate.Parentmodel insideget_parent_dual_authavoids circular import betweenauth.pyandmodels.py. Acceptable pattern.Backwards Compatibility (PASS)
?token=query param remains optional (Query(None)), so existing email-link callers work unchanged.Blast Radius (PASS)
register.pyandtryouts.pyhave zero diff, confirmed.Test Coverage (PASS)
Code Quality (PASS)
joinedload(Parent.players)eager loading in the dependency satisfies the constraint.joinedloadimports from jersey.py and checkout.py -- clean.async defsince theyawaitthe dual-auth dependency.Nits (non-blocking)
VERDICT: APPROVED
PR #258 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Pydantic (with Stripe integration)
Auth logic (
get_parent_dual_auth):?token=is present, it takes precedence regardless of whether a Bearer token is also sent. This prevents ambiguity._get_optional_usercorrectly wrapsget_current_userand returnsNoneinstead of raising 401 when no Bearer token is present. The existing_bearer_scheme = HTTPBearer(auto_error=False)already returnsNonefor missing auth headers, so the chain is sound.from basketball_api.models import Parentinside the function body correctly avoids a circular import (auth.py does not import models at module level, and models.py does not import auth).ilikefor email matching follows the established codebase pattern (used identically incoaches_api.py:113,account.py:57,public.py:290).joinedload(Parent.players)ensures the eager load happens in both paths, matching what the removed inline code was doing.Route changes:
jersey_player_infocorrectly drops thedbparameter since all DB work is now in the dependency. Theparent.playersaccess works because of the eager load.jersey_checkoutandcreate_checkout_sessioncorrectly retaindb: Session = Depends(get_db)since they perform additional DB operations downstream.Depends(get_db)by callable identity within a single request, so thedbsession inget_parent_dual_authand in the route handler is the same instance. The Parent object returned from the dependency is usable in the route handler's session context.Sync-to-async change: All three route handlers changed from
deftoasync def. These handlers still do synchronous SQLAlchemy operations, which will now run on the event loop rather than in a threadpool. This matches the existing pattern in this codebase (submit_registration,check_email, etc. are alreadyasync defwith sync DB ops). Acceptable for this API's concurrency profile.Test coverage (
test_dual_auth.py):_make_jwt_clientoverriding_get_optional_user) correctly bypasses JWKS fetch while still exercising theget_parent_dual_authlogic.parent_with_player,_seed_tenant,opt_out_product) follows established test conventions fromconftest.py,test_jersey.py, andtest_checkout.py.finally: app.dependency_overrides.clear()in JWT tests prevents test pollution.Security:
==match (no injection risk).ilike(case-insensitive) which is the established pattern and appropriate for email addresses.BLOCKERS
None.
NITS
NIT: Missing return type annotation on
get_parent_dual_auth. The function returnsParentbut the signature isasync def get_parent_dual_auth(...):without-> Parent. Other dependencies inauth.py(get_current_user -> User,_get_optional_user -> User | None) have return type annotations. Adding-> "Parent"(string annotation to avoid the import at module level) would maintain consistency.NIT:
tokenparameter naming overlap. Theget_parent_dual_authdependency usestoken: str | None = Query(None, alias="token")where the parameter name and alias are the same. The alias is technically redundant here but does make the query param name explicit. No functional issue -- just noting for clarity.SOP COMPLIANCE
255-dual-auth-jersey-checkoutfollows{issue-number}-{kebab-case-purpose}conventionPROCESS OBSERVATIONS
player_idparam) rather than expanding this PR's scope.VERDICT: APPROVED