feat(billing): add INTERNAL_TESTER_EMAILS allowlist for self-serve soft cutover
Phase O Task 46 needs internal validation of the full self-serve flow against the prod backend before flipping SELF_SERVE_ENABLED public. This adds the per-email allowlist that bypasses the global flag for specific authenticated users. - INTERNAL_TESTER_EMAILS: comma-separated list, parsed by a Pydantic field_validator into a normalized lowercase list. Settings.is_internal_tester and Settings.is_self_serve_active_for centralize the allowlist + global-flag check; both endpoints below call the latter. - New get_current_user_optional dep — best-effort auth that returns None on missing/invalid token instead of 401. Used by /config/public so the same endpoint serves anonymous public callers and authenticated allowlist members. - /config/public now accepts optional auth and returns self_serve_enabled=True for authenticated allowlist members even when the global flag is off. Anonymous callers always see the global flag. - /auth/register replaces the SELF_SERVE_ENABLED check with the helper so a registering email on the allowlist can join without an invite code. Non-allowlist emails still 400 when self-serve is off. - docker-compose.dev.yml passes SELF_SERVE_ENABLED + INTERNAL_TESTER_EMAILS through; backend/.env.example documents both. Tests cover: allowlisted authenticated user sees true, non-allowlisted authenticated user sees the global flag, anonymous calls ignore the allowlist, allowlisted email registers without invite code, non-allowlisted email still blocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,40 @@ async def get_current_user(
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_user_optional(
|
||||
request: Request,
|
||||
db: Annotated[AsyncSession, Depends(get_admin_db)],
|
||||
) -> Optional[User]:
|
||||
"""Best-effort current user for endpoints that work both anonymous and authed.
|
||||
|
||||
Returns None on missing/invalid/expired token instead of raising. Used by
|
||||
surfaces like /config/public that anonymous clients can hit but where an
|
||||
authenticated user gets a tailored response (e.g. INTERNAL_TESTER_EMAILS
|
||||
allowlist override).
|
||||
"""
|
||||
auth_header = request.headers.get("Authorization") or request.headers.get("authorization")
|
||||
if not auth_header or not auth_header.lower().startswith("bearer "):
|
||||
return None
|
||||
token = auth_header.split(None, 1)[1].strip()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
payload = decode_token(token)
|
||||
if payload is None or payload.get("type") != "access":
|
||||
return None
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if user_id is None:
|
||||
return None
|
||||
try:
|
||||
user_uuid = UUID(user_id)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
result = await db.execute(select(User).where(User.id == user_uuid))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_refresh_token_payload(
|
||||
token: Annotated[str, Depends(oauth2_scheme)]
|
||||
) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user