diff --git a/app.py b/app.py
new file mode 100644
index 0000000..3b7cac9
--- /dev/null
+++ b/app.py
@@ -0,0 +1,1483 @@
+
+from flask import Flask, request, jsonify, session, render_template_string, redirect
+import os
+import json
+import random
+import string
+import re
+import hashlib
+from datetime import date, datetime, timedelta
+from functools import wraps
+
+app = Flask(__name__)
+app.secret_key = os.environ.get('SESSION_SECRET', 'dev-secret')
+
+# === BENUTZERVERWALTUNG ===
+default_users = [
+ {
+ 'id': 1,
+ 'username': 'admin',
+ 'password_hash': hashlib.sha256('changeme'.encode()).hexdigest(),
+ 'name': 'Administrator',
+ 'email': 'admin@restaurant.de',
+ 'role': 'admin',
+ 'is_active': True,
+ 'created_at': datetime.now().isoformat(),
+ 'last_login': None,
+ 'force_password_change': False # Nach erstmaligem Login Passwort ändern
+ }
+]
+
+users = default_users.copy()
+
+ROLE_PERMISSIONS = {
+ 'admin': ['dashboard', 'reservations', 'rooms', 'tables', 'templates', 'smtp', 'users', 'reports', 'settings', 'change_password'],
+ 'manager': ['dashboard', 'reservations', 'rooms', 'tables', 'templates', 'reports', 'change_password'],
+ 'staff': ['dashboard', 'reservations', 'rooms', 'change_password']
+}
+
+def hash_password(password):
+ return hashlib.sha256(password.encode()).hexdigest()
+
+def check_permission(user_role, permission):
+ return permission in ROLE_PERMISSIONS.get(user_role, [])
+
+def require_permission(permission):
+ def decorator(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ user = next((u for u in users if u['id'] == session['user_id']), None)
+ if not user or not user.get('is_active', False):
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ if not check_permission(user['role'], permission):
+ return jsonify({'error': 'Forbidden'}), 403
+
+ return f(*args, **kwargs)
+ return decorated
+ return decorator
+
+# SMTP-Konfiguration
+smtp_config = {
+ 'host': os.environ.get('SMTP_HOST', ''),
+ 'port': int(os.environ.get('SMTP_PORT', 587)),
+ 'user': os.environ.get('SMTP_USER', ''),
+ 'password': os.environ.get('SMTP_PASS', ''),
+ 'from': os.environ.get('SMTP_FROM', 'reservierung@restaurant.de')
+}
+
+email_templates = {
+ 'confirmation': """Sehr geehrte/r {name},
+
+vielen Dank für Ihre Reservierung bei uns.
+
+Buchungsdetails:
+- Buchungsnummer: {booking_id}
+- Datum: {date}
+- Uhrzeit: {time}
+- Personen: {guests}
+- Tische: {tables}
+
+Wir freuen uns auf Ihren Besuch!
+
+Mit freundlichen Grüßen
+Ihr Restaurant-Team
+""",
+ 'email_reply': """Sehr geehrte/r {name},
+
+vielen Dank für Ihre E-Mail. Wir haben Ihre Reservierungsanfrage erhalten.
+
+{parsed_info}
+
+Ihre Buchungsnummer lautet: {booking_id}
+
+Bei Rückfragen erreichen Sie uns telefonisch oder per E-Mail.
+
+Mit freundlichen Grüßen
+Ihr Restaurant-Team
+""",
+ 'user_welcome': """Sehr geehrte/r {name},
+
+Ihr Account für das Reservierungssystem wurde erstellt.
+
+Zugangsdaten:
+- Benutzername: {username}
+- Passwort: {password}
+- Rolle: {role}
+
+Bitte ändern Sie Ihr Passwort nach dem ersten Login.
+
+Mit freundlichen Grüßen
+Ihr Restaurant-Team
+""",
+ 'password_changed': """Sehr geehrte/r {name},
+
+Ihr Passwort wurde erfolgreich geändert.
+
+Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie bitte umgehend den Administrator.
+
+Mit freundlichen Grüßen
+Ihr Restaurant-Team
+"""
+}
+
+# Datenstruktur
+rooms = [
+ {
+ 'id': 1,
+ 'name': 'Hauptraum',
+ 'description': 'Großer Saal mit Fensterfront',
+ 'tables': [
+ {'id': 1, 'name': 'Tisch 1', 'seats': 4, 'shape': 'circle'},
+ {'id': 2, 'name': 'Tisch 2', 'seats': 2, 'shape': 'circle'},
+ {'id': 3, 'name': 'Tisch 3', 'seats': 6, 'shape': 'rect'},
+ {'id': 4, 'name': 'Tisch 4', 'seats': 4, 'shape': 'rect'},
+ ]
+ },
+ {
+ 'id': 2,
+ 'name': 'Nebenraum',
+ 'description': 'Gemütlicher kleiner Raum',
+ 'tables': [
+ {'id': 5, 'name': 'Tisch A', 'seats': 4, 'shape': 'circle'},
+ {'id': 6, 'name': 'Tisch B', 'seats': 4, 'shape': 'circle'},
+ ]
+ },
+ {
+ 'id': 3,
+ 'name': 'Terrasse',
+ 'description': 'Draußen unter dem Vordach',
+ 'tables': [
+ {'id': 7, 'name': 'Tisch T1', 'seats': 4, 'shape': 'rect'},
+ {'id': 8, 'name': 'Tisch T2', 'seats': 4, 'shape': 'rect'},
+ {'id': 9, 'name': 'Tisch T3', 'seats': 6, 'shape': 'rect'},
+ ]
+ }
+]
+
+reservations = []
+
+def generate_booking_id():
+ while True:
+ booking_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
+ if not any(r.get('booking_id') == booking_id for r in reservations):
+ return booking_id
+
+def get_table(table_id):
+ for room in rooms:
+ for table in room['tables']:
+ if table['id'] == table_id:
+ return table, room
+ return None, None
+
+def get_available_tables(date_str, time_str):
+ available = []
+ for room in rooms:
+ for table in room['tables']:
+ table_copy = table.copy()
+ table_copy['room_id'] = room['id']
+ table_copy['room_name'] = room['name']
+ available.append(table_copy)
+ return available
+
+def is_table_available(table_id, date_str, time_str):
+ for res in reservations:
+ if res['date'] == date_str and res['time'] == time_str:
+ if table_id in res.get('table_ids', []):
+ return False
+ return True
+
+def send_email_smtp(to_email, subject, body):
+ try:
+ import smtplib
+ from email.mime.text import MIMEText
+ from email.mime.multipart import MIMEMultipart
+
+ if not smtp_config['host']:
+ print(f"[SMTP] Kein SMTP konfiguriert. E-Mail würde an {to_email}:")
+ return True
+
+ msg = MIMEMultipart()
+ msg['From'] = smtp_config['from']
+ msg['To'] = to_email
+ msg['Subject'] = subject
+ msg.attach(MIMEText(body, 'plain', 'utf-8'))
+
+ server = smtplib.SMTP(smtp_config['host'], smtp_config['port'])
+ server.starttls()
+ server.login(smtp_config['user'], smtp_config['password'])
+ server.send_message(msg)
+ server.quit()
+ return True
+ except Exception as e:
+ print(f"[SMTP] Fehler: {e}")
+ return False
+
+# === HTML TEMPLATES ===
+
+PUBLIC_RESERVATION_HTML = '''
+
+
+
+
+
+ Reservierung - Restaurant
+
+
+
+
+
+
+
+
+
+
+
🎉
+
Vielen Dank für Ihre Reservierung!
+
Ihre Buchungsnummer:
+
ABC12345
+
Eine Bestätigungsmail wurde an gesendet.
+
+
+
+
+
+
+
+
+'''
+
+# Admin Login mit Passwort-Änderung-Aufforderung
+ADMIN_LOGIN_HTML = '''
+
+
+
+
+
+ Admin Login - Reservierung
+
+
+
+
+
🔐 Admin Login
+
Reservierungssystem
+
+ {% if force_password_change %}
+
+ ⚠️ Sicherheitshinweis: Bitte ändern Sie Ihr Passwort nach dem Login.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+# Passwort-Änderung Seite
+CHANGE_PASSWORD_HTML = '''
+
+
+
+
+
+ Passwort ändern - Reservierung
+
+
+
+
+
🔐 Passwort ändern
+
Sichern Sie Ihr Konto
+
+ {% if force_change %}
+
+ ⚠️ Sicherheitshinweis: Sie müssen Ihr Passwort ändern, bevor Sie fortfahren können.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ - ✓ Mindestens 8 Zeichen
+ - ✓ Großbuchstabe
+ - ✓ Kleinbuchstabe
+ - ✓ Zahl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+ADMIN_DASHBOARD_HTML = '''
+
+
+
+
+
+ Admin Dashboard - Reservierung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🔍 Schnellsuche
+
+
+
+
+
+
+
+
+
+
+
+
+
📋 Alle Buchungen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
👥 Benutzerverwaltung
+
+
+
+
+
+
+
+
+
+
✉️ E-Mail Templates
+
Buchungsbestätigung
+
+
Variablen: {name}, {booking_id}, {date}, {time}, {guests}, {tables}
+
+
E-Mail-Antwort
+
+
+
+
+
+
+
+
+
+
+
+
+
👤 Benutzer hinzufügen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
+# === API ROUTES ===
+
+@app.route('/api/login', methods=['POST'])
+def login():
+ data = request.get_json() or {}
+ username = data.get('username', '').lower()
+ password = data.get('password', '')
+
+ password_hash = hash_password(password)
+
+ user = next((u for u in users if u['username'].lower() == username and
+ u['password_hash'] == password_hash and u.get('is_active', False)), None)
+
+ if user:
+ session['user_id'] = user['id']
+ user['last_login'] = datetime.now().isoformat()
+ return jsonify({
+ 'status': 'ok',
+ 'user': {'id': user['id'], 'name': user['name'], 'role': user['role']},
+ 'force_password_change': user.get('force_password_change', False)
+ })
+
+ return jsonify({'error': 'Invalid username or password'}), 401
+
+@app.route('/api/logout', methods=['POST'])
+def logout():
+ session.pop('user_id', None)
+ return jsonify({'status': 'ok'})
+
+# Passwort ändern
+@app.route('/api/change-password', methods=['GET', 'POST'])
+def change_password():
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ user = next((u for u in users if u['id'] == session['user_id']), None)
+ if not user:
+ return jsonify({'error': 'User not found'}), 404
+
+ if request.method == 'GET':
+ return jsonify({
+ 'force_change': user.get('force_password_change', False)
+ })
+
+ # POST: Passwort ändern
+ data = request.get_json() or {}
+ current_password = data.get('current_password', '')
+ new_password = data.get('new_password', '')
+
+ # Validate current password
+ if hash_password(current_password) != user['password_hash']:
+ return jsonify({'error': 'Aktuelles Passwort ist falsch'}), 400
+
+ # Validate new password
+ if len(new_password) < 8:
+ return jsonify({'error': 'Neues Passwort muss mindestens 8 Zeichen haben'}), 400
+
+ # Update password
+ user['password_hash'] = hash_password(new_password)
+ user['force_password_change'] = False
+
+ # Send confirmation email
+ try:
+ body = email_templates['password_changed'].format(name=user['name'])
+ send_email_smtp(user['email'], 'Ihr Passwort wurde geändert', body)
+ except Exception as e:
+ print(f"Password change email error: {e}")
+
+ return jsonify({'status': 'ok'})
+
+@app.route('/api/me')
+def get_current_user():
+ if 'user_id' not in session:
+ return jsonify({'error': 'Not logged in'}), 401
+
+ user = next((u for u in users if u['id'] == session['user_id']), None)
+ if not user:
+ return jsonify({'error': 'User not found'}), 404
+
+ return jsonify({
+ 'user': {'id': user['id'], 'name': user['name'], 'role': user['role']},
+ 'permissions': ROLE_PERMISSIONS.get(user['role'], [])
+ })
+
+# Public API
+@app.route('/api/public/tables')
+def public_get_tables():
+ date_str = request.args.get('date', str(date.today()))
+ time_str = request.args.get('time', '19:00')
+ available = get_available_tables(date_str, time_str)
+ return jsonify({'tables': available})
+
+@app.route('/api/public/reservations', methods=['POST'])
+def public_create_reservation():
+ global reservations
+ data = request.get_json() or {}
+
+ required = ['name', 'email', 'date', 'time', 'guests', 'table_ids']
+ for field in required:
+ if not data.get(field):
+ return jsonify({'error': f'{field} is required'}), 400
+
+ if not re.match(r'^[^@]+@[^@]+\.[^@]+$', data['email']):
+ return jsonify({'error': 'Invalid email address'}), 400
+
+ booking_id = generate_booking_id()
+
+ table_names = []
+ for table_id in data['table_ids']:
+ table, room = get_table(table_id)
+ if table:
+ table_names.append(f"{room['name']}/{table['name']}" if room else table['name'])
+
+ reservation = {
+ 'id': len(reservations) + 1,
+ 'booking_id': booking_id,
+ 'name': data['name'],
+ 'email': data['email'],
+ 'phone': data.get('phone', ''),
+ 'date': data['date'],
+ 'time': data['time'],
+ 'guests': data['guests'],
+ 'table_ids': data['table_ids'],
+ 'table_names': ', '.join(table_names),
+ 'notes': data.get('notes', ''),
+ 'created_at': datetime.now().isoformat()
+ }
+ reservations.append(reservation)
+
+ try:
+ email_body = email_templates['confirmation'].format(
+ name=data['name'],
+ booking_id=booking_id,
+ date=data['date'],
+ time=data['time'],
+ guests=data['guests'],
+ tables=', '.join(table_names)
+ )
+ send_email_smtp(data['email'], f'Ihre Reservierung - {booking_id}', email_body)
+ except Exception as e:
+ print(f"Email error: {e}")
+
+ return jsonify({'status': 'ok', 'booking_id': booking_id})
+
+# Admin API
+@app.route('/api/admin/reservations')
+def admin_get_reservations():
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+ return jsonify({'reservations': reservations})
+
+@app.route('/api/admin/reservations/search')
+def admin_search_reservations():
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ query = request.args.get('q', '').lower()
+ if not query:
+ return jsonify({'reservations': reservations})
+
+ results = [r for r in reservations if query in r.get('name', '').lower() or
+ query in r.get('email', '').lower() or query in r.get('booking_id', '').lower()]
+ return jsonify({'reservations': results})
+
+# Users API
+@app.route('/api/admin/users')
+def admin_get_users():
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ current = next((u for u in users if u['id'] == session['user_id']), None)
+ if not current or current['role'] != 'admin':
+ return jsonify({'error': 'Forbidden'}), 403
+
+ safe_users = [{'id': u['id'], 'username': u['username'], 'name': u['name'],
+ 'email': u['email'], 'role': u['role'], 'is_active': u.get('is_active', True),
+ 'last_login': u.get('last_login')} for u in users]
+ return jsonify({'users': safe_users})
+
+@app.route('/api/admin/users', methods=['POST'])
+def admin_create_user():
+ global users
+
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ current = next((u for u in users if u['id'] == session['user_id']), None)
+ if not current or current['role'] != 'admin':
+ return jsonify({'error': 'Forbidden'}), 403
+
+ data = request.get_json() or {}
+
+ if not all(k in data for k in ['username', 'name', 'email', 'role']):
+ return jsonify({'error': 'Missing required fields'}), 400
+
+ if any(u['username'].lower() == data['username'].lower() for u in users):
+ return jsonify({'error': 'Username already exists'}), 400
+
+ temp_password = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
+
+ new_user = {
+ 'id': max([u['id'] for u in users], default=0) + 1,
+ 'username': data['username'],
+ 'password_hash': hash_password(temp_password),
+ 'name': data['name'],
+ 'email': data['email'],
+ 'role': data['role'],
+ 'is_active': True,
+ 'force_password_change': True, # Muss Passwort bei erstem Login ändern
+ 'created_at': datetime.now().isoformat(),
+ 'last_login': None
+ }
+ users.append(new_user)
+
+ if data.get('send_email', True):
+ try:
+ body = email_templates['user_welcome'].format(
+ name=data['name'],
+ username=data['username'],
+ password=temp_password,
+ role=data['role']
+ )
+ send_email_smtp(data['email'], 'Ihr Reservierungssystem-Zugang', body)
+ except Exception as e:
+ print(f"Welcome email error: {e}")
+
+ return jsonify({'status': 'ok', 'temp_password': temp_password if not data.get('send_email') else None})
+
+# Rooms, Templates, SMTP API
+@app.route('/api/rooms')
+def get_rooms():
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+ return jsonify({'rooms': rooms})
+
+@app.route('/api/admin/templates', methods=['GET', 'POST'])
+def handle_templates():
+ global email_templates
+
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ if request.method == 'POST':
+ data = request.get_json() or {}
+ if 'confirmation' in data:
+ email_templates['confirmation'] = data['confirmation']
+ if 'email_reply' in data:
+ email_templates['email_reply'] = data['email_reply']
+ return jsonify({'status': 'ok'})
+
+ return jsonify({'templates': email_templates})
+
+@app.route('/api/admin/smtp', methods=['GET', 'POST'])
+def handle_smtp():
+ global smtp_config
+
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ if request.method == 'POST':
+ data = request.get_json() or {}
+ smtp_config.update(data)
+ return jsonify({'status': 'ok'})
+
+ return jsonify({
+ 'host': smtp_config.get('host', ''),
+ 'port': smtp_config.get('port', 587),
+ 'user': smtp_config.get('user', ''),
+ 'from': smtp_config.get('from', '')
+ })
+
+@app.route('/api/admin/smtp/test', methods=['POST'])
+def test_smtp():
+ if 'user_id' not in session:
+ return jsonify({'error': 'Unauthorized'}), 401
+
+ result = send_email_smtp(
+ smtp_config.get('user', 'test@example.com'),
+ 'SMTP Test',
+ 'Dies ist eine Testnachricht.'
+ )
+ if result:
+ return jsonify({'status': 'ok'})
+ return jsonify({'error': 'SMTP test failed'}), 500
+
+# Main routes
+@app.route('/')
+def index():
+ if 'user_id' in session:
+ return redirect('/admin')
+ return render_template_string(PUBLIC_RESERVATION_HTML, min_date=str(date.today()))
+
+@app.route('/admin')
+def admin():
+ if 'user_id' not in session:
+ return render_template_string(ADMIN_LOGIN_HTML,
+ default_username='admin',
+ default_password='changeme',
+ force_password_change=False)
+
+ user = next((u for u in users if u['id'] == session['user_id']), None)
+ if not user:
+ session.pop('user_id', None)
+ return redirect('/admin')
+
+ return render_template_string(ADMIN_DASHBOARD_HTML,
+ template_confirmation=email_templates['confirmation'],
+ template_email_reply=email_templates['email_reply'],
+ smtp_host=smtp_config.get('host', ''),
+ smtp_port=smtp_config.get('port', 587),
+ smtp_user=smtp_config.get('user', ''),
+ smtp_password='***' if smtp_config.get('password') else '',
+ smtp_from=smtp_config.get('from', '')
+ )
+
+@app.route('/change-password')
+def change_password_page():
+ if 'user_id' not in session:
+ return redirect('/')
+
+ user = next((u for u in users if u['id'] == session['user_id']), None)
+ if not user:
+ session.pop('user_id', None)
+ return redirect('/')
+
+ return render_template_string(CHANGE_PASSWORD_HTML,
+ force_change=user.get('force_password_change', False))
+
+if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=8080)