""" JWT Module — Access and refresh token creation, validation, and revocation. """ from datetime import datetime, timedelta, timezone from typing import Optional, Literal from jose import jwt, JWTError from fastapi import HTTPException, status from app.core.config import get_settings def create_access_token( user_id: str, email: str, roles: list[str] | None = None, extra_claims: dict | None = None, ) -> str: """ Create a short-lived access token (default 30 minutes). Contains user identity and role information. """ settings = get_settings() now = datetime.now(timezone.utc) expire = now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) payload = { "sub": user_id, "email": email, "roles": roles or [], "type": "access", "iat": now, "exp": expire, } if extra_claims: payload.update(extra_claims) return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) def create_refresh_token( user_id: str, session_id: str, family_id: str | None = None, ) -> tuple[str, datetime]: """ Create a long-lived refresh token (default 7 days). Returns (token_string, expires_at). """ settings = get_settings() now = datetime.now(timezone.utc) expire = now + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) payload = { "sub": user_id, "session_id": session_id, "family_id": family_id or session_id, "type": "refresh", "iat": now, "exp": expire, } token = jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) return token, expire def decode_token( token: str, expected_type: Literal["access", "refresh"] | None = None, ) -> dict: """ Decode and validate a JWT token. Raises HTTPException on invalid/expired tokens. """ settings = get_settings() try: payload = jwt.decode( token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM], ) except JWTError as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid or expired token: {str(e)}", headers={"WWW-Authenticate": "Bearer"}, ) # Validate token type if specified if expected_type and payload.get("type") != expected_type: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Expected {expected_type} token, got {payload.get('type')}", headers={"WWW-Authenticate": "Bearer"}, ) return payload def decode_token_no_exception(token: str) -> Optional[dict]: """Decode a token without raising exceptions. Returns None on failure.""" settings = get_settings() try: return jwt.decode( token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM], ) except JWTError: return None