Redesign profile photo upload with circular crop UI, server-side processing, and form integration
- Replace square preview with 280x280 circular container with crop ring overlay - Zoom slider and drag-to-pan for precise crop positioning - Client-side crop via canvas at 400x400 output resolution - Server-side resize to 400x400 (LANCZOS), EXIF stripping, RGB conversion, JPEG optimization - Generate CSRF token in before_request so photo upload fetch works - Save button now also submits the profile form (photo + fields updated together) - Remove dead photo-upload-form handler and unused CSS - Clean up debug markers and attendee code field from profile page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -610,6 +610,10 @@ def before_request():
|
||||
if lang in SUPPORTED_LOCALES:
|
||||
session['locale'] = lang
|
||||
|
||||
# Ensure CSRF token exists for all state-changing requests
|
||||
if 'csrf_token' not in session:
|
||||
session['csrf_token'] = secrets.token_hex(32)
|
||||
|
||||
# CSRF validation for state-changing requests
|
||||
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
|
||||
# Skip CSRF for login (users don't have a session yet)
|
||||
@@ -5332,7 +5336,7 @@ def staff_profile():
|
||||
@app.route('/attendee/photo', methods=['POST'])
|
||||
@login_required
|
||||
def upload_photo():
|
||||
"""Upload profile photo."""
|
||||
"""Upload and process profile photo (resize, strip EXIF, optimize)."""
|
||||
if session.get('user_type') != 'attendee':
|
||||
return jsonify({'error': 'Access denied'}), 403
|
||||
|
||||
@@ -5343,44 +5347,65 @@ def upload_photo():
|
||||
if file.filename == '':
|
||||
return jsonify({'error': 'No file selected'}), 400
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
# Read file content for validation
|
||||
file_content = file.read()
|
||||
try:
|
||||
from PIL import Image
|
||||
img = Image.open(io.BytesIO(file_content))
|
||||
img.verify()
|
||||
except Exception:
|
||||
return jsonify({'error': 'Invalid image file'}), 400
|
||||
if not (file and allowed_file(file.filename)):
|
||||
return jsonify({'error': 'Invalid file type'}), 400
|
||||
|
||||
# Generate unique filename
|
||||
ext = file.filename.rsplit('.', 1)[1].lower()
|
||||
filename = f"{uuid.uuid4()}.{ext}"
|
||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
file_content = file.read()
|
||||
|
||||
# Delete old photo if exists
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT profile_picture FROM attendees WHERE id = %s", (session['user_id'],))
|
||||
row = cursor.fetchone()
|
||||
old_photo = row['profile_picture'] if row else None
|
||||
if old_photo:
|
||||
old_path = os.path.join(app.config['UPLOAD_FOLDER'], old_photo)
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
# Process image: validate, resize, strip EXIF, optimize
|
||||
try:
|
||||
from PIL import Image
|
||||
img = Image.open(io.BytesIO(file_content))
|
||||
img.verify()
|
||||
# Re-open after verify() — verify() consumes the image
|
||||
img = Image.open(io.BytesIO(file_content))
|
||||
except Exception:
|
||||
return jsonify({'error': 'Invalid image file'}), 400
|
||||
|
||||
# Save the file
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(file_content)
|
||||
try:
|
||||
# Convert to RGB (handles RGBA PNG, CMYK, etc.)
|
||||
if img.mode in ('RGBA', 'P', 'LA', 'CMYK'):
|
||||
img = img.convert('RGB')
|
||||
|
||||
cursor.execute("UPDATE attendees SET profile_picture = %s WHERE id = %s", (filename, session['user_id']))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
# Resize to standard dimensions
|
||||
img = img.resize((400, 400), Image.LANCZOS)
|
||||
|
||||
return jsonify({'success': True, 'filename': filename})
|
||||
# Strip EXIF data for privacy
|
||||
from PIL import ImageOps
|
||||
img = ImageOps.exif_transpose(img)
|
||||
|
||||
return jsonify({'error': 'Invalid file type'}), 400
|
||||
# Save as optimized JPEG, no EXIF
|
||||
output = io.BytesIO()
|
||||
img.save(output, format='JPEG', quality=85, optimize=True)
|
||||
output.seek(0)
|
||||
processed_content = output.read()
|
||||
except Exception:
|
||||
return jsonify({'error': 'Failed to process image'}), 500
|
||||
|
||||
# Always save as .jpg since we convert to JPEG
|
||||
filename = f"{uuid.uuid4()}.jpg"
|
||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
|
||||
# Delete old photo if exists
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT profile_picture FROM attendees WHERE id = %s", (session['user_id'],))
|
||||
row = cursor.fetchone()
|
||||
old_photo = row['profile_picture'] if row else None
|
||||
if old_photo:
|
||||
old_path = os.path.join(app.config['UPLOAD_FOLDER'], old_photo)
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(processed_content)
|
||||
|
||||
cursor.execute("UPDATE attendees SET profile_picture = %s WHERE id = %s", (filename, session['user_id']))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({'success': True, 'filename': filename})
|
||||
|
||||
|
||||
# Routes - Attendees list and connections
|
||||
|
||||
Reference in New Issue
Block a user