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:
2026-05-27 07:10:52 +00:00
parent a3546b4c01
commit 3c26f1310e
4 changed files with 380 additions and 305 deletions
+58 -33
View File
@@ -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