Files
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

163 lines
5.0 KiB
Python

"""Authentication router endpoints."""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional
from datetime import datetime, timezone, timezone
from app.core.database import get_db
from app.core.security import (
verify_password,
hash_password,
create_access_token,
create_refresh_token,
decode_token,
)
from app.models.models import User
from app.schemas.schemas import (
LoginRequest,
LoginResponse,
RefreshTokenRequest,
TokenResponse,
UserCreate,
UserResponse,
)
router = APIRouter()
@router.post("/login", response_model=LoginResponse)
async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
"""Authenticate user and return JWT tokens."""
# Find user by username
stmt = select(User).where(User.username == request.username)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user or not verify_password(request.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is disabled",
)
if user.is_banned and user.ban_ends and user.ban_ends > datetime.now():
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account is banned",
)
# Update last login
user.last_login = datetime.now(timezone.utc)
await db.flush()
# Generate tokens
access_token = create_access_token(str(user.id), user.privlevel or "User")
refresh_token = create_refresh_token(str(user.id), user.privlevel or "User")
return LoginResponse(
access_token=access_token,
refresh_token=refresh_token,
user=UserResponse.model_validate(user).model_dump(),
)
@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(request: RefreshTokenRequest, db: AsyncSession = Depends(get_db)):
"""Refresh access token using refresh token."""
payload = decode_token(request.refresh_token)
if not payload or payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
)
# Verify user still exists and is active
user_id = payload["sub"]
stmt = select(User).where(User.id == int(user_id))
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User account is invalid",
)
# Generate new access token
access_token = create_access_token(str(user.id), user.privlevel or "User")
return TokenResponse(access_token=access_token)
@router.post("/register", response_model=UserResponse)
async def register(request: UserCreate, db: AsyncSession = Depends(get_db)):
"""Register a new user account."""
# Check if username exists
stmt = select(User).where(User.username == request.username)
result = await db.execute(stmt)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username already exists",
)
# Check if email exists (if provided)
if request.email:
stmt = select(User).where(User.email == request.email)
result = await db.execute(stmt)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Email already registered",
)
# Create new user
new_user = User(
username=request.username,
password_hash=hash_password(request.password),
salt="random_salt", # In production, generate random salt
email=request.email,
country=request.country,
real_name=request.real_name,
)
db.add(new_user)
await db.flush()
return UserResponse.model_validate(new_user)
@router.get("/me", response_model=UserResponse)
async def get_current_user(
token: str,
db: AsyncSession = Depends(get_db),
):
"""Get current authenticated user."""
payload = decode_token(token)
if not payload or payload.get("type") != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token",
)
user_id = payload["sub"]
stmt = select(User).where(User.id == int(user_id))
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return UserResponse.model_validate(user)