feat: implement client-side image resizing for avatar uploads

- Add resizeImage function to SettingsPage
- Resize images larger than 2MB to max 800x800 pixels
- Convert to JPEG with 80% quality to reduce file size
- Add server-side validation for 10MB file size limit
- Maintain aspect ratio during resizing
- Log resizing details for debugging

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mistral Vibe
2026-03-30 19:40:33 +02:00
parent 675399836c
commit a63a1571ba
2 changed files with 89 additions and 2 deletions

View File

@@ -83,7 +83,7 @@ async def upload_avatar(
print(f"Avatar upload called for member {current_member.id}") print(f"Avatar upload called for member {current_member.id}")
print(f"File: {file.filename}, Content-Type: {file.content_type}") print(f"File: {file.filename}, Content-Type: {file.content_type}")
# Validate file type and size # Validate file type
if not file.content_type.startswith("image/"): if not file.content_type.startswith("image/"):
print("Invalid file type") print("Invalid file type")
raise HTTPException( raise HTTPException(
@@ -91,6 +91,15 @@ async def upload_avatar(
detail="Only image files are allowed" detail="Only image files are allowed"
) )
# Validate file size (10MB limit)
max_size = 10 * 1024 * 1024 # 10MB
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"
)
# Create uploads directory if it doesn't exist # Create uploads directory if it doesn't exist
upload_dir = "uploads/avatars" upload_dir = "uploads/avatars"
os.makedirs(upload_dir, exist_ok=True) os.makedirs(upload_dir, exist_ok=True)

View File

@@ -44,6 +44,74 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Image resizing function
const resizeImage = (file: File, maxWidth: number, maxHeight: number): Promise<File> => {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (event) => {
if (typeof event.target?.result !== 'string') {
reject(new Error('Failed to read file'));
return;
}
img.onload = () => {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
// Calculate new dimensions
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Failed to get canvas context'));
return;
}
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error('Failed to create blob'));
return;
}
const resizedFile = new File([blob], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
});
console.log(`Resized image from ${img.width}x${img.height} to ${width}x${height}`);
console.log(`File size reduced from ${file.size} to ${resizedFile.size} bytes`);
resolve(resizedFile);
}, 'image/jpeg', 0.8); // JPEG quality 80%
};
img.onerror = reject;
img.src = event.target?.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
const saveMutation = useMutation({ const saveMutation = useMutation({
mutationFn: () => mutationFn: () =>
updateSettings({ updateSettings({
@@ -127,9 +195,19 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
if (file) { if (file) {
console.log("Selected file:", file.name, file.type, file.size); console.log("Selected file:", file.name, file.type, file.size);
setUploading(true); setUploading(true);
try { try {
// Check file size and resize if needed
const maxSize = 2 * 1024 * 1024; // 2MB
let processedFile = file;
if (file.size > maxSize) {
console.log("File too large, resizing...");
processedFile = await resizeImage(file, 800, 800); // Max 800x800
}
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', processedFile, processedFile.name || file.name);
console.log("Uploading file to /auth/me/avatar"); console.log("Uploading file to /auth/me/avatar");
const response = await api.post<MemberRead>('/auth/me/avatar', formData); const response = await api.post<MemberRead>('/auth/me/avatar', formData);