Files
mtgonline/backend/app/core/security.py
T
akadmin 915242b330 feat: MTG database integration with Redis caching
- Added MTG card ORM models (mtg_cards, mtg_sets tables)
- Created card_database service with search, get_by_name, get_by_set
- Added Redis client with caching layer (3600s TTL default)
- Created card router with caching on all endpoints:
  - Search cards (5min cache)
  - Get card by name (10min cache)
  - Get cards by set (15min cache)
  - Get card types/rarities (30min cache)
  - Get sets (1hr cache)
  - Get statistics (1hr cache)
- Updated settings.py:
  - Added JWT_SECRET_KEY field
  - Added DB_CONFIG and REDIS_CONFIG dictionaries
- Updated security.py to use JWT_SECRET_KEY with fallback
- Updated auth.py to use timezone-aware datetimes
- Updated refresh_mtg.py to use settings instead of os.environ
- Updated mtg_monitor.py to use settings for connections
- Added services package with __init__.py

All 20 tests passing.
2026-07-18 18:41:05 +00:00

118 lines
3.6 KiB
Python

"""
Security utilities for authentication and password hashing.
Implements JWT token management and bcrypt password hashing with salt.
"""
from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi import Header, HTTPException, status
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.settings import get_settings
settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a plain password against a bcrypt hash."""
return pwd_context.verify(plain_password, hashed_password)
def hash_password(password: str) -> str:
"""Hash a password using bcrypt with configurable rounds."""
return pwd_context.hash(password, rounds=settings.BCRYPT_ROUNDS)
def create_access_token(
subject: str,
privlevel: str = "User",
expires_delta: Optional[timedelta] = None,
) -> str:
"""Create a JWT access token."""
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
)
# Use JWT_SECRET_KEY if available, fall back to SECRET_KEY
secret = getattr(settings, 'JWT_SECRET_KEY', None) or settings.SECRET_KEY
payload = {
"sub": subject,
"exp": expire,
"iat": datetime.now(timezone.utc),
"type": "access",
"privlevel": privlevel,
}
return jwt.encode(payload, secret, algorithm=settings.JWT_ALGORITHM)
def create_refresh_token(subject: str, privlevel: str = "User") -> str:
"""Create a JWT refresh token with longer expiry."""
expire = datetime.now(timezone.utc) + timedelta(
days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
)
# Use JWT_SECRET_KEY if available, fall back to SECRET_KEY
secret = getattr(settings, 'JWT_SECRET_KEY', None) or settings.SECRET_KEY
payload = {
"sub": subject,
"exp": expire,
"iat": datetime.now(timezone.utc),
"type": "refresh",
"privlevel": privlevel,
}
return jwt.encode(payload, secret, algorithm=settings.JWT_ALGORITHM)
def decode_token(token: str) -> Optional[dict]:
"""Decode and validate a JWT token."""
try:
# Use JWT_SECRET_KEY if available, fall back to SECRET_KEY
secret = getattr(settings, 'JWT_SECRET_KEY', None) or settings.SECRET_KEY
payload = jwt.decode(
token,
secret,
algorithms=[settings.JWT_ALGORITHM],
)
return payload
except JWTError:
return None
def get_current_user(authorization: str = Header(...)) -> dict:
"""Extract user info from JWT token in Authorization header."""
# Parse Bearer token
parts = authorization.split()
if len(parts) != 2 or parts[0].lower() != 'bearer':
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication scheme"
)
token = parts[1]
payload = decode_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
if payload.get("type") != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type"
)
return {
"user_id": payload.get("sub"),
"privlevel": payload.get("privlevel", "User"),
"token_type": payload.get("type"),
}