Compare commits

..

3 Commits

2 changed files with 79 additions and 8 deletions

75
app.py
View File

@ -3,9 +3,42 @@ 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
csp = {
'default-src': [
"'self'"
],
'script-src': [
"'self'",
'https://cdn.jsdelivr.net/npm/chart.js',
"'unsafe-inline'"
],
'style-src': [
"'self'",
"'unsafe-inline'"
],
'img-src': [
"'self'",
'data:'
]
}
Talisman(app, content_security_policy=csp)
# Logging Configuration
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
db = SQLAlchemy(app)
# Models
@ -74,8 +107,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 +183,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 +202,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
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')

View File

@ -1,4 +1,6 @@
flask
flask_sqlalchemy
sqlalchemy
Flask
Flask-SQLAlchemy
SQLAlchemy
psycopg2-binary
Flask-WTF
Flask-Talisman