- Removed 11 duplicate router files (auth.py, users.py, decks.py, card_router.py, interactions.py, user_data.py, rooms.py, games/router.py, refresh.py, card_import.py, ws.py) - Removed duplicate core files (logging_config.py, settings.py) - Removed unused services (card_mirror_service.py, mtgjson_loader.py, mtgjson_uploader.py) - Removed unused schemas (proto_messages.py, protocol_constants.py) - Created run.py startup script for simple execution - Created app/core/config.py for unified configuration management - Created app/core/logging.py for logging configuration - Consolidated routers into 6 focused files (auth_router.py, users_router.py, decks_router.py, cards_router.py, user_data_router.py, games_router.py) - Updated all imports to use consolidated model paths - Added backward compatibility aliases in database.py - Created SIMPLIFICATION_SUMMARY.md documentation Architecture improvements: - Single unified database engine with auto-detection (SQLite/PostgreSQL) - Consolidated models into 2 files (models.py, mtg_models.py) - Clean router structure with one file per domain - Simplified configuration with sensible defaults - SQLite by default for development, PostgreSQL for production Testing: - All imports verified working - App loads successfully - Ready for development and testing
239 lines
6.4 KiB
Python
239 lines
6.4 KiB
Python
"""
|
|
Security utilities for authentication and password hashing.
|
|
|
|
Implements JWT token management and bcrypt password hashing with salt.
|
|
Provides functions for creating, validating, and decoding JWT tokens,
|
|
as well as password verification and hashing.
|
|
|
|
## Features
|
|
|
|
- Bcrypt password hashing with configurable rounds
|
|
- JWT access and refresh token creation
|
|
- Token decoding and validation
|
|
- FastAPI dependency for extracting current user from Authorization header
|
|
|
|
## Usage
|
|
|
|
from app.core.security import (
|
|
verify_password,
|
|
hash_password,
|
|
create_access_token,
|
|
create_refresh_token,
|
|
decode_token,
|
|
get_current_user,
|
|
)
|
|
|
|
# Hash and verify passwords
|
|
hashed = hash_password("secure_password")
|
|
is_valid = verify_password("secure_password", hashed)
|
|
|
|
# Create and decode tokens
|
|
token = create_access_token(subject="user_id", privlevel="Admin")
|
|
payload = decode_token(token)
|
|
|
|
# FastAPI dependency
|
|
@app.get("/protected")
|
|
async def protected_route(user: dict = Depends(get_current_user)):
|
|
...
|
|
"""
|
|
|
|
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.config import get_config
|
|
|
|
config = get_config()
|
|
|
|
# Password hashing context with bcrypt
|
|
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.
|
|
|
|
Args:
|
|
plain_password: The plain text password to verify.
|
|
hashed_password: The bcrypt hash to verify against.
|
|
|
|
Returns:
|
|
True if the password matches the hash, False otherwise.
|
|
|
|
Example:
|
|
>>> hashed = hash_password("my_password")
|
|
>>> verify_password("my_password", hashed)
|
|
True
|
|
>>> verify_password("wrong_password", hashed)
|
|
False
|
|
"""
|
|
return pwd_context.verify(plain_password, hashed_password)
|
|
|
|
|
|
def hash_password(password: str) -> str:
|
|
"""Hash a password using bcrypt with configurable rounds.
|
|
|
|
Args:
|
|
password: The plain text password to hash.
|
|
|
|
Returns:
|
|
The bcrypt hash string.
|
|
|
|
Example:
|
|
>>> hashed = hash_password("secure_password")
|
|
>>> len(hashed) > 0
|
|
True
|
|
"""
|
|
return pwd_context.hash(password, rounds=config.BCRYPT_ROUNDS)
|
|
|
|
|
|
def create_access_token(
|
|
subject: str,
|
|
privlevel: str = "User",
|
|
expires_delta: Optional[timedelta] = None,
|
|
) -> str:
|
|
"""Create a JWT access token.
|
|
|
|
Args:
|
|
subject: The user identifier (typically user ID or username).
|
|
privlevel: User privilege level (default "User").
|
|
expires_delta: Optional custom expiration time delta.
|
|
If not provided, uses JWT_ACCESS_TOKEN_EXPIRE_MINUTES from config.
|
|
|
|
Returns:
|
|
Encoded JWT access token string.
|
|
|
|
Example:
|
|
>>> token = create_access_token(subject="user123", privlevel="Admin")
|
|
>>> isinstance(token, str)
|
|
True
|
|
"""
|
|
if expires_delta:
|
|
expire = datetime.now(timezone.utc) + expires_delta
|
|
else:
|
|
expire = datetime.now(timezone.utc) + timedelta(
|
|
minutes=config.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
|
)
|
|
|
|
secret = config.jwt_secret
|
|
|
|
payload = {
|
|
"sub": subject,
|
|
"exp": expire,
|
|
"iat": datetime.now(timezone.utc),
|
|
"type": "access",
|
|
"privlevel": privlevel,
|
|
}
|
|
return jwt.encode(payload, secret, algorithm=config.JWT_ALGORITHM)
|
|
|
|
|
|
def create_refresh_token(subject: str, privlevel: str = "User") -> str:
|
|
"""Create a JWT refresh token with longer expiry.
|
|
|
|
Args:
|
|
subject: The user identifier (typically user ID or username).
|
|
privlevel: User privilege level (default "User").
|
|
|
|
Returns:
|
|
Encoded JWT refresh token string.
|
|
|
|
Example:
|
|
>>> token = create_refresh_token(subject="user123")
|
|
>>> isinstance(token, str)
|
|
True
|
|
"""
|
|
expire = datetime.now(timezone.utc) + timedelta(
|
|
days=config.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
|
)
|
|
|
|
secret = config.jwt_secret
|
|
|
|
payload = {
|
|
"sub": subject,
|
|
"exp": expire,
|
|
"iat": datetime.now(timezone.utc),
|
|
"type": "refresh",
|
|
"privlevel": privlevel,
|
|
}
|
|
return jwt.encode(payload, secret, algorithm=config.JWT_ALGORITHM)
|
|
|
|
|
|
def decode_token(token: str) -> Optional[dict]:
|
|
"""Decode and validate a JWT token.
|
|
|
|
Args:
|
|
token: The JWT token string to decode.
|
|
|
|
Returns:
|
|
The token payload dictionary if valid, None if invalid or expired.
|
|
|
|
Example:
|
|
>>> token = create_access_token(subject="user123")
|
|
>>> payload = decode_token(token)
|
|
>>> payload["sub"]
|
|
'user123'
|
|
"""
|
|
try:
|
|
secret = config.jwt_secret
|
|
|
|
payload = jwt.decode(
|
|
token,
|
|
secret,
|
|
algorithms=[config.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.
|
|
|
|
FastAPI dependency that validates the Bearer token and extracts
|
|
user information from the decoded payload.
|
|
|
|
Args:
|
|
authorization: The Authorization header value (Bearer <token>).
|
|
|
|
Returns:
|
|
Dictionary containing user_id, privlevel, and token_type.
|
|
|
|
Raises:
|
|
HTTPException: 401 if token is invalid, missing, or wrong type.
|
|
|
|
Example:
|
|
>>> @app.get("/protected")
|
|
... async def protected(user: dict = Depends(get_current_user)):
|
|
... return {"user_id": user["user_id"]}
|
|
"""
|
|
# 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"),
|
|
}
|