- 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
246 lines
7.3 KiB
Python
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) |