feat: add auth system, boss pokemon details, moves/abilities API, and run ownership
Some checks failed
CI / backend-tests (push) Failing after 1m16s
CI / frontend-tests (push) Successful in 57s

Add user authentication with login/signup/protected routes, boss pokemon
detail fields and result team tracking, moves and abilities selector
components and API, run ownership and visibility controls, and various
UI improvements across encounters, run list, and journal pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 21:41:38 +01:00
parent a6cb309b8b
commit 0a519e356e
69 changed files with 3574 additions and 693 deletions

View File

@@ -0,0 +1,83 @@
from dataclasses import dataclass
import jwt
from fastapi import Depends, HTTPException, Request, status
from app.core.config import settings
@dataclass
class AuthUser:
"""Authenticated user info extracted from JWT."""
id: str # Supabase user UUID
email: str | None = None
role: str | None = None
def _extract_token(request: Request) -> str | None:
"""Extract Bearer token from Authorization header."""
auth_header = request.headers.get("Authorization")
if not auth_header:
return None
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != "bearer":
return None
return parts[1]
def _verify_jwt(token: str) -> dict | None:
"""Verify JWT against Supabase JWT secret. Returns payload or None."""
if not settings.supabase_jwt_secret:
return None
try:
payload = jwt.decode(
token,
settings.supabase_jwt_secret,
algorithms=["HS256"],
audience="authenticated",
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def get_current_user(request: Request) -> AuthUser | None:
"""
Extract and verify the current user from the request.
Returns AuthUser if valid token, None otherwise.
"""
token = _extract_token(request)
if not token:
return None
payload = _verify_jwt(token)
if not payload:
return None
# Supabase JWT has 'sub' as user ID
user_id = payload.get("sub")
if not user_id:
return None
return AuthUser(
id=user_id,
email=payload.get("email"),
role=payload.get("role"),
)
def require_auth(user: AuthUser | None = Depends(get_current_user)) -> AuthUser:
"""
Dependency that requires authentication.
Raises 401 if no valid token is present.
"""
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"},
)
return user

View File

@@ -17,5 +17,10 @@ class Settings(BaseSettings):
# Database settings
database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/nuzlocke"
# Supabase Auth
supabase_url: str | None = None
supabase_anon_key: str | None = None
supabase_jwt_secret: str | None = None
settings = Settings()