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 (absolute path to ensure consistency) UPLOAD_DIR = Path("/app/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 # Ensure directory exists (may be missing after volume recreate) UPLOAD_DIR.mkdir(parents=True, exist_ok=True) 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