Compare commits
No commits in common. "b612b23340074e6ad8c2efb06eca91b080f59165" and "7dce92f5c1647138c7ec841a534840070e6c5deb" have entirely different histories.
b612b23340
...
7dce92f5c1
75
app.py
75
app.py
@ -3,42 +3,9 @@ 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
|
|
||||||
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)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
@ -107,38 +74,8 @@ 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
|
||||||
@ -183,8 +120,9 @@ 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:
|
||||||
logger.error(f"Error during database operation: {e}")
|
# The transaction will be rolled back automatically if an exception occurs
|
||||||
raise ValueError("An error occurred while processing the data.")
|
print("Error during database operation:", e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -202,17 +140,10 @@ 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
|
||||||
try:
|
|
||||||
json_to_db(data)
|
json_to_db(data)
|
||||||
return jsonify({"status": "success"})
|
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')
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
Flask
|
flask
|
||||||
Flask-SQLAlchemy
|
flask_sqlalchemy
|
||||||
SQLAlchemy
|
sqlalchemy
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
Flask-WTF
|
|
||||||
Flask-Talisman
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user