Files
reservierungssystem/app.py
T

1492 lines
62 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '''
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reservierung - Restaurant</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem 1rem;
}
.container { max-width: 800px; margin: 0 auto; }
.header { text-align: center; margin-bottom: 2rem; color: white; }
.header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
.card {
background: white;
border-radius: 20px;
padding: 2rem;
margin-bottom: 1.5rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.card h2 { color: #333; margin-bottom: 1.5rem; font-size: 1.5rem; }
.form-group { margin-bottom: 1.25rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #555; }
.form-group label .required { color: #ef4444; }
.form-group input, .form-group select {
width: 100%; padding: 1rem; border: 2px solid #e0e0e0;
border-radius: 10px; font-size: 1rem;
}
.form-group input:focus { outline: none; border-color: #667eea; }
.checkbox-group {
display: flex; align-items: center; gap: 0.75rem;
padding: 1rem; background: #f8f9fa; border-radius: 10px;
border: 2px solid #e0e0e0; cursor: pointer;
}
.checkbox-group input[type="checkbox"] { width: 24px; height: 24px; }
.table-selection {
display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.75rem;
}
.table-option {
border: 2px solid #e0e0e0; border-radius: 12px; padding: 1rem;
text-align: center; cursor: pointer; transition: all 0.2s;
}
.table-option.selected { border-color: #667eea; background: #f0f4ff; }
.btn {
width: 100%; padding: 1.25rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 10px;
font-size: 1.1rem; font-weight: 600; cursor: pointer;
}
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.success-message { text-align: center; padding: 3rem 2rem; }
.booking-id {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; padding: 1rem 2rem; border-radius: 10px;
font-size: 1.5rem; font-weight: bold; margin: 1rem 0; letter-spacing: 2px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🍽️ Tischreservierung</h1>
<p>Reservieren Sie Ihren Tisch einfach online</p>
</div>
<div id="reservation-form">
<div class="card">
<h2>📅 Wann möchten Sie reservieren?</h2>
<div class="form-group">
<label>Datum <span class="required">*</span></label>
<input type="date" id="res-date" min="{{ min_date }}">
</div>
<div class="form-group">
<label>Uhrzeit <span class="required">*</span></label>
<select id="res-time">
<option value="">Bitte wählen...</option>
<option value="11:00">11:00</option>
<option value="12:00">12:00</option>
<option value="13:00">13:00</option>
<option value="18:00">18:00</option>
<option value="19:00" selected>19:00</option>
<option value="20:00">20:00</option>
<option value="21:00">21:00</option>
</select>
</div>
</div>
<div class="card">
<h2>👥 Wie viele Personen?</h2>
<div class="form-group">
<label>Anzahl Personen <span class="required">*</span></label>
<input type="number" id="res-guests" value="2" min="1" max="50">
</div>
</div>
<div class="card">
<h2>🪑 Welcher Raum?</h2>
<div class="form-group" id="group-room">
<label>Raum wählen <span class="required">*</span></label>
<select id="res-room" onchange="checkRoomCapacity()">
<option value="">Bitte wählen...</option>
<option value="1">Saal (bis 40 Personen)</option>
<option value="2">Nebenzimmer (bis 20 Personen)</option>
<option value="3">Wintergarten (bis 15 Personen)</option>
</select>
<div class="error-msg" id="room-error" style="display: none; color: #ef4444; font-size: 0.85rem; margin-top: 0.5rem;">Bitte wählen Sie einen Raum aus</div>
<div id="room-info" style="margin-top: 0.5rem; color: #667eea; font-size: 0.9rem;"></div>
</div>
</div>
<div class="card">
<h2>👤 Ihre Kontaktdaten</h2>
<div class="form-group">
<label>Name <span class="required">*</span></label>
<input type="text" id="res-name" placeholder="Ihr vollständiger Name">
</div>
<div class="form-group">
<label>E-Mail <span class="required">*</span></label>
<input type="email" id="res-email" placeholder="ihre@email.de">
</div>
<div class="form-group">
<label>Telefon (optional)</label>
<input type="tel" id="res-phone" placeholder="+49 123 456789">
</div>
<div class="form-group">
<label>Sonderwünsche (optional)</label>
<input type="text" id="res-notes" placeholder="z.B. Hochstuhl, Allergien...">
</div>
</div>
<div class="card">
<h2>🤖 Sicherheitsprüfung</h2>
<label class="checkbox-group" onclick="toggleCaptcha()">
<input type="checkbox" id="captcha-check">
<label for="captcha-check">Ich bin kein Roboter</label>
</label>
</div>
<div class="card" style="background: transparent; box-shadow: none; padding: 0;">
<button class="btn" id="submit-btn" onclick="submitReservation()" disabled>
📧 Reservierung mit Bestätigungsmail senden
</button>
</div>
</div>
<div id="success-message" class="card hidden">
<div class="success-message">
<div style="font-size: 4rem; margin-bottom: 1rem;">🎉</div>
<h2>Vielen Dank für Ihre Reservierung!</h2>
<p>Ihre Buchungsnummer:</p>
<div class="booking-id" id="success-booking-id">ABC12345</div>
<p>Eine Bestätigungsmail wurde an <strong id="success-email"></strong> gesendet.</p>
<button class="btn" style="margin-top: 2rem;" onclick="location.reload()">Neue Reservierung</button>
</div>
</div>
</div>
<script>
let selectedTables = [];
document.getElementById('res-date').valueAsDate = new Date();
document.getElementById('res-date').min = new Date().toISOString().split('T')[0];
document.getElementById('res-date').addEventListener('change', loadAvailableTables);
document.getElementById('res-time').addEventListener('change', loadAvailableTables);
async function loadAvailableTables() {
const date = document.getElementById('res-date').value;
const time = document.getElementById('res-time').value;
if (!date || !time) return;
const res = await fetch(`/api/public/tables?date=${date}&time=${time}`);
const data = await res.json();
const container = document.getElementById('available-tables');
if (data.tables.length === 0) {
container.innerHTML = '<p style="color: #ef4444; text-align: center; padding: 2rem;">😔 Leider sind keine Tische mehr verfügbar.</p>';
return;
}
container.innerHTML = '<div class="table-selection">' +
data.tables.map(t => `
<div class="table-option" onclick="toggleTable(${t.id})" id="table-${t.id}">
<input type="checkbox" style="margin-bottom: 0.5rem;" onclick="event.stopPropagation()">
<div><strong>${t.name}</strong></div>
<div style="font-size: 0.85rem; color: #666;">${t.seats} Plätze</div>
</div>
`).join('') + '</div>';
}
function toggleTable(tableId) {
const index = selectedTables.indexOf(tableId);
if (index > -1) selectedTables.splice(index, 1);
else selectedTables.push(tableId);
document.querySelectorAll('.table-option').forEach(el => el.classList.remove('selected'));
selectedTables.forEach(id => document.getElementById('table-' + id).classList.add('selected'));
}
function toggleCaptcha() {
const captcha = document.getElementById('captcha-check').checked;
document.getElementById('submit-btn').disabled = !captcha || selectedTables.length === 0;
}
async function submitReservation() {
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = '⏳ Wird gespeichert...';
const data = {
name: document.getElementById('res-name').value,
email: document.getElementById('res-email').value,
phone: document.getElementById('res-phone').value,
date: document.getElementById('res-date').value,
time: document.getElementById('res-time').value,
guests: parseInt(document.getElementById('res-guests').value),
table_ids: selectedTables,
notes: document.getElementById('res-notes').value
};
if (!data.name || !data.email || selectedTables.length === 0) {
alert('Bitte füllen Sie alle Pflichtfelder aus.');
btn.disabled = false;
return;
}
const res = await fetch('/api/public/reservations', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.status === 'ok') {
document.getElementById('reservation-form').classList.add('hidden');
document.getElementById('success-message').classList.remove('hidden');
document.getElementById('success-booking-id').textContent = result.booking_id;
document.getElementById('success-email').textContent = data.email;
} else {
alert('Fehler: ' + result.error);
btn.disabled = false;
}
}
</script>
</body>
</html>
'''
# Admin Login mit Passwort-Änderung-Aufforderung
ADMIN_LOGIN_HTML = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - Reservierung</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; display: flex; justify-content: center; align-items: center; }
.login-box { background: white; padding: 3rem; border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3); width: 100%; max-width: 400px; }
h1 { color: #333; margin-bottom: 0.5rem; text-align: center; }
.form-group { margin-bottom: 1rem; }
label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #555; }
input { width: 100%; padding: 1rem; border: 2px solid #e0e0e0;
border-radius: 10px; font-size: 1rem; }
button { width: 100%; padding: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.error { color: #ef4444; text-align: center; margin-top: 1rem; }
.warning { background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.9rem; }
</style>
</head>
<body>
<div class="login-box">
<h1>🔐 Admin Login</h1>
<p style="color: #666; margin-bottom: 2rem; text-align: center;">Reservierungssystem</p>
{% if force_password_change %}
<div class="warning">
⚠️ <strong>Sicherheitshinweis:</strong> Bitte ändern Sie Ihr Passwort nach dem Login.
</div>
{% endif %}
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="username" placeholder="Benutzername" value="{{ default_username }}">
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="password" placeholder="Passwort" value="{{ default_password }}">
</div>
<button onclick="login()">Anmelden</button>
<div id="error-msg" class="error"></div>
</div>
<script>
async function login() {
const res = await fetch('/api/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value
})
});
const data = await res.json();
if (data.status === 'ok') {
if (data.force_password_change) {
location.href = '/change-password';
} else {
location.href = '/admin';
}
} else {
document.getElementById('error-msg').textContent = data.error || 'Login fehlgeschlagen';
}
}
document.getElementById('password').addEventListener('keypress', e => { if (e.key === 'Enter') login(); });
</script>
</body>
</html>
'''
# Passwort-Änderung Seite
CHANGE_PASSWORD_HTML = '''
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort ändern - Reservierung</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; display: flex; justify-content: center; align-items: center; }
.box { background: white; padding: 3rem; border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3); width: 100%; max-width: 450px; }
h1 { color: #333; margin-bottom: 0.5rem; text-align: center; }
.subtitle { color: #666; text-align: center; margin-bottom: 2rem; }
.form-group { margin-bottom: 1.25rem; }
label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #555; }
input { width: 100%; padding: 1rem; border: 2px solid #e0e0e0;
border-radius: 10px; font-size: 1rem; }
input:focus { outline: none; border-color: #667eea; }
button { width: 100%; padding: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
.error { color: #ef4444; text-align: center; margin-top: 1rem; }
.success { color: #10b981; text-align: center; margin-top: 1rem; }
.info { background: #f0f4ff; border: 1px solid #667eea; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; font-size: 0.9rem; color: #4338ca; }
.requirements { font-size: 0.85rem; color: #666; margin-top: 0.5rem; }
.requirements li { margin-bottom: 0.25rem; }
</style>
</head>
<body>
<div class="box">
<h1>🔐 Passwort ändern</h1>
<p class="subtitle">Sichern Sie Ihr Konto</p>
{% if force_change %}
<div class="info">
⚠️ <strong>Sicherheitshinweis:</strong> Sie müssen Ihr Passwort ändern, bevor Sie fortfahren können.
</div>
{% endif %}
<div class="form-group">
<label>Aktuelles Passwort</label>
<input type="password" id="current-password" placeholder="Ihr aktuelles Passwort">
</div>
<div class="form-group">
<label>Neues Passwort</label>
<input type="password" id="new-password" placeholder="Mindestens 8 Zeichen">
</div>
<ul class="requirements">
<li id="req-length">✓ Mindestens 8 Zeichen</li>
<li id="req-upper">✓ Großbuchstabe</li>
<li id="req-lower">✓ Kleinbuchstabe</li>
<li id="req-number">✓ Zahl</li>
</ul>
<div class="form-group" style="margin-top: 1rem;">
<label>Neues Passwort wiederholen</label>
<input type="password" id="confirm-password" placeholder="Passwort bestätigen">
</div>
<button id="submit-btn" onclick="changePassword()">Passwort speichern</button>
<div id="message"></div>
</div>
<script>
function validatePassword() {
const pwd = document.getElementById('new-password').value;
document.getElementById('req-length').style.color = pwd.length >= 8 ? '#10b981' : '#ef4444';
document.getElementById('req-length').textContent = (pwd.length >= 8 ? '' : '') + ' Mindestens 8 Zeichen';
document.getElementById('req-upper').style.color = /[A-Z]/.test(pwd) ? '#10b981' : '#ef4444';
document.getElementById('req-upper').textContent = (/[A-Z]/.test(pwd) ? '' : '') + ' Großbuchstabe';
document.getElementById('req-lower').style.color = /[a-z]/.test(pwd) ? '#10b981' : '#ef4444';
document.getElementById('req-lower').textContent = (/[a-z]/.test(pwd) ? '' : '') + ' Kleinbuchstabe';
document.getElementById('req-number').style.color = /[0-9]/.test(pwd) ? '#10b981' : '#ef4444';
document.getElementById('req-number').textContent = (/[0-9]/.test(pwd) ? '' : '') + ' Zahl';
}
document.getElementById('new-password').addEventListener('input', validatePassword);
async function changePassword() {
const current = document.getElementById('current-password').value;
const newPwd = document.getElementById('new-password').value;
const confirm = document.getElementById('confirm-password').value;
if (newPwd !== confirm) {
document.getElementById('message').innerHTML = '<div class="error">Die Passwörter stimmen nicht überein.</div>';
return;
}
if (newPwd.length < 8) {
document.getElementById('message').innerHTML = '<div class="error">Das neue Passwort muss mindestens 8 Zeichen haben.</div>';
return;
}
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = 'Speichere...';
const res = await fetch('/api/change-password', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
current_password: current,
new_password: newPwd
})
});
const data = await res.json();
if (data.status === 'ok') {
document.getElementById('message').innerHTML = '<div class="success">✅ Passwort erfolgreich geändert! Weiterleitung...</div>';
setTimeout(() => location.href = '/admin', 1500);
} else {
document.getElementById('message').innerHTML = '<div class="error">' + (data.error || 'Fehler beim Ändern des Passworts') + '</div>';
btn.disabled = false;
btn.textContent = 'Passwort speichern';
}
}
</script>
</body>
</html>
'''
ADMIN_DASHBOARD_HTML = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - Reservierung</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #f5f7fa; min-height: 100vh; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;
padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 1.5rem; }
.user-info { display: flex; align-items: center; gap: 1rem; }
.role-badge { background: rgba(255,255,255,0.2); padding: 0.25rem 0.75rem; border-radius: 4px; font-size: 0.8rem; text-transform: uppercase; }
.nav { background: white; padding: 1rem 2rem; border-bottom: 1px solid #e0e0e0; display: flex; gap: 0.5rem; flex-wrap: wrap; }
.nav-btn { padding: 0.5rem 1rem; border: none; background: #f0f0f0; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
.nav-btn.active { background: #667eea; color: white; }
.nav-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.container { max-width: 1400px; margin: 2rem auto; padding: 0 1rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1.5rem; }
.card { background: white; border-radius: 16px; padding: 1.5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.card h2 { color: #333; margin-bottom: 1rem; font-size: 1.25rem; }
.btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;
padding: 0.75rem 1.5rem; border-radius: 8px; cursor: pointer; }
.btn-secondary { background: #6b7280; }
.btn-danger { background: #ef4444; }
.btn-sm { padding: 0.5rem 1rem; font-size: 0.85rem; }
.search-box { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.search-box input { flex: 1; padding: 0.75rem; border: 1px solid #ddd; border-radius: 8px; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #e0e0e0; }
.table th { font-weight: 600; color: #555; }
.badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
.badge-admin { background: #ef4444; color: white; }
.badge-manager { background: #f59e0b; color: white; }
.badge-staff { background: #10b981; color: white; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: #555; }
.form-group input, .form-group select { width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 8px; }
textarea { width: 100%; min-height: 150px; padding: 0.75rem; border: 1px solid #ddd; border-radius: 8px; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center; }
.modal-overlay.active { display: flex; }
.modal { background: white; border-radius: 16px; padding: 2rem; width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; }
.modal h2 { margin-bottom: 1.5rem; }
.hidden { display: none !important; }
.alert { padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
.alert.success { background: #d1fae5; color: #065f46; }
.alert.error { background: #fee2e2; color: #991b1b; }
.permission-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
.permission-tag { background: #e0e7ff; color: #4338ca; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; }
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
.stat-card { background: white; border-radius: 12px; padding: 1.5rem; text-align: center; }
.stat-value { font-size: 2rem; font-weight: bold; color: #667eea; }
.stat-label { color: #666; font-size: 0.9rem; margin-top: 0.5rem; }
.dropdown { position: relative; display: inline-block; }
.dropdown-content { display: none; position: absolute; right: 0; background: white; min-width: 200px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1); border-radius: 8px; z-index: 100; }
.dropdown-content a { color: #333; padding: 0.75rem 1rem; text-decoration: none; display: block; border-bottom: 1px solid #f0f0f0; }
.dropdown-content a:hover { background: #f8f9fa; }
.dropdown-content a:last-child { border-bottom: none; }
.dropdown:hover .dropdown-content { display: block; }
@media (max-width: 768px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
</style>
</head>
<body>
<div class="header">
<h1>📊 Reservierung Admin</h1>
<div class="user-info">
<div class="dropdown">
<span id="current-user-name" style="cursor: pointer;">User ▼</span>
<div class="dropdown-content">
<a href="/change-password">🔐 Passwort ändern</a>
<a href="#" onclick="logout(); return false;">🚪 Abmelden</a>
</div>
</div>
<span id="current-user-role" class="role-badge">Role</span>
</div>
</div>
<div class="nav">
<button class="nav-btn active" onclick="showTab('dashboard')">📈 Dashboard</button>
<button class="nav-btn" onclick="showTab('reservations')">📋 Buchungen</button>
<button class="nav-btn" onclick="showTab('rooms')">🏠 Räume</button>
<button class="nav-btn" id="btn-users" onclick="showTab('users')">👥 Benutzer</button>
<button class="nav-btn" id="btn-templates" onclick="showTab('templates')">✉️ Templates</button>
<button class="nav-btn" id="btn-settings" onclick="showTab('settings')">⚙️ Einstellungen</button>
</div>
<div class="container">
<!-- Dashboard -->
<div id="tab-dashboard" class="tab-content">
<div class="stats-grid" style="margin-bottom: 2rem;">
<div class="stat-card">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">Gesamtbuchungen</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-today">0</div>
<div class="stat-label">Heute</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-week">0</div>
<div class="stat-label">Diese Woche</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-users">0</div>
<div class="stat-label">Benutzer</div>
</div>
</div>
<div class="grid">
<div class="card">
<h2>🔍 Schnellsuche</h2>
<div class="search-box">
<input type="text" id="quick-search" placeholder="Name, E-Mail oder Buchungsnummer...">
<button class="btn" onclick="doQuickSearch()">Suchen</button>
</div>
<div id="quick-results"></div>
</div>
</div>
</div>
<!-- Reservations -->
<div id="tab-reservations" class="tab-content hidden">
<div class="card">
<h2>📋 Alle Buchungen</h2>
<div class="search-box">
<input type="text" id="res-search" placeholder="Suchen...">
<button class="btn" onclick="searchReservations()">🔍 Suchen</button>
<button class="btn btn-secondary" onclick="resetResSearch()">❌ Reset</button>
</div>
<div id="reservations-list"></div>
</div>
</div>
<!-- Rooms -->
<div id="tab-rooms" class="tab-content hidden">
<div class="grid" id="rooms-list"></div>
</div>
<!-- Users -->
<div id="tab-users" class="tab-content hidden">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2>👥 Benutzerverwaltung</h2>
<button class="btn" onclick="showAddUserModal()"> Benutzer hinzufügen</button>
</div>
<div id="users-list"></div>
</div>
</div>
<!-- Templates -->
<div id="tab-templates" class="tab-content hidden">
<div class="card">
<h2>✉️ E-Mail Templates</h2>
<h3 style="margin-top: 1rem;">Buchungsbestätigung</h3>
<textarea id="template-confirmation">{{ template_confirmation }}</textarea>
<p style="font-size: 0.85rem; color: #666; margin: 0.5rem 0;">Variablen: {name}, {booking_id}, {date}, {time}, {guests}, {tables}</p>
<h3 style="margin-top: 1rem;">E-Mail-Antwort</h3>
<textarea id="template-email-reply">{{ template_email_reply }}</textarea>
<button class="btn" style="margin-top: 1rem;" onclick="saveTemplates()">💾 Speichern</button>
</div>
</div>
<!-- Settings -->
<div id="tab-settings" class="tab-content hidden">
<div class="card">
<h2>⚙️ SMTP-Einstellungen</h2>
<div class="form-group">
<label>SMTP-Server</label>
<input type="text" id="smtp-host" value="{{ smtp_host }}" placeholder="smtp.gmail.com">
</div>
<div class="form-group">
<label>Port</label>
<input type="number" id="smtp-port" value="{{ smtp_port }}" placeholder="587">
</div>
<div class="form-group">
<label>Benutzername</label>
<input type="text" id="smtp-user" value="{{ smtp_user }}" placeholder="email@gmail.com">
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" id="smtp-pass" value="{{ smtp_password }}" placeholder="App-Passwort">
</div>
<div class="form-group">
<label>Absender</label>
<input type="email" id="smtp-from" value="{{ smtp_from }}" placeholder="reservierung@restaurant.de">
</div>
<button class="btn" onclick="saveSMTP()">💾 Speichern</button>
<button class="btn btn-secondary" style="margin-left: 0.5rem;" onclick="testSMTP()">📧 Testmail</button>
</div>
</div>
</div>
<!-- Add User Modal -->
<div id="user-modal" class="modal-overlay" onclick="if(event.target===this)closeUserModal()">
<div class="modal">
<h2>👤 Benutzer hinzufügen</h2>
<div class="form-group">
<label>Benutzername *</label>
<input type="text" id="new-user-username" placeholder="max.mustermann">
</div>
<div class="form-group">
<label>Name *</label>
<input type="text" id="new-user-name" placeholder="Max Mustermann">
</div>
<div class="form-group">
<label>E-Mail *</label>
<input type="email" id="new-user-email" placeholder="max@restaurant.de">
</div>
<div class="form-group">
<label>Rolle *</label>
<select id="new-user-role">
<option value="staff">Mitarbeiter (nur Buchungen)</option>
<option value="manager">Manager (Buchungen + Räume)</option>
<option value="admin">Administrator (alle Rechte)</option>
</select>
</div>
<div id="user-role-info" style="background: #f0f4ff; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.9rem;">
<strong>Berechtigungen:</strong>
<div class="permission-list" id="role-permissions"></div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="new-user-send-email" checked>
Willkommens-E-Mail mit Passwort senden
</label>
</div>
<div style="display: flex; gap: 0.5rem;">
<button class="btn" onclick="createUser()">💾 Erstellen</button>
<button class="btn btn-secondary" onclick="closeUserModal()">❌ Abbrechen</button>
</div>
</div>
</div>
<script>
let currentUser = null;
let allReservations = [];
loadCurrentUser();
loadReservations();
document.getElementById('new-user-role').addEventListener('change', updateRoleInfo);
async function loadCurrentUser() {
const res = await fetch('/api/me');
const data = await res.json();
if (data.user) {
currentUser = data.user;
document.getElementById('current-user-name').textContent = data.user.name + '';
document.getElementById('current-user-role').textContent = data.user.role;
if (!data.permissions.includes('users')) {
document.getElementById('btn-users').style.display = 'none';
}
if (!data.permissions.includes('templates')) {
document.getElementById('btn-templates').style.display = 'none';
}
if (!data.permissions.includes('settings')) {
document.getElementById('btn-settings').style.display = 'none';
}
}
}
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.getElementById('tab-' + tab).classList.remove('hidden');
document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
event.target.classList.add('active');
if (tab === 'reservations') loadReservations();
if (tab === 'rooms') loadRooms();
if (tab === 'users') loadUsers();
}
async function logout() {
await fetch('/api/logout', {method: 'POST'});
location.href = '/';
}
// Search, Reservations, Rooms, Users functions...
async function doQuickSearch() {
const query = document.getElementById('quick-search').value;
if (!query) return;
await searchReservations(query);
showTab('reservations');
}
async function searchReservations(query = null) {
query = query || document.getElementById('res-search').value;
const res = await fetch('/api/admin/reservations/search?q=' + encodeURIComponent(query));
const data = await res.json();
currentReservations = data.reservations;
renderReservations(currentReservations);
}
async function resetResSearch() {
document.getElementById('res-search').value = '';
await loadReservations();
}
async function loadReservations() {
const res = await fetch('/api/admin/reservations');
const data = await res.json();
currentReservations = data.reservations;
renderReservations(currentReservations);
updateStats(data.reservations);
}
function renderReservations(reservations) {
const container = document.getElementById('reservations-list');
if (reservations.length === 0) {
container.innerHTML = '<p style="color: #999; text-align: center; padding: 2rem;">Keine Buchungen</p>';
return;
}
container.innerHTML = `
<table class="table">
<thead>
<tr>
<th>Buchungsnr.</th>
<th>Name</th>
<th>Datum/Zeit</th>
<th>Personen</th>
</tr>
</thead>
<tbody>
${reservations.map(r => `
<tr>
<td><code style="background: #667eea; color: white; padding: 0.2rem 0.5rem; border-radius: 4px;">${r.booking_id}</code></td>
<td><strong>${r.name}</strong></td>
<td>${r.date}<br><small>${r.time}</small></td>
<td>${r.guests}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function updateStats(reservations) {
const today = new Date().toISOString().split('T')[0];
const weekLater = new Date(Date.now() + 7*24*60*60*1000).toISOString().split('T')[0];
document.getElementById('stat-total').textContent = reservations.length;
document.getElementById('stat-today').textContent = reservations.filter(r => r.date === today).length;
document.getElementById('stat-week').textContent = reservations.filter(r => r.date >= today && r.date <= weekLater).length;
}
async function loadRooms() {
const res = await fetch('/api/rooms');
const data = await res.json();
const container = document.getElementById('rooms-list');
container.innerHTML = data.rooms.map(room => `
<div class="card">
<h2>${room.name}</h2>
<p style="color: #666; margin-bottom: 1rem;">${room.description}</p>
<p><strong>${room.tables.length} Tische</strong></p>
</div>
`).join('');
}
async function loadUsers() {
const res = await fetch('/api/admin/users');
const data = await res.json();
document.getElementById('stat-users').textContent = data.users.length;
const container = document.getElementById('users-list');
container.innerHTML = `
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Benutzername</th>
<th>Rolle</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${data.users.map(u => `
<tr>
<td><strong>${u.name}</strong></td>
<td>${u.username}</td>
<td><span class="badge badge-${u.role}">${u.role}</span></td>
<td>${u.is_active ? '✅ Aktiv' : '❌ Inaktiv'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function showAddUserModal() {
document.getElementById('user-modal').classList.add('active');
updateRoleInfo();
}
function closeUserModal() {
document.getElementById('user-modal').classList.remove('active');
}
function updateRoleInfo() {
const role = document.getElementById('new-user-role').value;
const perms = {
'admin': ['Dashboard', 'Buchungen', 'Räume', 'Benutzer', 'Templates', 'Einstellungen'],
'manager': ['Dashboard', 'Buchungen', 'Räume', 'Templates'],
'staff': ['Dashboard', 'Buchungen']
};
document.getElementById('role-permissions').innerHTML = perms[role].map(p =>
`<span class="permission-tag">${p}</span>`
).join('');
}
async function createUser() {
const data = {
username: document.getElementById('new-user-username').value,
name: document.getElementById('new-user-name').value,
email: document.getElementById('new-user-email').value,
role: document.getElementById('new-user-role').value,
send_email: document.getElementById('new-user-send-email').checked
};
if (!data.username || !data.name || !data.email) {
alert('Bitte alle Pflichtfelder ausfüllen!');
return;
}
const res = await fetch('/api/admin/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.status === 'ok') {
alert(`Benutzer erstellt!${result.temp_password ? ` Temporäres Passwort: ${result.temp_password}` : ''}`);
closeUserModal();
loadUsers();
} else {
alert('Fehler: ' + result.error);
}
}
async function saveTemplates() {
const res = await fetch('/api/admin/templates', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
confirmation: document.getElementById('template-confirmation').value,
email_reply: document.getElementById('template-email-reply').value
})
});
alert((await res.json()).status === 'ok' ? 'Gespeichert!' : 'Fehler');
}
async function saveSMTP() {
const res = await fetch('/api/admin/smtp', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
host: document.getElementById('smtp-host').value,
port: parseInt(document.getElementById('smtp-port').value),
user: document.getElementById('smtp-user').value,
password: document.getElementById('smtp-pass').value,
from: document.getElementById('smtp-from').value
})
});
alert((await res.json()).status === 'ok' ? 'Gespeichert!' : 'Fehler');
}
async function testSMTP() {
const res = await fetch('/api/admin/smtp/test', {method: 'POST'});
const data = await res.json();
alert(data.status === 'ok' ? 'Testmail gesendet!' : 'Fehler: ' + data.error);
}
</script>
</body>
</html>
'''
# === 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)