- 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.
163 lines
5.0 KiB
Python
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)
|