feat: add input validation, security headers, and error handling to Flask app
This commit is contained in:
parent
7dce92f5c1
commit
118eb82393
61
app.py
61
app.py
@ -3,9 +3,24 @@ from flask import Flask, render_template, request, jsonify
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import func
|
||||
import os
|
||||
import logging
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_talisman import Talisman
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'change-this-secret-key')
|
||||
|
||||
# CSRF Protection
|
||||
csrf = CSRFProtect(app)
|
||||
|
||||
# Security Headers
|
||||
Talisman(app)
|
||||
|
||||
# Logging Configuration
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# Models
|
||||
@ -74,8 +89,38 @@ def db_to_json():
|
||||
"guestNames": guest_names
|
||||
}
|
||||
|
||||
import re
|
||||
|
||||
def validate_player_name(name):
|
||||
# Only allow a-z, A-Z, 0-9, spaces, hyphens, and periods, max 50 chars
|
||||
return bool(re.fullmatch(r'[a-zA-Z0-9 .-]{1,50}', name))
|
||||
|
||||
def validate_guest_name(name):
|
||||
# Only allow a-z, A-Z, 0-9, spaces, hyphens, and periods, max 50 chars
|
||||
return bool(re.fullmatch(r'[a-zA-Z0-9 .-]{1,50}', name))
|
||||
|
||||
def validate_date_str(date_str):
|
||||
# Format: DD/MM/YY
|
||||
return bool(re.fullmatch(r'\d{2}/\d{2}/\d{2}', date_str))
|
||||
|
||||
def json_to_db(data):
|
||||
try:
|
||||
# Validate players
|
||||
for name in data.get("players", []):
|
||||
if not validate_player_name(name):
|
||||
logger.warning(f"Invalid player name: {name}")
|
||||
raise ValueError("Invalid player name.")
|
||||
# Validate dates
|
||||
for date_str in data.get("dates", []):
|
||||
if not validate_date_str(date_str):
|
||||
logger.warning(f"Invalid date string: {date_str}")
|
||||
raise ValueError("Invalid date string.")
|
||||
# Validate guest names
|
||||
for guest_name in data.get("guestNames", {}).values():
|
||||
if not validate_guest_name(guest_name):
|
||||
logger.warning(f"Invalid guest name: {guest_name}")
|
||||
raise ValueError("Invalid guest name.")
|
||||
|
||||
# Start a single transaction for all operations
|
||||
with db.session.begin():
|
||||
# Clear existing attendance records
|
||||
@ -120,9 +165,8 @@ def json_to_db(data):
|
||||
|
||||
# The transaction will be committed automatically if no exceptions occur
|
||||
except Exception as e:
|
||||
# The transaction will be rolled back automatically if an exception occurs
|
||||
print("Error during database operation:", e)
|
||||
raise
|
||||
logger.error(f"Error during database operation: {e}")
|
||||
raise ValueError("An error occurred while processing the data.")
|
||||
|
||||
|
||||
|
||||
@ -140,10 +184,17 @@ def get_data():
|
||||
return jsonify(db_to_json())
|
||||
|
||||
@app.route('/api/data', methods=['POST'])
|
||||
@csrf.exempt # Remove this line if you want to enforce CSRF on API endpoints (for APIs, you may want to handle CSRF differently)
|
||||
def update_data():
|
||||
data = request.json
|
||||
json_to_db(data)
|
||||
return jsonify({"status": "success"})
|
||||
try:
|
||||
json_to_db(data)
|
||||
return jsonify({"status": "success"})
|
||||
except ValueError as e:
|
||||
return jsonify({"status": "error", "message": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in /api/data POST: {e}")
|
||||
return jsonify({"status": "error", "message": "Internal server error."}), 500
|
||||
|
||||
# Optional: export from DB as JSON (for compatibility)
|
||||
@app.route('/export-data')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user