from datetime import datetime, timedelta, timezone from typing import Optional from jose import JWTError, jwt from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from app.core.config import settings # Use bcrypt directly to avoid passlib/bcrypt version incompatibility import bcrypt as _bcrypt security_scheme = HTTPBearer() def hash_password(password: str) -> str: return _bcrypt.hashpw(password.encode("utf-8"), _bcrypt.gensalt()).decode("utf-8") def verify_password(plain_password: str, hashed_password: str) -> bool: try: return _bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8")) except Exception: return False def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) ) to_encode.update({"exp": expire, "type": "access"}) return jwt.encode(to_encode, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) def create_refresh_token(data: dict) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) to_encode.update({"exp": expire, "type": "refresh"}) return jwt.encode(to_encode, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) def decode_token(token: str) -> dict: try: payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM]) return payload except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token", headers={"WWW-Authenticate": "Bearer"}, ) async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security_scheme), ): payload = decode_token(credentials.credentials) if payload.get("type") != "access": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type", ) user_id = payload.get("sub") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload", ) return int(user_id) async def get_optional_current_user( credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)), ) -> Optional[int]: if credentials is None: return None try: payload = decode_token(credentials.credentials) if payload.get("type") != "access": return None user_id = payload.get("sub") if user_id is None: return None return int(user_id) except Exception: return None async def get_current_user_sse( token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer(auto_error=False)), ) -> int: """ Special version of get_current_user that also checks query parameters for the token. Required for EventSource (SSE) which doesn't support custom headers easily. """ actual_token = token if not actual_token and credentials: actual_token = credentials.credentials if not actual_token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated", ) payload = decode_token(actual_token) user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload", ) return int(user_id)