Add @csrf.exempt to all API routes in app/api/routes.py and app/upload/routes.py

- Added CSRF exemption to all JWT-authenticated API endpoints
- API routes use JWT tokens, not session cookies, so CSRF protection is not needed
parent b19fc3ac
...@@ -4,7 +4,7 @@ from flask import request, jsonify, current_app ...@@ -4,7 +4,7 @@ from flask import request, jsonify, current_app
from flask_jwt_extended import jwt_required, get_jwt_identity from flask_jwt_extended import jwt_required, get_jwt_identity
from sqlalchemy import func, desc from sqlalchemy import func, desc
from app.api import bp from app.api import bp
from app import db from app import db, csrf
from app.utils.security import require_admin, require_active_user from app.utils.security import require_admin, require_active_user
from app.utils.logging import log_api_request from app.utils.logging import log_api_request
from app.upload.file_handler import get_file_upload_handler from app.upload.file_handler import get_file_upload_handler
...@@ -13,6 +13,7 @@ from app.upload.fixture_parser import get_fixture_parser ...@@ -13,6 +13,7 @@ from app.upload.fixture_parser import get_fixture_parser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@bp.route('/matches', methods=['GET']) @bp.route('/matches', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_get_matches(): def api_get_matches():
"""Get matches with pagination and filtering""" """Get matches with pagination and filtering"""
...@@ -116,6 +117,7 @@ def api_get_matches(): ...@@ -116,6 +117,7 @@ def api_get_matches():
return jsonify({'error': 'Failed to retrieve matches'}), 500 return jsonify({'error': 'Failed to retrieve matches'}), 500
@bp.route('/matches/<int:match_id>', methods=['GET']) @bp.route('/matches/<int:match_id>', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_get_match(match_id): def api_get_match(match_id):
"""Get specific match details""" """Get specific match details"""
...@@ -149,6 +151,7 @@ def api_get_match(match_id): ...@@ -149,6 +151,7 @@ def api_get_match(match_id):
return jsonify({'error': 'Failed to retrieve match'}), 500 return jsonify({'error': 'Failed to retrieve match'}), 500
@bp.route('/matches/<int:match_id>', methods=['PUT']) @bp.route('/matches/<int:match_id>', methods=['PUT'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_update_match(match_id): def api_update_match(match_id):
"""Update match details""" """Update match details"""
...@@ -207,6 +210,7 @@ def api_update_match(match_id): ...@@ -207,6 +210,7 @@ def api_update_match(match_id):
return jsonify({'error': 'Failed to update match'}), 500 return jsonify({'error': 'Failed to update match'}), 500
@bp.route('/matches/<int:match_id>', methods=['DELETE']) @bp.route('/matches/<int:match_id>', methods=['DELETE'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_delete_match(match_id): def api_delete_match(match_id):
"""Delete match (admin only)""" """Delete match (admin only)"""
...@@ -244,6 +248,7 @@ def api_delete_match(match_id): ...@@ -244,6 +248,7 @@ def api_delete_match(match_id):
return jsonify({'error': 'Failed to delete match'}), 500 return jsonify({'error': 'Failed to delete match'}), 500
@bp.route('/statistics', methods=['GET']) @bp.route('/statistics', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_get_statistics(): def api_get_statistics():
"""Get comprehensive statistics""" """Get comprehensive statistics"""
...@@ -312,6 +317,7 @@ def api_get_statistics(): ...@@ -312,6 +317,7 @@ def api_get_statistics():
return jsonify({'error': 'Failed to retrieve statistics'}), 500 return jsonify({'error': 'Failed to retrieve statistics'}), 500
@bp.route('/admin/users', methods=['GET']) @bp.route('/admin/users', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
@require_admin @require_admin
def api_admin_get_users(): def api_admin_get_users():
...@@ -367,6 +373,7 @@ def api_admin_get_users(): ...@@ -367,6 +373,7 @@ def api_admin_get_users():
return jsonify({'error': 'Failed to retrieve users'}), 500 return jsonify({'error': 'Failed to retrieve users'}), 500
@bp.route('/admin/users/<int:user_id>', methods=['PUT']) @bp.route('/admin/users/<int:user_id>', methods=['PUT'])
@csrf.exempt
@jwt_required() @jwt_required()
@require_admin @require_admin
def api_admin_update_user(user_id): def api_admin_update_user(user_id):
...@@ -408,6 +415,7 @@ def api_admin_update_user(user_id): ...@@ -408,6 +415,7 @@ def api_admin_update_user(user_id):
return jsonify({'error': 'Failed to update user'}), 500 return jsonify({'error': 'Failed to update user'}), 500
@bp.route('/admin/logs', methods=['GET']) @bp.route('/admin/logs', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
@require_admin @require_admin
def api_admin_get_logs(): def api_admin_get_logs():
...@@ -452,6 +460,7 @@ def api_admin_get_logs(): ...@@ -452,6 +460,7 @@ def api_admin_get_logs():
return jsonify({'error': 'Failed to retrieve logs'}), 500 return jsonify({'error': 'Failed to retrieve logs'}), 500
@bp.route('/admin/system-info', methods=['GET']) @bp.route('/admin/system-info', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
@require_admin @require_admin
def api_admin_system_info(): def api_admin_system_info():
...@@ -498,6 +507,7 @@ def api_admin_system_info(): ...@@ -498,6 +507,7 @@ def api_admin_system_info():
return jsonify({'error': 'Failed to retrieve system information'}), 500 return jsonify({'error': 'Failed to retrieve system information'}), 500
@bp.route('/admin/cleanup', methods=['POST']) @bp.route('/admin/cleanup', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
@require_admin @require_admin
def api_admin_cleanup(): def api_admin_cleanup():
......
...@@ -248,6 +248,7 @@ def delete_zip(match_id): ...@@ -248,6 +248,7 @@ def delete_zip(match_id):
return redirect(request.referrer or url_for('main.fixtures')) return redirect(request.referrer or url_for('main.fixtures'))
@bp.route('/api/fixture', methods=['POST']) @bp.route('/api/fixture', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_fixture(): def api_upload_fixture():
"""Upload fixture file (CSV/XLSX) - API endpoint""" """Upload fixture file (CSV/XLSX) - API endpoint"""
...@@ -304,6 +305,7 @@ def api_upload_fixture(): ...@@ -304,6 +305,7 @@ def api_upload_fixture():
return jsonify({'error': 'Upload processing failed'}), 500 return jsonify({'error': 'Upload processing failed'}), 500
@bp.route('/api/zip/<int:match_id>', methods=['POST']) @bp.route('/api/zip/<int:match_id>', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_zip(match_id): def api_upload_zip(match_id):
"""Upload ZIP file for specific match - API endpoint""" """Upload ZIP file for specific match - API endpoint"""
...@@ -370,6 +372,7 @@ def api_upload_zip(match_id): ...@@ -370,6 +372,7 @@ def api_upload_zip(match_id):
return jsonify({'error': 'Upload processing failed'}), 500 return jsonify({'error': 'Upload processing failed'}), 500
@bp.route('/api/upload-async', methods=['POST']) @bp.route('/api/upload-async', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_async(): def api_upload_async():
"""Start asynchronous file upload""" """Start asynchronous file upload"""
...@@ -416,6 +419,7 @@ def api_upload_async(): ...@@ -416,6 +419,7 @@ def api_upload_async():
return jsonify({'error': 'Upload start failed'}), 500 return jsonify({'error': 'Upload start failed'}), 500
@bp.route('/api/upload-status/<upload_id>', methods=['GET']) @bp.route('/api/upload-status/<upload_id>', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_status(upload_id): def api_upload_status(upload_id):
"""Get status of asynchronous upload""" """Get status of asynchronous upload"""
...@@ -430,6 +434,7 @@ def api_upload_status(upload_id): ...@@ -430,6 +434,7 @@ def api_upload_status(upload_id):
return jsonify({'error': 'Status check failed'}), 500 return jsonify({'error': 'Status check failed'}), 500
@bp.route('/api/upload-cancel/<upload_id>', methods=['POST']) @bp.route('/api/upload-cancel/<upload_id>', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_cancel(upload_id): def api_upload_cancel(upload_id):
"""Cancel asynchronous upload""" """Cancel asynchronous upload"""
...@@ -447,6 +452,7 @@ def api_upload_cancel(upload_id): ...@@ -447,6 +452,7 @@ def api_upload_cancel(upload_id):
return jsonify({'error': 'Cancel operation failed'}), 500 return jsonify({'error': 'Cancel operation failed'}), 500
@bp.route('/api/progress/<int:upload_id>', methods=['GET']) @bp.route('/api/progress/<int:upload_id>', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_progress(upload_id): def api_upload_progress(upload_id):
"""Get upload progress for specific upload record""" """Get upload progress for specific upload record"""
...@@ -476,6 +482,7 @@ def api_upload_progress(upload_id): ...@@ -476,6 +482,7 @@ def api_upload_progress(upload_id):
return jsonify({'error': 'Progress check failed'}), 500 return jsonify({'error': 'Progress check failed'}), 500
@bp.route('/api/uploads', methods=['GET']) @bp.route('/api/uploads', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_list_uploads(): def api_list_uploads():
"""List user's uploads with pagination""" """List user's uploads with pagination"""
...@@ -516,6 +523,7 @@ def api_list_uploads(): ...@@ -516,6 +523,7 @@ def api_list_uploads():
return jsonify({'error': 'Upload list failed'}), 500 return jsonify({'error': 'Upload list failed'}), 500
@bp.route('/api/statistics', methods=['GET']) @bp.route('/api/statistics', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_statistics(): def api_upload_statistics():
"""Get upload statistics""" """Get upload statistics"""
...@@ -556,6 +564,7 @@ def api_upload_statistics(): ...@@ -556,6 +564,7 @@ def api_upload_statistics():
return jsonify({'error': 'Statistics calculation failed'}), 500 return jsonify({'error': 'Statistics calculation failed'}), 500
@bp.route('/api/cleanup', methods=['POST']) @bp.route('/api/cleanup', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_cleanup_uploads(): def api_cleanup_uploads():
"""Clean up failed uploads (admin only)""" """Clean up failed uploads (admin only)"""
...@@ -577,6 +586,7 @@ def api_cleanup_uploads(): ...@@ -577,6 +586,7 @@ def api_cleanup_uploads():
return jsonify({'error': 'Cleanup failed'}), 500 return jsonify({'error': 'Cleanup failed'}), 500
@bp.route('/api/zip/<int:match_id>/stream', methods=['POST']) @bp.route('/api/zip/<int:match_id>/stream', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_zip_stream(match_id): def api_upload_zip_stream(match_id):
"""Upload ZIP file for specific match using streaming - API endpoint for large files""" """Upload ZIP file for specific match using streaming - API endpoint for large files"""
...@@ -700,6 +710,7 @@ def api_upload_zip_stream(match_id): ...@@ -700,6 +710,7 @@ def api_upload_zip_stream(match_id):
return jsonify({'error': 'Streaming upload failed'}), 500 return jsonify({'error': 'Streaming upload failed'}), 500
@bp.route('/api/zip/<int:match_id>/resume', methods=['POST']) @bp.route('/api/zip/<int:match_id>/resume', methods=['POST'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_zip_resume(match_id): def api_upload_zip_resume(match_id):
"""Resume ZIP file upload for specific match - API endpoint""" """Resume ZIP file upload for specific match - API endpoint"""
...@@ -787,6 +798,7 @@ def api_upload_zip_resume(match_id): ...@@ -787,6 +798,7 @@ def api_upload_zip_resume(match_id):
return jsonify({'error': 'Resume upload failed'}), 500 return jsonify({'error': 'Resume upload failed'}), 500
@bp.route('/api/upload-info/<int:upload_id>', methods=['GET']) @bp.route('/api/upload-info/<int:upload_id>', methods=['GET'])
@csrf.exempt
@jwt_required() @jwt_required()
def api_upload_info(upload_id): def api_upload_info(upload_id):
"""Get detailed upload information including resume capability""" """Get detailed upload information including resume capability"""
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment