Spaces:
Paused
Paused
| """ | |
| 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() | |