Files
akadmin 900f51af3a Complete architecture simplification: 54+ files → 37 files
- 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
2026-08-26 15:15:36 +00:00

246 lines
7.3 KiB
Python

"""
Authentication Router
This module provides the authentication router for the MTG Online application.
It handles user authentication, registration, and session management through
JWT (JSON Web Token) based authentication.
The router provides the following endpoints:
- POST /login: Authenticate user credentials and return JWT tokens
- POST /refresh: Refresh an expired access token using a refresh token
- POST /register: Register a new user account
- GET /me: Retrieve the current authenticated user's profile
All endpoints use async SQLAlchemy for database operations and FastAPI
dependencies for database session management.
"""
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.security import (
create_access_token,
create_refresh_token,
decode_token,
hash_password,
verify_password,
)
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),
) -> LoginResponse:
"""Authenticate user and return JWT tokens.
Validates the provided username and password against the database.
Checks if the account is active and not banned before issuing tokens.
Updates the user's last login timestamp on successful authentication.
Args:
request: The login request containing username and password.
db: The async database session.
Returns:
LoginResponse containing access_token, refresh_token, and user data.
Raises:
HTTPException 401: Invalid username or password.
HTTPException 403: Account is disabled or currently banned.
"""
# 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),
) -> TokenResponse:
"""Refresh access token using refresh token.
Validates the provided refresh token and issues a new access token
if the token is valid and the associated user account is still active.
Args:
request: The refresh token request.
db: The async database session.
Returns:
TokenResponse containing a new access_token.
Raises:
HTTPException 401: Invalid refresh token or inactive user account.
"""
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),
) -> UserResponse:
"""Register a new user account.
Creates a new user in the database after checking for duplicate
username and email addresses. The password is hashed before storage.
Args:
request: The user registration data.
db: The async database session.
Returns:
UserResponse containing the newly created user data.
Raises:
HTTPException 409: Username or email already exists.
"""
# 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),
) -> UserResponse:
"""Get current authenticated user.
Decodes the provided JWT access token and retrieves the associated
user profile from the database.
Args:
token: The JWT access token.
db: The async database session.
Returns:
UserResponse containing the current user's profile data.
Raises:
HTTPException 401: Invalid or expired token.
HTTPException 404: User not found in database.
"""
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)