diff --git a/Dockerfile b/Dockerfile index 6184f95..129ec98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.12-slim 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/ ./ diff --git a/app/auth.py b/app/auth.py index 8734504..d98dfab 100644 --- a/app/auth.py +++ b/app/auth.py @@ -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 diff --git a/app/login_routes.py b/app/login_routes.py index 926415d..f3c0ffa 100644 --- a/app/login_routes.py +++ b/app/login_routes.py @@ -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}) diff --git a/app/main.py b/app/main.py index 01311f9..1d89d46 100644 --- a/app/main.py +++ b/app/main.py @@ -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: diff --git a/app/templates/index.html b/app/templates/index.html index 060ba60..daa8869 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -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 @@