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 flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
from flask_wtf import CSRFProtect
|
||||||
|
from flask_talisman import Talisman
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
|
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)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
@ -74,8 +89,38 @@ def db_to_json():
|
|||||||
"guestNames": guest_names
|
"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):
|
def json_to_db(data):
|
||||||
try:
|
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
|
# Start a single transaction for all operations
|
||||||
with db.session.begin():
|
with db.session.begin():
|
||||||
# Clear existing attendance records
|
# Clear existing attendance records
|
||||||
@ -120,9 +165,8 @@ def json_to_db(data):
|
|||||||
|
|
||||||
# The transaction will be committed automatically if no exceptions occur
|
# The transaction will be committed automatically if no exceptions occur
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# The transaction will be rolled back automatically if an exception occurs
|
logger.error(f"Error during database operation: {e}")
|
||||||
print("Error during database operation:", e)
|
raise ValueError("An error occurred while processing the data.")
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -140,10 +184,17 @@ def get_data():
|
|||||||
return jsonify(db_to_json())
|
return jsonify(db_to_json())
|
||||||
|
|
||||||
@app.route('/api/data', methods=['POST'])
|
@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():
|
def update_data():
|
||||||
data = request.json
|
data = request.json
|
||||||
json_to_db(data)
|
try:
|
||||||
return jsonify({"status": "success"})
|
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)
|
# Optional: export from DB as JSON (for compatibility)
|
||||||
@app.route('/export-data')
|
@app.route('/export-data')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user