from __future__ import annotations import os from datetime import datetime, timedelta, timezone from fastapi import Depends, HTTPException from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt from passlib.context import CryptContext from sqlalchemy.orm import Session from backend.db import get_db from backend.models import User pwd_context = CryptContext(schemes=["argon2", "bcrypt"], deprecated="auto") bearer = HTTPBearer(auto_error=False) AUTH_VERSION = "argon2-bcrypt-compat-2026-04-07" def _env(name: str, default: str) -> str: value = os.getenv(name) return value if value else default JWT_SECRET = _env("JWT_SECRET", "change-me") JWT_ALG = _env("JWT_ALG", "HS256") JWT_EXPIRES_MINUTES = int(_env("JWT_EXPIRES_MINUTES", "480")) # 8h def hash_password(password: str) -> str: try: return pwd_context.hash(password) except Exception as e: orig_len = len(password.encode("utf-8")) raise RuntimeError( f"{str(e)} (orig_bytes_len={orig_len})" ) from e def verify_password(password: str, password_hash: str) -> bool: try: return pwd_context.verify(password, password_hash) except Exception as e: orig_len = len(password.encode("utf-8")) raise RuntimeError( f"{str(e)} (orig_bytes_len={orig_len})" ) from e def password_needs_update(password_hash: str) -> bool: return pwd_context.needs_update(password_hash) def create_access_token(*, user_id: str) -> str: now = datetime.now(timezone.utc) payload = { "sub": user_id, "iat": int(now.timestamp()), "exp": int((now + timedelta(minutes=JWT_EXPIRES_MINUTES)).timestamp()), } return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALG) def get_current_user( creds: HTTPAuthorizationCredentials | None = Depends(bearer), db: Session = Depends(get_db), ) -> User: if creds is None or not creds.credentials: raise HTTPException(status_code=401, detail="Not authenticated") token = creds.credentials try: payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALG]) user_id = payload.get("sub") if not user_id: raise HTTPException(status_code=401, detail="Invalid token") except JWTError: raise HTTPException(status_code=401, detail="Invalid token") user = db.get(User, user_id) if not user or not user.is_active: raise HTTPException(status_code=401, detail="User inactive or not found") return user