Files
rehearshalhub/api/src/rehearsalhub/routers/auth.py
Mistral Vibe b20b98a17a fix: improve error handling for avatar uploads
- Change invalid file type error from 400 to 422 for better frontend handling
- Add specific error message for 422 responses in frontend
- Improve error message clarity
- Better error classification and user guidance

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:49:39 +02:00

137 lines
5.0 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
import os
import uuid
from rehearsalhub.db.engine import get_session
from rehearsalhub.db.models import Member
from rehearsalhub.dependencies import get_current_member
from rehearsalhub.repositories.member import MemberRepository
from rehearsalhub.schemas.auth import LoginRequest, RegisterRequest, TokenResponse
from rehearsalhub.schemas.member import MemberRead, MemberSettingsUpdate
from rehearsalhub.services.auth import AuthService
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=MemberRead, status_code=status.HTTP_201_CREATED)
async def register(req: RegisterRequest, session: AsyncSession = Depends(get_session)):
svc = AuthService(session)
try:
member = await svc.register(req)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
return MemberRead.from_model(member)
@router.post("/login", response_model=TokenResponse)
async def login(req: LoginRequest, session: AsyncSession = Depends(get_session)):
svc = AuthService(session)
token = await svc.login(req.email, req.password)
if token is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
)
return token
@router.get("/me", response_model=MemberRead)
async def get_me(current_member: Member = Depends(get_current_member)):
return MemberRead.from_model(current_member)
@router.patch("/me/settings", response_model=MemberRead)
async def update_settings(
data: MemberSettingsUpdate,
session: AsyncSession = Depends(get_session),
current_member: Member = Depends(get_current_member),
):
print(f"Update settings called for member {current_member.id}")
print(f"Update data: {data.model_dump()}")
repo = MemberRepository(session)
updates: dict = {}
if data.display_name is not None:
updates["display_name"] = data.display_name
if data.nc_url is not None:
updates["nc_url"] = data.nc_url.rstrip("/") if data.nc_url else None
if data.nc_username is not None:
updates["nc_username"] = data.nc_username or None
if data.nc_password is not None:
updates["nc_password"] = data.nc_password or None
if data.avatar_url is not None:
updates["avatar_url"] = data.avatar_url or None
print(f"Updates to apply: {updates}")
if updates:
member = await repo.update(current_member, **updates)
print("Settings updated successfully")
else:
member = current_member
print("No updates to apply")
return MemberRead.from_model(member)
@router.post("/me/avatar", response_model=MemberRead)
async def upload_avatar(
file: UploadFile = File(...),
session: AsyncSession = Depends(get_session),
current_member: Member = Depends(get_current_member),
):
"""Upload and set user avatar image."""
print(f"Avatar upload called for member {current_member.id}")
print(f"File: {file.filename}, Content-Type: {file.content_type}")
# Validate file type
if not file.content_type.startswith("image/"):
print("Invalid file type")
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Only image files are allowed (JPG, PNG, GIF, etc.)"
)
# Validate file size (5MB limit for upload endpoint)
max_size = 5 * 1024 * 1024 # 5MB
if file.size > max_size:
print(f"File too large: {file.size} bytes (max {max_size})")
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"File too large. Maximum size is {max_size / 1024 / 1024}MB. Please resize your image and try again."
)
# Create uploads directory if it doesn't exist
upload_dir = "uploads/avatars"
os.makedirs(upload_dir, exist_ok=True)
print(f"Using upload directory: {upload_dir}")
# Generate unique filename
file_ext = file.filename.split(".")[-1] if "." in file.filename else "jpg"
filename = f"{uuid.uuid4()}.{file_ext}"
file_path = f"{upload_dir}/{filename}"
print(f"Saving file to: {file_path}")
# Save file
try:
contents = await file.read()
print(f"File size: {len(contents)} bytes")
with open(file_path, "wb") as buffer:
buffer.write(contents)
print("File saved successfully")
except Exception as e:
print(f"Failed to save file: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to save avatar: {str(e)}"
)
# Update member's avatar URL
repo = MemberRepository(session)
avatar_url = f"/api/static/avatars/{filename}"
print(f"Setting avatar URL to: {avatar_url}")
member = await repo.update(current_member, avatar_url=avatar_url)
print("Avatar updated successfully")
return MemberRead.from_model(member)