feat: Implement CSRF protection and basic form validation, streamline form data, and add comprehensive unit and E2E tests.

This commit is contained in:
Feiko Wielsma 2026-03-25 09:10:34 +01:00
parent 4c435f8e17
commit 197ae8d75b
4 changed files with 120 additions and 19 deletions

58
app.py
View file

@ -1,10 +1,26 @@
import os
import yaml
import gspread
from flask import Flask, render_template, request, redirect, url_for, abort
import secrets
from flask import Flask, render_template, request, redirect, url_for, abort, session
from datetime import datetime
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32))
def generate_csrf_token():
if '_csrf_token' not in session:
session['_csrf_token'] = secrets.token_hex(16)
return session['_csrf_token']
app.jinja_env.globals['csrf_token'] = generate_csrf_token
@app.before_request
def csrf_protect():
if request.method == "POST":
token = session.get('_csrf_token', None)
if not token or token != request.form.get('_csrf_token'):
abort(403)
# Load Configuration
def load_config():
@ -47,20 +63,20 @@ def get_public_participants(sheet_id, tab_name=None):
public_list = []
for row in data_rows:
# Ensure row is long enough to avoid errors
if len(row) < 17:
if len(row) < 14:
continue
# Extract ONLY non-sensitive columns based on our save order
# 1: Klasse, 2: Zeilnummer, 3: Bootnaam, 4: Boottype
# 7: Naam, 10: Plaats, 16: Vereniging
# 7: Naam, 8: Plaats, 13: Vereniging
entry = {
'klasse': row[1],
'zeilnummer': row[2],
'bootnaam': row[3],
'boottype': row[4],
'naam': row[7],
'plaats': row[10],
'vereniging': row[16]
'plaats': row[8],
'vereniging': row[13]
}
public_list.append(entry)
@ -101,8 +117,16 @@ def event_form(event_slug):
event_data['sheet_id'] = sheet_id
if request.method == 'POST':
# Basic Validation
required_fields = ['klasse', 'zeilnummer', 'bootnaam', 'naam', 'telefoonmobiel', 'email']
for field in required_fields:
val = request.form.get(field)
if not val or not val.strip():
participants = get_public_participants(sheet_id, tab_name)
return render_template('form.html', event=event_data, slug=event_slug, participants=participants, error="Oeps! Bepaalde verplichte velden ontbreken.")
form_data = [
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 0: Timestamp
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 0: Timestamp
request.form.get('klasse'), # 1: Klasse
request.form.get('zeilnummer'), # 2: Zeilnummer
request.form.get('bootnaam'), # 3: Bootnaam
@ -110,23 +134,19 @@ def event_form(event_slug):
", ".join([k for k in ['genua', 'rolfok', 'spinaker', 'halfwinder', 'genaker', 'alleendacron'] if k in request.form]), # 5: Zeilvoering
request.form.get('schroef'), # 6: Schroef
request.form.get('naam'), # 7: Naam (Keep public)
"", # 8: Straat (REMOVED)
"", # 9: Postcode (REMOVED)
request.form.get('plaats'), # 10: Plaats
request.form.get('land', 'Nederland'), # 11: Land
request.form.get('telefoonmobiel'), # 12: Mobiel (PRIVATE)
"", # 13: Vast (REMOVED)
request.form.get('email'), # 14: Email (PRIVATE)
request.form.get('startlicentienummer'), # 15: Licentie
request.form.get('vereniging'), # 16: Vereniging
"", # 17: Buffet (REMOVED)
"", # 18: Ontbijt (REMOVED)
request.form.get('opmerkingen') # 19: Opmerkingen
request.form.get('plaats'), # 8: Plaats
request.form.get('land', 'Nederland'), # 9: Land
request.form.get('telefoonmobiel'), # 10: Mobiel (PRIVATE)
request.form.get('email'), # 11: Email (PRIVATE)
request.form.get('startlicentienummer'), # 12: Licentie
request.form.get('vereniging'), # 13: Vereniging
request.form.get('opmerkingen') # 14: Opmerkingen
]
sheet = get_google_sheet(sheet_id, tab_name)
if sheet:
sheet.append_row(form_data)
if not os.environ.get('TESTING_NO_APPEND'):
sheet.append_row(form_data)
return redirect(url_for('success', event_slug=event_slug))
else:
return f"Error: Could not connect to Google Sheet Tab '{tab_name}'. Check server logs."