volsu-contests/backend/app/routers/auth.py
2025-11-30 19:55:50 +03:00

180 lines
5.2 KiB
Python

import os
import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate, UserResponse, Token
from app.services.auth import get_password_hash, verify_password, create_access_token
from app.dependencies import get_current_user
# Avatar upload directory
UPLOAD_DIR = Path("uploads/avatars")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
router = APIRouter()
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserCreate,
db: AsyncSession = Depends(get_db),
):
# Check if email already exists
result = await db.execute(select(User).where(User.email == user_data.email))
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
# Create user
user = User(
email=user_data.email,
username=user_data.username,
password_hash=get_password_hash(user_data.password),
role="participant",
)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@router.post("/login", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db),
):
# Find user by email (username field contains email)
result = await db.execute(select(User).where(User.email == form_data.username))
user = result.scalar_one_or_none()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
)
access_token = create_access_token(data={"sub": str(user.id)})
return Token(access_token=access_token)
@router.get("/me", response_model=UserResponse)
async def get_me(
current_user: User = Depends(get_current_user),
):
return current_user
@router.put("/me", response_model=UserResponse)
async def update_profile(
user_data: UserUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
update_data = user_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(current_user, field, value)
await db.commit()
await db.refresh(current_user)
return current_user
@router.post("/me/avatar", response_model=UserResponse)
async def upload_avatar(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# Validate file extension
ext = Path(file.filename).suffix.lower() if file.filename else ""
if ext not in ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File type not allowed. Allowed types: {', '.join(ALLOWED_EXTENSIONS)}",
)
# Read file content
content = await file.read()
# Validate file size
if len(content) > MAX_FILE_SIZE:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File too large. Maximum size: {MAX_FILE_SIZE // 1024 // 1024}MB",
)
# Delete old avatar if exists
if current_user.avatar_url:
old_path = UPLOAD_DIR / current_user.avatar_url.split("/")[-1]
if old_path.exists():
old_path.unlink()
# Save new avatar
filename = f"{current_user.id}_{uuid.uuid4().hex}{ext}"
file_path = UPLOAD_DIR / filename
with open(file_path, "wb") as f:
f.write(content)
# Update user avatar URL
current_user.avatar_url = f"/uploads/avatars/{filename}"
await db.commit()
await db.refresh(current_user)
return current_user
@router.delete("/me/avatar", response_model=UserResponse)
async def delete_avatar(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
if current_user.avatar_url:
old_path = UPLOAD_DIR / current_user.avatar_url.split("/")[-1]
if old_path.exists():
old_path.unlink()
current_user.avatar_url = None
await db.commit()
await db.refresh(current_user)
return current_user
@router.get("/users", response_model=list[UserResponse])
async def get_all_users(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get all users (admin only)"""
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required",
)
result = await db.execute(select(User).order_by(User.created_at.desc()))
users = result.scalars().all()
return users