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