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:
+64
-14
@@ -2,35 +2,85 @@ import os
|
||||
import hashlib
|
||||
import random
|
||||
import time
|
||||
import io
|
||||
import base64
|
||||
from functools import wraps
|
||||
from flask import session, request, jsonify
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
ADMIN_PASSWORD_PLAIN = 'changeme'
|
||||
SECRET_KEY = os.environ.get('SESSION_SECRET', 'dev-secret-change-in-production')
|
||||
|
||||
def generate_captcha():
|
||||
a = random.randint(1, 15)
|
||||
b = random.randint(1, 15)
|
||||
op = random.choice(['+', '-'])
|
||||
if op == '+':
|
||||
answer = a + b
|
||||
else:
|
||||
answer = a - b
|
||||
token = hashlib.sha256(f"{a}{op}{b}{int(time.time()/600)}captcha".encode()).hexdigest()[:16]
|
||||
# FIX: Speichere Antwort in Session
|
||||
session['captcha_answer'] = answer
|
||||
"""Generiert ein Bild-Captcha mit 4 Zeichen (Zahlen und Buchstaben)"""
|
||||
# 4 zufaellige Zeichen (ohne aehnlich aussehende wie O/0, I/l)
|
||||
chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
code = ''.join(random.choice(chars) for _ in range(4))
|
||||
|
||||
# Token fuer Validierung
|
||||
token = hashlib.sha256(f"{code}{int(time.time()/600)}captcha".encode()).hexdigest()[:16]
|
||||
|
||||
# Antwort in Session speichern
|
||||
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 {
|
||||
"question": f"{a} {op} {b} = ?",
|
||||
"token": token,
|
||||
"answer": answer
|
||||
"image": f"data:image/png;base64,{img_base64}",
|
||||
"token": token
|
||||
}
|
||||
|
||||
def verify_captcha(token, answer):
|
||||
"""Ueberprueft die Captcha-Antwort"""
|
||||
if not token or not answer:
|
||||
return False
|
||||
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_token', None)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
+3
-2
@@ -7,8 +7,8 @@ auth_bp = Blueprint('auth', __name__)
|
||||
@auth_bp.route('/api/captcha', methods=['GET'])
|
||||
def get_captcha():
|
||||
captcha = generate_captcha()
|
||||
session['captcha_answer'] = captcha['answer']
|
||||
return jsonify({"question": captcha['question'], "token": captcha['token']})
|
||||
# captcha enthaelt jetzt 'image' und 'token' (kein 'answer' mehr)
|
||||
return jsonify({"image": captcha["image"], "token": captcha["token"]})
|
||||
|
||||
@auth_bp.route('/api/admin/login', methods=['POST'])
|
||||
def admin_login():
|
||||
@@ -32,3 +32,4 @@ def check_session():
|
||||
role = session.get('user_role')
|
||||
if role:
|
||||
return jsonify({"role": role, "logged_in": True})
|
||||
return jsonify({"logged_in": False})
|
||||
|
||||
+21
@@ -46,6 +46,15 @@ DEFAULT_OPEN_HOUR = 10 # 10:00
|
||||
DEFAULT_CLOSE_HOUR = 23 # 23:00
|
||||
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():
|
||||
"""App initialisieren"""
|
||||
init_db()
|
||||
@@ -273,6 +282,18 @@ def reservations():
|
||||
# POST: Neue Reservierung
|
||||
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
|
||||
# Frontend sendet 'time', Backend erwartet 'time_from'/'time_to'
|
||||
if 'time' in data and 'time_from' not in data:
|
||||
|
||||
+47
-18
@@ -74,6 +74,15 @@
|
||||
|
||||
.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 */
|
||||
.modal {
|
||||
display: none;
|
||||
@@ -198,26 +207,34 @@
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Captcha */
|
||||
/* Bild-Captcha */
|
||||
.captcha-box {
|
||||
background: var(--bg);
|
||||
padding: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.captcha-question {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin-bottom: 0.5rem;
|
||||
.captcha-image {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
width: 120px;
|
||||
width: 140px;
|
||||
text-align: center;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Guest View Restrictions */
|
||||
@@ -302,31 +319,39 @@
|
||||
<div id="bookingSection" style="display: none;">
|
||||
<h2 style="margin: 2rem 0 1rem;">Tisch reservieren</h2>
|
||||
|
||||
<!-- Bild-Captcha -->
|
||||
<div class="captcha-box">
|
||||
<div class="captcha-question" id="captchaQuestion">Lade Captcha...</div>
|
||||
<input type="number" class="captcha-input" id="captchaAnswer" placeholder="?" required>
|
||||
<img id="captchaImage" class="captcha-image" src="" alt="Captcha" width="180" height="60">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<form id="bookingForm" onsubmit="handleBooking(event)">
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<label>Name *</label>
|
||||
<input type="text" id="bookingName" required placeholder="Max Mustermann">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>E-Mail *</label>
|
||||
<input type="email" id="bookingEmail" required placeholder="max@beispiel.de">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Telefon</label>
|
||||
<input type="tel" id="bookingPhone" placeholder="+49 123 456789">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Datum</label>
|
||||
<label>Datum *</label>
|
||||
<input type="date" id="bookingDate" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Zeit</label>
|
||||
<label>Zeit *</label>
|
||||
<input type="time" id="bookingTime" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Personen</label>
|
||||
<label>Personen *</label>
|
||||
<input type="number" id="bookingGuests" min="1" max="20" value="2" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%">Reservierung anfragen</button>
|
||||
@@ -401,15 +426,17 @@
|
||||
document.getElementById('bookingSection').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Captcha
|
||||
// Bild-Captcha laden
|
||||
async function loadCaptcha() {
|
||||
try {
|
||||
const res = await fetch('/api/captcha');
|
||||
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('captchaAnswer').value = '';
|
||||
} 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 = {
|
||||
name: document.getElementById('bookingName').value,
|
||||
email: document.getElementById('bookingEmail').value,
|
||||
phone: document.getElementById('bookingPhone').value,
|
||||
date: document.getElementById('bookingDate').value,
|
||||
time_from: document.getElementById('bookingTime').value,
|
||||
guests: parseInt(document.getElementById('bookingGuests').value),
|
||||
room_id: selectedRoom,
|
||||
captcha_token: captchaToken,
|
||||
captcha_answer: captchaAnswer
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user