from __future__ import annotations import os import hashlib 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=["bcrypt"], deprecated="auto") bearer = HTTPBearer(auto_error=False) AUTH_VERSION = "prehash-sha256-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 _bcrypt_safe_password(password: str) -> str: """ bcrypt only uses first 72 bytes of the input. To support long passwords safely, pre-hash using SHA-256 when needed. """ raw = password.encode("utf-8") if len(raw) <= 72: return password return hashlib.sha256(raw).hexdigest() def hash_password(password: str) -> str: return pwd_context.hash(_bcrypt_safe_password(password)) def verify_password(password: str, password_hash: str) -> bool: return pwd_context.verify(_bcrypt_safe_password(password), 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