|
20 | 20 | import requests |
21 | 21 | import secrets |
22 | 22 | from urllib.parse import urlencode |
| 23 | +from werkzeug.utils import secure_filename |
| 24 | +import magic |
23 | 25 |
|
24 | 26 | import logging |
25 | 27 | from datetime import datetime, timedelta, timezone |
@@ -1904,6 +1906,75 @@ def logout(): |
1904 | 1906 | return resp, status |
1905 | 1907 |
|
1906 | 1908 |
|
| 1909 | +UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads', 'avatars') |
| 1910 | +os.makedirs(UPLOAD_FOLDER, exist_ok=True) |
| 1911 | +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER |
| 1912 | +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} |
| 1913 | +ALLOWED_MIME_TYPES = {'image/png', 'image/jpeg', 'image/gif'} |
| 1914 | + |
| 1915 | +def allowed_file(filename): |
| 1916 | + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS |
| 1917 | + |
| 1918 | +@app.route('/api/v1/users/<int:user_id>/avatar', methods=['POST']) |
| 1919 | +@jwt_required() |
| 1920 | +def upload_avatar(user_id): |
| 1921 | + """Upload and update user avatar with MIME type and magic number validation.""" |
| 1922 | + current_user_id = get_jwt_identity() |
| 1923 | + |
| 1924 | + if str(user_id) != str(current_user_id): |
| 1925 | + return forbidden_error("Unauthorized access to another user's profile") |
| 1926 | + |
| 1927 | + if 'file' not in request.files: |
| 1928 | + return jsonify({"error": "No file part in the request"}), 400 |
| 1929 | + |
| 1930 | + file = request.files['file'] |
| 1931 | + |
| 1932 | + if file.filename == '': |
| 1933 | + return jsonify({"error": "No file selected for uploading"}), 400 |
| 1934 | + |
| 1935 | + if not allowed_file(file.filename): |
| 1936 | + return jsonify({"error": "Allowed file types are png, jpg, jpeg, gif"}), 400 |
| 1937 | + |
| 1938 | + # Validate Content-Type header |
| 1939 | + file_content_type = file.content_type |
| 1940 | + if file_content_type not in ALLOWED_MIME_TYPES: |
| 1941 | + return jsonify({"error": "Invalid Content-Type"}), 400 |
| 1942 | + |
| 1943 | + # Verify file magic numbers |
| 1944 | + file_bytes = file.read(2048) |
| 1945 | + file.seek(0) # Reset file pointer after reading |
| 1946 | + |
| 1947 | + try: |
| 1948 | + mime = magic.Magic(mime=True) |
| 1949 | + file_mime = mime.from_buffer(file_bytes) |
| 1950 | + |
| 1951 | + if file_mime not in ALLOWED_MIME_TYPES: |
| 1952 | + return jsonify({"error": "Invalid file content (magic number mismatch)"}), 400 |
| 1953 | + except Exception as e: |
| 1954 | + logger.error(f"Error validating file magic numbers: {e}") |
| 1955 | + return jsonify({"error": "Could not validate file format"}), 500 |
| 1956 | + |
| 1957 | + filename = secure_filename(file.filename) |
| 1958 | + unique_filename = f"user_{user_id}_{filename}" |
| 1959 | + filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename) |
| 1960 | + |
| 1961 | + try: |
| 1962 | + file.save(filepath) |
| 1963 | + |
| 1964 | + user = User.query.get(user_id) |
| 1965 | + if user: |
| 1966 | + user.profile_picture = f"/uploads/avatars/{unique_filename}" |
| 1967 | + db.session.commit() |
| 1968 | + |
| 1969 | + return jsonify({ |
| 1970 | + "message": "Avatar uploaded successfully", |
| 1971 | + "profile_picture": f"/uploads/avatars/{unique_filename}" |
| 1972 | + }), 200 |
| 1973 | + except Exception as e: |
| 1974 | + logger.error(f"Failed to save avatar: {e}") |
| 1975 | + return jsonify({"error": "Failed to save file"}), 500 |
| 1976 | + |
| 1977 | + |
1907 | 1978 | @app.route('/api/v1/auth/forgot-password', methods=['POST']) |
1908 | 1979 | @csrf.exempt |
1909 | 1980 | @limiter.limit("5 per minute") |
|
0 commit comments