Files
WealthWise/backend/app/schemas/token.py

127 lines
5.1 KiB
Python

"""Pydantic models for JWT token handling and user context.
This module defines the data structures for:
- TokenPayload: Full JWT payload from Supabase
- UserContext: Clean user representation for API context
"""
from typing import Any, Optional
from pydantic import BaseModel, Field
class TokenPayload(BaseModel):
"""Supabase JWT token payload structure.
This model represents the complete JWT payload that Supabase Auth
includes in the access token. It contains all claims including
standard JWT claims and Supabase-specific claims.
Attributes:
sub: Subject (user UUID)
exp: Expiration timestamp (Unix)
iat: Issued at timestamp (Unix)
aud: Audience (should be "authenticated")
email: User's email address
phone: User's phone number (if available)
app_metadata: Application-specific metadata (includes role)
user_metadata: User-specific metadata
role: User's database role (e.g., "authenticated", "anon")
aal: Authenticator assurance level (aal1, aal2)
amr: Authentication methods reference
session_id: Session UUID
is_anonymous: Whether this is an anonymous user
"""
# Standard JWT claims
sub: str = Field(description="Subject (user UUID)")
exp: Optional[int] = Field(default=None, description="Expiration timestamp (Unix)")
iat: Optional[int] = Field(default=None, description="Issued at timestamp (Unix)")
iss: Optional[str] = Field(default=None, description="Issuer")
aud: str = Field(description="Audience (should be 'authenticated')")
# User identity claims
email: Optional[str] = Field(default=None, description="User's email address")
phone: Optional[str] = Field(default=None, description="User's phone number")
email_confirmed_at: Optional[str] = Field(default=None, description="Email confirmation timestamp")
phone_confirmed_at: Optional[str] = Field(default=None, description="Phone confirmation timestamp")
# Supabase metadata
app_metadata: dict[str, Any] = Field(
default_factory=dict,
description="Application-specific metadata (provider, role, etc.)"
)
user_metadata: dict[str, Any] = Field(
default_factory=dict,
description="User-specific metadata"
)
# Supabase-specific claims
role: Optional[str] = Field(default=None, description="Database role")
aal: Optional[str] = Field(default=None, description="Authenticator assurance level")
amr: Optional[list[dict[str, Any]]] = Field(default=None, description="Authentication methods reference")
session_id: Optional[str] = Field(default=None, description="Session UUID")
is_anonymous: Optional[bool] = Field(default=False, description="Whether user is anonymous")
class Config:
"""Pydantic configuration."""
extra = "allow" # Allow additional claims not explicitly defined
class UserContext(BaseModel):
"""Clean user context for API endpoints.
This model represents the authenticated user information that
is passed to API endpoints via the get_current_user dependency.
It contains only the essential information needed by most endpoints.
Attributes:
id: User UUID from the 'sub' claim
email: User's email address
role: User's role (extracted from app_metadata or role claim)
is_anonymous: Whether this is an anonymous user
session_id: Current session ID
"""
id: str = Field(description="User UUID")
email: Optional[str] = Field(default=None, description="User's email address")
role: str = Field(default="authenticated", description="User's role")
is_anonymous: bool = Field(default=False, description="Whether user is anonymous")
session_id: Optional[str] = Field(default=None, description="Current session ID")
@classmethod
def from_token_payload(cls, payload: TokenPayload) -> "UserContext":
"""Create UserContext from TokenPayload.
Extracts user information from the full JWT payload and creates
a clean UserContext object. Role is extracted from app_metadata
if available, otherwise defaults to the role claim or 'authenticated'.
Args:
payload: Validated TokenPayload from JWT
Returns:
UserContext with extracted user information
"""
# Extract role from app_metadata or fall back to role claim
role = payload.app_metadata.get("role", payload.role or "authenticated")
return cls(
id=payload.sub,
email=payload.email,
role=role,
is_anonymous=payload.is_anonymous or False,
session_id=payload.session_id,
)
class Config:
"""Pydantic configuration."""
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@wealthwise.app",
"role": "authenticated",
"is_anonymous": False,
"session_id": "session-uuid-here",
}
}