import base64 from datetime import datetime, timedelta from typing import Optional from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from passlib.context import CryptContext from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from cryptography.fernet import Fernet from app.config import settings from app.database.postgres import get_db, User # OAuth2 scheme oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") # Derive symmetric encryption key from JWT Secret def get_fernet() -> Fernet: secret_bytes = settings.JWT_SECRET.encode() # Fernet key needs to be 32 bytes and urlsafe-base64 encoded derived_key = base64.urlsafe_b64encode(secret_bytes[:32].ljust(32, b'-')) return Fernet(derived_key) def encrypt_token(token: str) -> str: """Encrypt a token before database storage.""" if not token: return "" fernet = get_fernet() return fernet.encrypt(token.encode()).decode() def decrypt_token(encrypted_token: str) -> str: """Decrypt a token retrieved from database.""" if not encrypted_token: return "" try: fernet = get_fernet() return fernet.decrypt(encrypted_token.encode()).decode() except Exception: return "" import bcrypt 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 get_password_hash(password: str) -> str: salt = bcrypt.gensalt() return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8') def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) return encoded_jwt async def get_current_user( token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db) ) -> User: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM]) user_id_str: str = payload.get("sub") if user_id_str is None: raise credentials_exception except JWTError: raise credentials_exception # Query Postgres for the user result = await db.execute(select(User).where(User.id == user_id_str)) user = result.scalars().first() if user is None: raise credentials_exception return user