"""JWT authentication and password hashing service.""" from __future__ import annotations from datetime import datetime, timedelta, timezone from jose import JWTError, jwt from passlib.context import CryptContext from fastapi import HTTPException, status, Depends from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from app.config import get_settings pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") security = HTTPBearer() def hash_password(password: str) -> str: """Hash a plain-text password using bcrypt.""" return pwd_context.hash(password) def verify_password(plain: str, hashed: str) -> bool: """Verify a plain-text password against its hash.""" return pwd_context.verify(plain, hashed) def create_access_token(user_id: str, email: str) -> str: """Create a JWT access token.""" settings = get_settings() expire = datetime.now(timezone.utc) + timedelta(hours=settings.jwt_expiry_hours) payload = { "sub": user_id, "email": email, "exp": expire, "iat": datetime.now(timezone.utc), } return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) def decode_token(token: str) -> dict: """Decode and validate a JWT token.""" settings = get_settings() try: payload = jwt.decode( token, settings.jwt_secret, algorithms=[settings.jwt_algorithm] ) return payload except JWTError as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid or expired token: {str(e)}", headers={"WWW-Authenticate": "Bearer"}, ) def get_current_user_id( credentials: HTTPAuthorizationCredentials = Depends(security), ) -> str: """FastAPI dependency — extract user_id from the Authorization header.""" payload = decode_token(credentials.credentials) user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token missing user identifier.", ) return user_id def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), ) -> dict: """FastAPI dependency — extract user_id and email from the JWT.""" payload = decode_token(credentials.credentials) user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token missing user identifier.", ) return {"id": user_id, "email": payload.get("email", "")}