diff --git a/app.py b/app.py index d1dde68..5871630 100644 --- a/app.py +++ b/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')