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 } ] 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} - Raum: {room} 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 - Räume mit Kapazität und Block-Status rooms = [ { 'id': 1, 'name': 'Hauptraum', 'description': 'Großer Saal mit Fensterfront', 'capacity': 40, 'blocked': False, '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', 'capacity': 20, 'blocked': False, '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', 'capacity': 15, 'blocked': False, '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_room(room_id): for room in rooms: if room['id'] == room_id: return room return None 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: if room.get('blocked', False): continue for table in room['tables']: table_copy = table.copy() table_copy['room_id'] = room['id'] table_copy['room_name'] = room['name'] if is_table_available(table['id'], date_str, time_str): 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 auto_assign_tables(room_id, guests, date_str, time_str): """Automatische Tisch-Zuweisung basierend auf Raum und Gästeanzahl""" room = get_room(room_id) if not room: return None, "Raum nicht gefunden" if room.get('blocked', False): return None, "Raum ist derzeit nicht verfügbar" # Alle verfügbaren Tische im Raum available_tables = [] for table in room['tables']: if is_table_available(table['id'], date_str, time_str): available_tables.append(table) if not available_tables: return None, "Keine Tische im Raum verfügbar" # Sortiere nach Sitzplätzen (größte zuerst) available_tables.sort(key=lambda t: t['seats'], reverse=True) # Finde beste Kombination für Gäste total_seats = sum(t['seats'] for t in available_tables) if total_seats < guests: return None, f"Nicht genügend Platz im Raum. Verfügbar: {total_seats} Plätze" # Wähle Tische aus (greedy - erst große, dann kleine) selected_tables = [] remaining_guests = guests for table in available_tables: if remaining_guests <= 0: break selected_tables.append(table) remaining_guests -= table['seats'] return [t['id'] for t in selected_tables], None 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

🍽️ Tischreservierung

Reservieren Sie Ihren Tisch einfach online

📅 Wann möchten Sie reservieren?

👥 Wie viele Personen?

🏠 Welcher Raum?

Bitte wählen Sie einen Raum aus

👤 Ihre Kontaktdaten

''' # Admin HTML Templates... ADMIN_LOGIN_HTML = ''' Admin Login - Reservierung

🔐 Admin Login

Reservierungssystem

{% if force_password_change %}
⚠️ Sicherheitshinweis: Bitte ändern Sie Ihr Passwort nach dem Login.
{% endif %}
''' 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 %}
''' ADMIN_DASHBOARD_HTML = ''' Admin Dashboard - Reservierung

📊 Reservierung Admin

Role
0
Gesamtbuchungen
0
Heute
0
Diese Woche
0
Räume

🔍 Schnellsuche

''' # === 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) }) data = request.get_json() or {} current_password = data.get('current_password', '') new_password = data.get('new_password', '') if hash_password(current_password) != user['password_hash']: return jsonify({'error': 'Aktuelles Passwort ist falsch'}), 400 if len(new_password) < 8: return jsonify({'error': 'Neues Passwort muss mindestens 8 Zeichen haben'}), 400 user['password_hash'] = hash_password(new_password) user['force_password_change'] = False 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 - Räume @app.route('/api/public/rooms') def public_get_rooms(): """Öffentliche Raum-Liste (nicht blockierte Räume)""" available = [] for room in rooms: if not room.get('blocked', False): available.append({ 'id': room['id'], 'name': room['name'], 'description': room.get('description', ''), 'capacity': room.get('capacity', 0), 'tables': room['tables'] }) return jsonify({'rooms': available}) # Public API - Reservierung @app.route('/api/public/reservations', methods=['POST']) def public_create_reservation(): global reservations data = request.get_json() or {} # NEU: room_id statt table_ids required = ['name', 'email', 'date', 'time', 'guests', 'room_id'] 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 room_id = int(data['room_id']) guests = data['guests'] date_str = data['date'] time_str = data['time'] # Raum validieren room = get_room(room_id) if not room: return jsonify({'error': 'Ungültiger Raum'}), 400 if room.get('blocked', False): return jsonify({'error': 'Raum ist derzeit nicht verfügbar'}), 400 # Kapazität prüfen if guests > room.get('capacity', 0): return jsonify({'error': f'Zu viele Gäste für diesen Raum (max. {room["capacity"]})'}), 400 # Automatische Tisch-Zuweisung table_ids, error = auto_assign_tables(room_id, guests, date_str, time_str) if error: return jsonify({'error': error}), 400 booking_id = generate_booking_id() table_names = [] for table_id in table_ids: table, _ = get_table(table_id) if table: table_names.append(table['name']) reservation = { 'id': len(reservations) + 1, 'booking_id': booking_id, 'name': data['name'], 'email': data['email'], 'phone': data.get('phone', ''), 'date': date_str, 'time': time_str, 'guests': guests, 'room_id': room_id, 'room_name': room['name'], 'table_ids': 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=date_str, time=time_str, guests=guests, room=room['name'] ) 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 - Reservierungen @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}) # Admin API - Users @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, '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}) # Admin API - Räume (NEU) @app.route('/api/admin/rooms') def admin_get_rooms(): """Liste aller Räume für Admins""" if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'rooms': rooms}) @app.route('/api/admin/rooms', methods=['POST']) def admin_create_room(): global rooms if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 data = request.get_json() or {} if not data.get('name'): return jsonify({'error': 'Name erforderlich'}), 400 capacity = data.get('capacity', 0) if capacity < 1: return jsonify({'error': 'Kapazität muss mindestens 1 sein'}), 400 new_room = { 'id': max([r['id'] for r in rooms], default=0) + 1, 'name': data['name'], 'description': data.get('description', ''), 'capacity': capacity, 'blocked': False, 'tables': [] } rooms.append(new_room) return jsonify({'status': 'ok', 'room': new_room}) @app.route('/api/admin/rooms/', methods=['PUT']) def admin_update_room(room_id): global rooms if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 room = get_room(room_id) if not room: return jsonify({'error': 'Raum nicht gefunden'}), 404 data = request.get_json() or {} if data.get('name'): room['name'] = data['name'] if 'description' in data: room['description'] = data['description'] if 'capacity' in data: capacity = data['capacity'] if capacity < 1: return jsonify({'error': 'Kapazität muss mindestens 1 sein'}), 400 room['capacity'] = capacity return jsonify({'status': 'ok', 'room': room}) @app.route('/api/admin/rooms/', methods=['DELETE']) def admin_delete_room(room_id): global rooms, reservations if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 room = get_room(room_id) if not room: return jsonify({'error': 'Raum nicht gefunden'}), 404 # Prüfe auf aktive Reservierungen für diesen Raum active_reservations = [r for r in reservations if r.get('room_id') == room_id] if active_reservations: return jsonify({'error': f'Raum hat {len(active_reservations)} aktive Reservierungen'}), 400 rooms = [r for r in rooms if r['id'] != room_id] return jsonify({'status': 'ok'}) @app.route('/api/admin/rooms//tables', methods=['POST']) def admin_add_table(room_id): global rooms if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 room = get_room(room_id) if not room: return jsonify({'error': 'Raum nicht gefunden'}), 404 data = request.get_json() or {} if not data.get('name'): return jsonify({'error': 'Tischname erforderlich'}), 400 seats = data.get('seats', 0) if seats < 1: return jsonify({'error': 'Plätze müssen mindestens 1 sein'}), 400 # Generiere eindeutige Tisch-ID über alle Räume max_table_id = 0 for r in rooms: for t in r['tables']: max_table_id = max(max_table_id, t['id']) new_table = { 'id': max_table_id + 1, 'name': data['name'], 'seats': seats, 'shape': data.get('shape', 'rect') } room['tables'].append(new_table) return jsonify({'status': 'ok', 'table': new_table}) @app.route('/api/admin/rooms//tables/', methods=['DELETE']) def admin_delete_table(room_id, table_id): global rooms if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 room = get_room(room_id) if not room: return jsonify({'error': 'Raum nicht gefunden'}), 404 # Prüfe ob Tisch in Reservierungen verwendet wird for res in reservations: if table_id in res.get('table_ids', []): return jsonify({'error': 'Tisch ist in einer Reservierung'}), 400 room['tables'] = [t for t in room['tables'] if t['id'] != table_id] return jsonify({'status': 'ok'}) @app.route('/api/admin/rooms//block', methods=['POST']) def admin_block_room(room_id): global rooms if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 room = get_room(room_id) if not room: return jsonify({'error': 'Raum nicht gefunden'}), 404 room['blocked'] = True return jsonify({'status': 'ok'}) @app.route('/api/admin/rooms//unblock', methods=['POST']) def admin_unblock_room(room_id): global rooms if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 room = get_room(room_id) if not room: return jsonify({'error': 'Raum nicht gefunden'}), 404 room['blocked'] = False return jsonify({'status': 'ok'}) # Rooms API (für Admin-Dashboard) @app.route('/api/rooms') def get_rooms(): if 'user_id' not in session: return jsonify({'error': 'Unauthorized'}), 401 return jsonify({'rooms': rooms}) # Templates & SMTP @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)