diff --git a/backend/app.py b/backend/app.py index bdf05170c..173b1c0a5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -20,6 +20,8 @@ import requests import secrets from urllib.parse import urlencode +from werkzeug.utils import secure_filename +import magic import logging from datetime import datetime, timedelta, timezone @@ -1904,6 +1906,75 @@ def logout(): return resp, status +UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads', 'avatars') +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} +ALLOWED_MIME_TYPES = {'image/png', 'image/jpeg', 'image/gif'} + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +@app.route('/api/v1/users//avatar', methods=['POST']) +@jwt_required() +def upload_avatar(user_id): + """Upload and update user avatar with MIME type and magic number validation.""" + current_user_id = get_jwt_identity() + + if str(user_id) != str(current_user_id): + return forbidden_error("Unauthorized access to another user's profile") + + if 'file' not in request.files: + return jsonify({"error": "No file part in the request"}), 400 + + file = request.files['file'] + + if file.filename == '': + return jsonify({"error": "No file selected for uploading"}), 400 + + if not allowed_file(file.filename): + return jsonify({"error": "Allowed file types are png, jpg, jpeg, gif"}), 400 + + # Validate Content-Type header + file_content_type = file.content_type + if file_content_type not in ALLOWED_MIME_TYPES: + return jsonify({"error": "Invalid Content-Type"}), 400 + + # Verify file magic numbers + file_bytes = file.read(2048) + file.seek(0) # Reset file pointer after reading + + try: + mime = magic.Magic(mime=True) + file_mime = mime.from_buffer(file_bytes) + + if file_mime not in ALLOWED_MIME_TYPES: + return jsonify({"error": "Invalid file content (magic number mismatch)"}), 400 + except Exception as e: + logger.error(f"Error validating file magic numbers: {e}") + return jsonify({"error": "Could not validate file format"}), 500 + + filename = secure_filename(file.filename) + unique_filename = f"user_{user_id}_{filename}" + filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename) + + try: + file.save(filepath) + + user = User.query.get(user_id) + if user: + user.profile_picture = f"/uploads/avatars/{unique_filename}" + db.session.commit() + + return jsonify({ + "message": "Avatar uploaded successfully", + "profile_picture": f"/uploads/avatars/{unique_filename}" + }), 200 + except Exception as e: + logger.error(f"Failed to save avatar: {e}") + return jsonify({"error": "Failed to save file"}), 500 + + @app.route('/api/v1/auth/forgot-password', methods=['POST']) @csrf.exempt @limiter.limit("5 per minute") diff --git a/backend/requirements.txt b/backend/requirements.txt index 5726fbba4..1940cad79 100644 Binary files a/backend/requirements.txt and b/backend/requirements.txt differ diff --git a/frontend/data/contributors.json b/frontend/data/contributors.json index 725dfdd51..b6c1e2c2f 100644 --- a/frontend/data/contributors.json +++ b/frontend/data/contributors.json @@ -197,6 +197,28 @@ "site_admin": false, "contributions": 23 }, + { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false, + "contributions": 13 + }, { "login": "github-actions[bot]", "id": 41898282,