Skip to content

Commit a22d44c

Browse files
First pass at user profile image validation
1 parent 6e6b13b commit a22d44c

File tree

3 files changed

+84
-3
lines changed

3 files changed

+84
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies = [
2020
"resend<3.0.0,>=2.4.0",
2121
"bcrypt<5.0.0,>=4.2.0",
2222
"fastapi<1.0.0,>=0.115.5",
23+
"pillow>=11.0.0",
2324
]
2425

2526
[dependency-groups]

routers/user.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
1-
from fastapi import APIRouter, Depends, Form, UploadFile, File
1+
from fastapi import APIRouter, Depends, Form, UploadFile, File, HTTPException
22
from fastapi.responses import RedirectResponse, Response
33
from pydantic import BaseModel, EmailStr
44
from sqlmodel import Session
55
from typing import Optional
66
from utils.models import User, DataIntegrityError
77
from utils.auth import get_session, get_authenticated_user, verify_password, PasswordValidationError
8+
from PIL import Image
9+
import io
810

911
router = APIRouter(prefix="/user", tags=["user"])
1012

1113

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+
1239
# --- Server Request and Response Models ---
1340

1441

@@ -61,11 +88,62 @@ async def update_profile(
6188
user: User = Depends(get_authenticated_user),
6289
session: Session = Depends(get_session)
6390
):
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+
64143
# Update user details
65144
user.name = user_profile.name
66145
user.email = user_profile.email
67-
68-
# Handle avatar update
146+
69147
if user_profile.avatar_file:
70148
user.avatar_data = user_profile.avatar_file
71149
user.avatar_content_type = user_profile.avatar_content_type

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)