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)