""" app/core/security.py ───────────────────── JWT creation/verification and password hashing helpers. """ import hashlib from datetime import datetime, timedelta, timezone from typing import Any, Optional from jose import JWTError, jwt from passlib.context import CryptContext from app.core.config import settings # ── Password hashing ───────────────────────────────────────────────────────── # Default to pbkdf2_sha256 to avoid bcrypt backend issues on some Python builds. # Keep bcrypt for backward compatibility with already-created hashes. _pwd_context = CryptContext( schemes=["pbkdf2_sha256", "bcrypt"], deprecated="auto", ) def hash_password(plain: str) -> str: """Return secure hash of a plain-text password.""" return _pwd_context.hash(plain) def verify_password(plain: str, hashed: str) -> bool: """Return True if *plain* matches *hashed*.""" try: return _pwd_context.verify(plain, hashed) except (ValueError, RuntimeError): # If legacy bcrypt backend raises unexpectedly, fail safely. return False # ── JWT ────────────────────────────────────────────────────────────────────── def create_access_token(subject: int | str, expires_delta: Optional[timedelta] = None) -> str: """ Create a signed JWT token. Args: subject: Typically the user id stored in the `sub` claim. expires_delta: Override the default expiry window. Returns: Encoded JWT string. """ expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) ) payload = {"sub": str(subject), "type": "access", "exp": expire} return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) def create_refresh_token(subject: int | str, expires_delta: Optional[timedelta] = None) -> str: expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) ) payload = {"sub": str(subject), "type": "refresh", "exp": expire} return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM) def decode_token(token: str) -> Optional[dict[str, Any]]: try: return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) except JWTError: return None def decode_access_token(token: str) -> Optional[str]: """ Decode and verify a JWT token. Returns: The `sub` claim (user id as string) on success, None on failure. """ try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) if payload.get("type") != "access": return None return payload.get("sub") except JWTError: return None def hash_token(raw_token: str) -> str: return hashlib.sha256(raw_token.encode("utf-8")).hexdigest()