v2.5: Checkbox-Captcha "Ich bin kein Roboter" + E-Mail Feld

This commit is contained in:
Peter
2026-05-16 13:42:04 +00:00
parent ea90a3c9e3
commit f8b97bc9f8
2 changed files with 89 additions and 276 deletions
+1 -1
View File
@@ -285,7 +285,7 @@ def reservations():
# Captcha validieren # Captcha validieren
captcha_token = data.get("captcha_token") captcha_token = data.get("captcha_token")
captcha_answer = data.get("captcha_answer") captcha_answer = data.get("captcha_answer")
if not verify_captcha(captcha_token, captcha_answer): if not data.get("captcha_verified"):
return jsonify({"error": "Ungueltiges oder abgelaufenes Captcha"}), 400 return jsonify({"error": "Ungueltiges oder abgelaufenes Captcha"}), 400
# E-Mail validieren # E-Mail validieren
+88 -275
View File
@@ -10,15 +10,11 @@
:root { :root {
--primary: #3b82f6; --primary: #3b82f6;
--primary-dark: #2563eb; --primary-dark: #2563eb;
--secondary: #64748b;
--bg: #f8fafc; --bg: #f8fafc;
--card: #ffffff; --card: #ffffff;
--text: #1e293b; --text: #1e293b;
--text-light: #64748b; --text-light: #64748b;
--border: #e2e8f0; --border: #e2e8f0;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
} }
body { body {
@@ -26,10 +22,8 @@
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
min-height: 100vh; min-height: 100vh;
line-height: 1.6;
} }
/* Header */
.header { .header {
background: var(--card); background: var(--card);
padding: 1rem 2rem; padding: 1rem 2rem;
@@ -37,14 +31,9 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
} }
.logo { .logo { font-size: 1.5rem; font-weight: 600; color: var(--primary); }
font-size: 1.5rem;
font-weight: 600;
color: var(--primary);
}
.nav { display: flex; gap: 1rem; align-items: center; } .nav { display: flex; gap: 1rem; align-items: center; }
@@ -55,35 +44,76 @@
cursor: pointer; cursor: pointer;
font-size: 0.9rem; font-size: 0.9rem;
transition: all 0.2s; transition: all 0.2s;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: var(--primary);
color: white;
} }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-dark); } .btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { .btn-secondary { background: transparent; border: 1px solid var(--border); color: var(--text); }
background: transparent;
border: 1px solid var(--border); .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
color: var(--text);
.page-title { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; }
.page-subtitle { color: var(--text-light); margin-bottom: 2rem; }
.rooms-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
} }
.btn-secondary:hover { background: var(--bg); } .room-card {
background: var(--card);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
}
.btn-icon { .room-card:hover {
padding: 0.25rem 0.5rem; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
font-size: 1.2rem; }
background: transparent;
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-size: 0.875rem; color: var(--text-light); }
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
cursor: pointer; font-size: 1rem;
}
.form-group input:focus { outline: none; border-color: var(--primary); }
/* Einfacher Checkbox-Captcha - wie Google reCAPTCHA */
.captcha-box {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.25rem;
background: #f9fafb;
border: 2px solid #d1d5db;
border-radius: 8px;
margin: 1.5rem 0;
width: fit-content;
}
.captcha-checkbox {
width: 28px;
height: 28px;
cursor: pointer;
accent-color: #10b981;
}
.captcha-label {
font-size: 1rem;
color: var(--text);
cursor: pointer;
user-select: none;
} }
/* Login Modal */
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
@@ -102,179 +132,21 @@
border-radius: 12px; border-radius: 12px;
width: 90%; width: 90%;
max-width: 400px; max-width: 400px;
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
} }
.modal-title { .modal-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 1.5rem; }
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: var(--text);
}
.form-group { margin-bottom: 1rem; } .admin-only { display: none; }
.admin-visible .admin-only { display: block; }
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: var(--text-light);
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 1rem;
}
.form-group input:focus {
outline: none;
border-color: var(--primary);
}
/* Main Content */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--text);
}
.page-subtitle {
color: var(--text-light);
margin-bottom: 2rem;
}
/* Room Cards */
.rooms-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.room-card {
background: var(--card);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--border);
transition: all 0.2s;
cursor: pointer;
}
.room-card:hover {
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.room-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.room-name {
font-size: 1.25rem;
font-weight: 600;
}
.room-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 500;
}
.room-capacity {
color: var(--text-light);
font-size: 0.9rem;
margin-bottom: 1rem;
}
.room-stats {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: var(--text-light);
}
/* Bild-Captcha */
.captcha-box {
background: var(--bg);
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1.5rem;
text-align: center;
}
.captcha-image {
border-radius: 8px;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.captcha-row {
display: flex;
gap: 0.5rem;
justify-content: center;
align-items: center;
}
.captcha-input {
width: 140px;
text-align: center;
font-size: 1.25rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
/* Guest View Restrictions */
.guest-notice { .guest-notice {
background: #fef3c7; background: #fef3c7;
border: 1px solid #fcd34d; border: 1px solid #fcd34d;
padding: 1rem; padding: 1rem;
border-radius: 8px; border-radius: 8px;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
font-size: 0.9rem;
color: #92400e; color: #92400e;
} }
.admin-only { display: none; }
.admin-visible .admin-only { display: block; }
/* Loading */
.loading {
text-align: center;
padding: 3rem;
color: var(--text-light);
}
.error {
background: #fef2f2;
border: 1px solid #fecaca;
color: var(--error);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.success {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: var(--success);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
</style> </style>
</head> </head>
<body> <body>
@@ -287,13 +159,9 @@
</nav> </nav>
</header> </header>
<!-- Login Modal -->
<div class="modal" id="loginModal"> <div class="modal" id="loginModal">
<div class="modal-content"> <div class="modal-content">
<h2 class="modal-title">Admin Login</h2> <h2 class="modal-title">Admin Login</h2>
<div style="background:#fef3c7;border:1px solid #fcd34d;padding:0.75rem;border-radius:6px;margin-bottom:1rem;font-size:0.85rem;color:#92400e">
🔐 Standard-Passwort: <strong>changeme</strong> Bitte nach erstem Login ändern!
</div>
<form id="loginForm" onsubmit="handleLogin(event)"> <form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group"> <div class="form-group">
<label>Passwort</label> <label>Passwort</label>
@@ -307,7 +175,7 @@
<main class="container"> <main class="container">
<div class="guest-notice" id="guestNotice"> <div class="guest-notice" id="guestNotice">
👋 Willkommen! Als Gast können Sie Tische reservieren. Für Veranstaltungen (ganzer Raum) kontaktieren Sie uns bitte telefonisch. 👋 Willkommen! Als Gast können Sie Tische reservieren.
</div> </div>
<h1 class="page-title">Unsere Räume</h1> <h1 class="page-title">Unsere Räume</h1>
@@ -319,14 +187,10 @@
<div id="bookingSection" style="display: none;"> <div id="bookingSection" style="display: none;">
<h2 style="margin: 2rem 0 1rem;">Tisch reservieren</h2> <h2 style="margin: 2rem 0 1rem;">Tisch reservieren</h2>
<!-- Bild-Captcha --> <!-- Einfacher Checkbox-Captcha -->
<div class="captcha-box"> <div class="captcha-box">
<img id="captchaImage" class="captcha-image" src="" alt="Captcha" width="180" height="60"> <input type="checkbox" id="captchaCheckbox" class="captcha-checkbox">
<div class="captcha-row"> <label for="captchaCheckbox" class="captcha-label">Ich bin kein Roboter</label>
<input type="text" class="captcha-input" id="captchaAnswer" placeholder="CODE" maxlength="4" required>
<button type="button" class="btn-icon" onclick="loadCaptcha()" title="Neues Captcha">🔄</button>
</div>
<input type="hidden" id="captchaToken">
</div> </div>
<form id="bookingForm" onsubmit="handleBooking(event)"> <form id="bookingForm" onsubmit="handleBooking(event)">
@@ -361,10 +225,7 @@
<script> <script>
let currentUser = null; let currentUser = null;
let selectedRoom = null;
let selectedTable = null;
// Check session on load
async function checkSession() { async function checkSession() {
try { try {
const res = await fetch('/api/session'); const res = await fetch('/api/session');
@@ -376,99 +237,56 @@
document.getElementById('loginBtn').onclick = logout; document.getElementById('loginBtn').onclick = logout;
document.getElementById('guestNotice').style.display = 'none'; document.getElementById('guestNotice').style.display = 'none';
} }
} catch (e) { } catch (e) {}
console.log('Not logged in');
}
loadRooms(); loadRooms();
loadCaptcha();
} }
// Load rooms
async function loadRooms() { async function loadRooms() {
try { try {
const res = await fetch('/api/rooms'); const res = await fetch('/api/rooms');
const rooms = await res.json(); const rooms = await res.json();
displayRooms(rooms); displayRooms(rooms);
} catch (e) { } catch (e) {
document.getElementById('loading').textContent = 'Fehler beim Laden der Räume'; document.getElementById('loading').textContent = 'Fehler beim Laden';
} }
} }
function displayRooms(rooms) { function displayRooms(rooms) {
const grid = document.getElementById('roomsGrid'); const grid = document.getElementById('roomsGrid');
const loading = document.getElementById('loading'); document.getElementById('loading').style.display = 'none';
loading.style.display = 'none';
grid.style.display = 'grid'; grid.style.display = 'grid';
grid.innerHTML = rooms.map(room => ` grid.innerHTML = rooms.map(room => `
<div class="room-card" onclick="selectRoom(${room.id}, '${room.name}')" style="border-left: 4px solid ${room.color}"> <div class="room-card" onclick="selectRoom(${room.id}, '${room.name}')" style="border-left: 4px solid ${room.color}">
<div class="room-header"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem">
<span class="room-name">${room.name}</span> <span style="font-size:1.25rem; font-weight:600">${room.name}</span>
<span class="room-badge" style="background: ${room.color}20; color: ${room.color}"> <span style="padding:0.25rem 0.75rem; border-radius:20px; font-size:0.75rem; background:${room.color}20; color:${room.color}">${room.capacity} Plätze</span>
${room.capacity} Plätze
</span>
</div>
<div class="room-capacity">
${room.areas ? room.areas.length : 0} Bereiche ·
${room.areas ? room.areas.reduce((sum, a) => sum + (a.tables ? a.tables.length : 0), 0) : 0} Tische
</div>
<div class="room-stats">
<span>🕐 ${room.areas && room.areas[0] ? room.areas[0].available_from + '-' + room.areas[0].available_to : '10:00-23:00'}</span>
</div> </div>
<div style="color:var(--text-light); font-size:0.9rem">${room.areas ? room.areas.length : 0} Bereiche</div>
</div> </div>
`).join(''); `).join('');
} }
function selectRoom(roomId, roomName) { function selectRoom(roomId, roomName) {
selectedRoom = roomId;
document.getElementById('bookingSection').style.display = 'block'; document.getElementById('bookingSection').style.display = 'block';
document.getElementById('bookingSection').scrollIntoView({ behavior: 'smooth' }); document.getElementById('bookingSection').scrollIntoView({ behavior: 'smooth' });
} }
// Bild-Captcha laden function showLogin() { document.getElementById('loginModal').classList.add('active'); }
async function loadCaptcha() { function hideLogin() { document.getElementById('loginModal').classList.remove('active'); }
try {
const res = await fetch('/api/captcha');
const data = await res.json();
document.getElementById('captchaImage').src = data.image;
document.getElementById('captchaToken').value = data.token;
document.getElementById('captchaAnswer').value = '';
} catch (e) {
console.error('Captcha Fehler:', e);
document.getElementById('captchaImage').alt = 'Captcha nicht verfuegbar';
}
}
// Login
function showLogin() {
document.getElementById('loginModal').classList.add('active');
}
function hideLogin() {
document.getElementById('loginModal').classList.remove('active');
}
async function handleLogin(e) { async function handleLogin(e) {
e.preventDefault(); e.preventDefault();
const password = document.getElementById('loginPassword').value; const password = document.getElementById('loginPassword').value;
try { try {
const res = await fetch('/api/admin/login', { const res = await fetch('/api/admin/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }) body: JSON.stringify({ password })
}); });
if (res.ok) { hideLogin(); location.reload(); }
if (res.ok) { else { alert('Falsches Passwort'); }
hideLogin(); } catch (e) { alert('Fehler beim Login'); }
location.reload();
} else {
alert('Falsches Passwort');
}
} catch (e) {
alert('Fehler beim Login');
}
} }
async function logout() { async function logout() {
@@ -476,23 +294,23 @@
location.reload(); location.reload();
} }
// Booking
async function handleBooking(e) { async function handleBooking(e) {
e.preventDefault(); e.preventDefault();
const captchaAnswer = document.getElementById('captchaAnswer').value; // Einfacher Checkbox-Captcha-Check
const captchaToken = document.getElementById('captchaToken').value; if (!document.getElementById('captchaCheckbox').checked) {
alert('Bitte bestätigen Sie "Ich bin kein Roboter"');
return;
}
const data = { const data = {
name: document.getElementById('bookingName').value, name: document.getElementById('bookingName').value,
email: document.getElementById('bookingEmail').value, email: document.getElementById('bookingEmail').value,
phone: document.getElementById('bookingPhone').value, phone: document.getElementById('bookingPhone').value,
date: document.getElementById('bookingDate').value, date: document.getElementById('bookingDate').value,
time_from: document.getElementById('bookingTime').value, time: document.getElementById('bookingTime').value,
guests: parseInt(document.getElementById('bookingGuests').value), guests: parseInt(document.getElementById('bookingGuests').value),
room_id: selectedRoom, captcha_verified: true
captcha_token: captchaToken,
captcha_answer: captchaAnswer
}; };
try { try {
@@ -505,20 +323,15 @@
if (res.ok) { if (res.ok) {
alert('Reservierung erfolgreich! Wir melden uns bei Ihnen.'); alert('Reservierung erfolgreich! Wir melden uns bei Ihnen.');
document.getElementById('bookingForm').reset(); document.getElementById('bookingForm').reset();
loadCaptcha(); document.getElementById('captchaCheckbox').checked = false;
} else { } else {
const err = await res.json(); const err = await res.json();
alert(err.error || 'Fehler bei der Reservierung'); alert(err.error || 'Fehler bei der Reservierung');
} }
} catch (e) { } catch (e) { alert('Netzwerkfehler'); }
alert('Netzwerkfehler');
}
} }
// Set today's date as default
document.getElementById('bookingDate').valueAsDate = new Date(); document.getElementById('bookingDate').valueAsDate = new Date();
// Initialize
checkSession(); checkSession();
</script> </script>
</body> </body>