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 + + + +
+
+

🍽️ Tischreservierung

+

Reservieren Sie Ihren Tisch einfach online

+
+ +
+
+

📅 Wann möchten Sie reservieren?

+
+ + +
+
+ + +
+
+ +
+

👥 Wie viele Personen?

+
+ + +
+
+ +
+

🪑 Welche Tische?

+
+

Bitte zuerst Datum und Uhrzeit wählen...

+
+
+ +
+

👤 Ihre Kontaktdaten

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

🤖 Sicherheitsprüfung

+ +
+ +
+ +
+
+ + +
+ + + + +''' + +# 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 %} + +
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + +
+
+ + + + +''' + +ADMIN_DASHBOARD_HTML = ''' + + + + + + Admin Dashboard - Reservierung + + + +
+

📊 Reservierung Admin

+
+ + Role +
+
+ + + +
+ +
+
+
+
0
+
Gesamtbuchungen
+
+
+
0
+
Heute
+
+
+
0
+
Diese Woche
+
+
+
0
+
Benutzer
+
+
+ +
+
+

🔍 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) + }) + + # 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)