|
1 |
| -from fastapi import APIRouter, Depends, Form, UploadFile, File |
| 1 | +from fastapi import APIRouter, Depends, Form, UploadFile, File, HTTPException |
2 | 2 | from fastapi.responses import RedirectResponse, Response
|
3 | 3 | from pydantic import BaseModel, EmailStr
|
4 | 4 | from sqlmodel import Session
|
5 | 5 | from typing import Optional
|
6 | 6 | from utils.models import User, DataIntegrityError
|
7 | 7 | from utils.auth import get_session, get_authenticated_user, verify_password, PasswordValidationError
|
| 8 | +from PIL import Image |
| 9 | +import io |
8 | 10 |
|
9 | 11 | router = APIRouter(prefix="/user", tags=["user"])
|
10 | 12 |
|
11 | 13 |
|
| 14 | +# --- Constants --- |
| 15 | + |
| 16 | + |
| 17 | +# 2MB in bytes |
| 18 | +MAX_FILE_SIZE = 2 * 1024 * 1024 |
| 19 | +ALLOWED_CONTENT_TYPES = { |
| 20 | + 'image/jpeg', |
| 21 | + 'image/png', |
| 22 | + 'image/webp' |
| 23 | +} |
| 24 | +MIN_DIMENSION = 100 |
| 25 | +MAX_DIMENSION = 2000 |
| 26 | +TARGET_SIZE = 500 |
| 27 | + |
| 28 | + |
| 29 | +# --- Custom Exceptions --- |
| 30 | + |
| 31 | + |
| 32 | +class InvalidImageError(HTTPException): |
| 33 | + """Raised when an invalid image is uploaded""" |
| 34 | + |
| 35 | + def __init__(self, message: str = "Invalid image file"): |
| 36 | + super().__init__(status_code=400, detail=message) |
| 37 | + |
| 38 | + |
12 | 39 | # --- Server Request and Response Models ---
|
13 | 40 |
|
14 | 41 |
|
@@ -61,11 +88,62 @@ async def update_profile(
|
61 | 88 | user: User = Depends(get_authenticated_user),
|
62 | 89 | session: Session = Depends(get_session)
|
63 | 90 | ):
|
| 91 | + # Handle avatar update |
| 92 | + if user_profile.avatar_file: |
| 93 | + # Check file size |
| 94 | + if len(user_profile.avatar_file) > MAX_FILE_SIZE: |
| 95 | + raise InvalidImageError( |
| 96 | + message="File too large (max 2MB)" |
| 97 | + ) |
| 98 | + |
| 99 | + # Check file type |
| 100 | + if user_profile.avatar_content_type not in ALLOWED_CONTENT_TYPES: |
| 101 | + raise InvalidImageError( |
| 102 | + message="Invalid file type. Must be JPEG, PNG, or WebP" |
| 103 | + ) |
| 104 | + |
| 105 | + try: |
| 106 | + # Open and validate image |
| 107 | + image = Image.open(io.BytesIO(user_profile.avatar_file)) |
| 108 | + width, height = image.size |
| 109 | + |
| 110 | + # Check minimum dimensions |
| 111 | + if width < MIN_DIMENSION or height < MIN_DIMENSION: |
| 112 | + raise InvalidImageError( |
| 113 | + message=f"Image too small. Minimum dimension is {MIN_DIMENSION}px" |
| 114 | + ) |
| 115 | + |
| 116 | + # Check maximum dimensions |
| 117 | + if width > MAX_DIMENSION or height > MAX_DIMENSION: |
| 118 | + raise InvalidImageError( |
| 119 | + message=f"Image too large. Maximum dimension is {MAX_DIMENSION}px" |
| 120 | + ) |
| 121 | + |
| 122 | + # Crop to square and resize |
| 123 | + min_dim = min(width, height) |
| 124 | + left = (width - min_dim) // 2 |
| 125 | + top = (height - min_dim) // 2 |
| 126 | + right = left + min_dim |
| 127 | + bottom = top + min_dim |
| 128 | + |
| 129 | + image = image.crop((left, top, right, bottom)) |
| 130 | + image = image.resize((TARGET_SIZE, TARGET_SIZE), Image.Resampling.LANCZOS) |
| 131 | + |
| 132 | + # Convert back to bytes |
| 133 | + output = io.BytesIO() |
| 134 | + image.save(output, format='PNG') |
| 135 | + user_profile.avatar_file = output.getvalue() |
| 136 | + user_profile.avatar_content_type = 'image/png' |
| 137 | + |
| 138 | + except Exception as e: |
| 139 | + raise InvalidImageError( |
| 140 | + message="Invalid image file" |
| 141 | + ) |
| 142 | + |
64 | 143 | # Update user details
|
65 | 144 | user.name = user_profile.name
|
66 | 145 | user.email = user_profile.email
|
67 |
| - |
68 |
| - # Handle avatar update |
| 146 | + |
69 | 147 | if user_profile.avatar_file:
|
70 | 148 | user.avatar_data = user_profile.avatar_file
|
71 | 149 | user.avatar_content_type = user_profile.avatar_content_type
|
|
0 commit comments