Reservierungssystem: E-Mail-Feld & Bild-Captcha hinzugefuegt

Aenderungen:
- E-Mail-Feld im Reservierungs-Formular hinzugefuegt (Pflichtfeld)
- Math-Captcha durch Bild-Captcha (4 Zeichen) ersetzt
- E-Mail-Validierung im Backend
- Captcha-Validierung fuer Reservierungen
- Pillow zu Dockerfile hinzugefuegt
This commit is contained in:
Peter
2026-05-16 13:29:25 +00:00
parent f26d02573e
commit ea90a3c9e3
6 changed files with 137 additions and 36 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ FROM python:3.12-slim
WORKDIR /app WORKDIR /app
RUN pip install --no-cache-dir flask flask-cors werkzeug requests RUN pip install --no-cache-dir flask flask-cors werkzeug requests Pillow
COPY app/ ./ COPY app/ ./
+64 -14
View File
@@ -2,35 +2,85 @@ import os
import hashlib import hashlib
import random import random
import time import time
import io
import base64
from functools import wraps from functools import wraps
from flask import session, request, jsonify from flask import session, request, jsonify
from PIL import Image, ImageDraw, ImageFont
ADMIN_PASSWORD_PLAIN = 'changeme' ADMIN_PASSWORD_PLAIN = 'changeme'
SECRET_KEY = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production') SECRET_KEY = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production')
def generate_captcha(): def generate_captcha():
a = random.randint(1, 15) """Generiert ein Bild-Captcha mit 4 Zeichen (Zahlen und Buchstaben)"""
b = random.randint(1, 15) # 4 zufaellige Zeichen (ohne aehnlich aussehende wie O/0, I/l)
op = random.choice(['+', '-']) chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
if op == '+': code = ''.join(random.choice(chars) for _ in range(4))
answer = a + b
else: # Token fuer Validierung
answer = a - b token = hashlib.sha256(f"{code}{int(time.time()/600)}captcha".encode()).hexdigest()[:16]
token = hashlib.sha256(f"{a}{op}{b}{int(time.time()/600)}captcha".encode()).hexdigest()[:16]
# FIX: Speichere Antwort in Session # Antwort in Session speichern
session['captcha_answer'] = answer session['captcha_answer'] = code
session['captcha_token'] = token
# Bild generieren
img = Image.new('RGB', (180, 60), color='#f8fafc')
draw = ImageDraw.Draw(img)
# Hintergrund-Noise (Punkte)
for _ in range(100):
x = random.randint(0, 180)
y = random.randint(0, 60)
draw.point((x, y), fill='#cbd5e1')
# Linien hinzufuegen
for _ in range(5):
x1 = random.randint(0, 180)
y1 = random.randint(0, 60)
x2 = random.randint(0, 180)
y2 = random.randint(0, 60)
draw.line([(x1, y1), (x2, y2)], fill='#94a3b8', width=1)
# Text zeichnen (mit leichter Rotation pro Buchstabe)
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 36)
except:
font = ImageFont.load_default()
positions = [20, 55, 90, 125]
for i, char in enumerate(code):
x = positions[i]
y = random.randint(12, 18)
# Zufaellige Farbe pro Buchstabe
color = random.choice(['#1e40af', '#166534', '#9a3412', '#701a75'])
draw.text((x, y), char, font=font, fill=color)
# Mehr Noise ueber den Text
for _ in range(50):
x = random.randint(0, 180)
y = random.randint(0, 60)
draw.point((x, y), fill='#64748b')
# In Base64 konvertieren
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
return { return {
"question": f"{a} {op} {b} = ?", "image": f"data:image/png;base64,{img_base64}",
"token": token, "token": token
"answer": answer
} }
def verify_captcha(token, answer): def verify_captcha(token, answer):
"""Ueberprueft die Captcha-Antwort"""
if not token or not answer: if not token or not answer:
return False return False
stored = session.get('captcha_answer') stored = session.get('captcha_answer')
if stored and str(stored) == str(answer): stored_token = session.get('captcha_token')
if stored and stored_token == token and str(stored).upper() == str(answer).upper():
session.pop('captcha_answer', None) session.pop('captcha_answer', None)
session.pop('captcha_token', None)
return True return True
return False return False
+3 -2
View File
@@ -7,8 +7,8 @@ auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/api/captcha', methods=['GET']) @auth_bp.route('/api/captcha', methods=['GET'])
def get_captcha(): def get_captcha():
captcha = generate_captcha() captcha = generate_captcha()
session['captcha_answer'] = captcha['answer'] # captcha enthaelt jetzt 'image' und 'token' (kein 'answer' mehr)
return jsonify({"question": captcha['question'], "token": captcha['token']}) return jsonify({"image": captcha["image"], "token": captcha["token"]})
@auth_bp.route('/api/admin/login', methods=['POST']) @auth_bp.route('/api/admin/login', methods=['POST'])
def admin_login(): def admin_login():
@@ -32,3 +32,4 @@ def check_session():
role = session.get('user_role') role = session.get('user_role')
if role: if role:
return jsonify({"role": role, "logged_in": True}) return jsonify({"role": role, "logged_in": True})
return jsonify({"logged_in": False})
+21
View File
@@ -46,6 +46,15 @@ DEFAULT_OPEN_HOUR = 10 # 10:00
DEFAULT_CLOSE_HOUR = 23 # 23:00 DEFAULT_CLOSE_HOUR = 23 # 23:00
RESERVATION_DURATION = 120 # Minuten RESERVATION_DURATION = 120 # Minuten
def is_valid_email(email):
"""Einfache E-Mail-Validierung"""
if not email:
return False
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return re.match(pattern, email) is not None
def init_app(): def init_app():
"""App initialisieren""" """App initialisieren"""
init_db() init_db()
@@ -273,6 +282,18 @@ def reservations():
# POST: Neue Reservierung # POST: Neue Reservierung
data = request.get_json() data = request.get_json()
# Captcha validieren
captcha_token = data.get("captcha_token")
captcha_answer = data.get("captcha_answer")
if not verify_captcha(captcha_token, captcha_answer):
return jsonify({"error": "Ungueltiges oder abgelaufenes Captcha"}), 400
# E-Mail validieren
email = data.get("email", "").strip()
if not email or not is_valid_email(email):
return jsonify({"error": "Gueltige E-Mail-Adresse erforderlich"}), 400
data["email"] = email
# Fix: time -> time_from/time_to Mapping # Fix: time -> time_from/time_to Mapping
# Frontend sendet 'time', Backend erwartet 'time_from'/'time_to' # Frontend sendet 'time', Backend erwartet 'time_from'/'time_to'
if 'time' in data and 'time_from' not in data: if 'time' in data and 'time_from' not in data:
+47 -18
View File
@@ -74,6 +74,15 @@
.btn-secondary:hover { background: var(--bg); } .btn-secondary:hover { background: var(--bg); }
.btn-icon {
padding: 0.25rem 0.5rem;
font-size: 1.2rem;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
}
/* Login Modal */ /* Login Modal */
.modal { .modal {
display: none; display: none;
@@ -198,26 +207,34 @@
color: var(--text-light); color: var(--text-light);
} }
/* Captcha */ /* Bild-Captcha */
.captcha-box { .captcha-box {
background: var(--bg); background: var(--bg);
padding: 1rem; padding: 1.5rem;
border-radius: 8px; border-radius: 8px;
margin-bottom: 1rem; margin-bottom: 1.5rem;
text-align: center; text-align: center;
} }
.captcha-question { .captcha-image {
font-size: 1.5rem; border-radius: 8px;
font-weight: 600; margin-bottom: 1rem;
color: var(--primary); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 0.5rem; }
.captcha-row {
display: flex;
gap: 0.5rem;
justify-content: center;
align-items: center;
} }
.captcha-input { .captcha-input {
width: 120px; width: 140px;
text-align: center; text-align: center;
font-size: 1.25rem; font-size: 1.25rem;
letter-spacing: 0.2em;
text-transform: uppercase;
} }
/* Guest View Restrictions */ /* Guest View Restrictions */
@@ -302,31 +319,39 @@
<div id="bookingSection" style="display: none;"> <div id="bookingSection" style="display: none;">
<h2 style="margin: 2rem 0 1rem;">Tisch reservieren</h2> <h2 style="margin: 2rem 0 1rem;">Tisch reservieren</h2>
<!-- Bild-Captcha -->
<div class="captcha-box"> <div class="captcha-box">
<div class="captcha-question" id="captchaQuestion">Lade Captcha...</div> <img id="captchaImage" class="captcha-image" src="" alt="Captcha" width="180" height="60">
<input type="number" class="captcha-input" id="captchaAnswer" placeholder="?" required> <div class="captcha-row">
<input type="text" class="captcha-input" id="captchaAnswer" placeholder="CODE" maxlength="4" required>
<button type="button" class="btn-icon" onclick="loadCaptcha()" title="Neues Captcha">🔄</button>
</div>
<input type="hidden" id="captchaToken"> <input type="hidden" id="captchaToken">
</div> </div>
<form id="bookingForm" onsubmit="handleBooking(event)"> <form id="bookingForm" onsubmit="handleBooking(event)">
<div class="form-group"> <div class="form-group">
<label>Name</label> <label>Name *</label>
<input type="text" id="bookingName" required placeholder="Max Mustermann"> <input type="text" id="bookingName" required placeholder="Max Mustermann">
</div> </div>
<div class="form-group">
<label>E-Mail *</label>
<input type="email" id="bookingEmail" required placeholder="max@beispiel.de">
</div>
<div class="form-group"> <div class="form-group">
<label>Telefon</label> <label>Telefon</label>
<input type="tel" id="bookingPhone" placeholder="+49 123 456789"> <input type="tel" id="bookingPhone" placeholder="+49 123 456789">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Datum</label> <label>Datum *</label>
<input type="date" id="bookingDate" required> <input type="date" id="bookingDate" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Zeit</label> <label>Zeit *</label>
<input type="time" id="bookingTime" required> <input type="time" id="bookingTime" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Personen</label> <label>Personen *</label>
<input type="number" id="bookingGuests" min="1" max="20" value="2" required> <input type="number" id="bookingGuests" min="1" max="20" value="2" required>
</div> </div>
<button type="submit" class="btn btn-primary" style="width: 100%">Reservierung anfragen</button> <button type="submit" class="btn btn-primary" style="width: 100%">Reservierung anfragen</button>
@@ -401,15 +426,17 @@
document.getElementById('bookingSection').scrollIntoView({ behavior: 'smooth' }); document.getElementById('bookingSection').scrollIntoView({ behavior: 'smooth' });
} }
// Captcha // Bild-Captcha laden
async function loadCaptcha() { async function loadCaptcha() {
try { try {
const res = await fetch('/api/captcha'); const res = await fetch('/api/captcha');
const data = await res.json(); const data = await res.json();
document.getElementById('captchaQuestion').textContent = data.question; document.getElementById('captchaImage').src = data.image;
document.getElementById('captchaToken').value = data.token; document.getElementById('captchaToken').value = data.token;
document.getElementById('captchaAnswer').value = '';
} catch (e) { } catch (e) {
document.getElementById('captchaQuestion').textContent = 'Captcha nicht verfügbar'; console.error('Captcha Fehler:', e);
document.getElementById('captchaImage').alt = 'Captcha nicht verfuegbar';
} }
} }
@@ -458,10 +485,12 @@
const data = { const data = {
name: document.getElementById('bookingName').value, name: document.getElementById('bookingName').value,
email: document.getElementById('bookingEmail').value,
phone: document.getElementById('bookingPhone').value, phone: document.getElementById('bookingPhone').value,
date: document.getElementById('bookingDate').value, date: document.getElementById('bookingDate').value,
time_from: document.getElementById('bookingTime').value, time_from: document.getElementById('bookingTime').value,
guests: parseInt(document.getElementById('bookingGuests').value), guests: parseInt(document.getElementById('bookingGuests').value),
room_id: selectedRoom,
captcha_token: captchaToken, captcha_token: captchaToken,
captcha_answer: captchaAnswer captcha_answer: captchaAnswer
}; };
+1 -1
View File
@@ -22,4 +22,4 @@ volumes:
networks: networks:
cctv-network: cctv-network:
external: true external: true
name: cctv_default name: reservierung-system_default