Initial Upload

This commit is contained in:
friloo 2026-04-21 17:45:58 +02:00 committed by GitHub
commit dbdc237fa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 7995 additions and 0 deletions

654
index.php Normal file
View file

@ -0,0 +1,654 @@
<?php
// Error Reporting für Debugging
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/includes/Database.php';
require_once __DIR__ . '/includes/Auth.php';
require_once __DIR__ . '/includes/UniFiController.php';
require_once __DIR__ . '/includes/Mailer.php';
$auth = new Auth();
$db = Database::getInstance();
$mailer = new Mailer();
// Settings laden
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
$logoUrl = $db->getSetting('logo_url', '');
$instructionHeader = $db->getSetting('instruction_header', 'So verwenden Sie Ihren Code');
$instructionText = $db->getSetting('instruction_text', '');
$publicAccess = $db->getSetting('public_access', 0);
$printTemplate = $db->getSetting('print_template', '<div style="text-align: center; padding: 40px;">
<h1>{APP_TITLE}</h1>
<h2>WLAN Zugangscode</h2>
<div style="font-size: 48px; font-weight: bold; margin: 30px 0; font-family: monospace; letter-spacing: 4px;">{VOUCHER_CODE}</div>
<p><strong>Gültig bis:</strong> {EXPIRY_DATE} um {EXPIRY_TIME} Uhr</p>
<p><strong>Standort:</strong> {SITE_NAME}</p>
<p><strong>Maximale Geräte:</strong> {MAX_USES}</p>
<hr style="margin: 30px 0;">
<div style="font-size: 14px; text-align: left;">
{INSTRUCTIONS}
</div>
</div>');
// Prüfen ob Login erforderlich - aber nur wenn nicht auf login.php
if (!$publicAccess && !$auth->isLoggedIn()) {
header('Location: login.php');
exit;
}
$error = '';
$success = '';
$voucherCode = '';
$voucherCreated = false;
$voucherData = [];
// Sites abrufen
if ($auth->isLoggedIn()) {
if ($auth->isAdmin()) {
$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 ORDER BY name");
} else {
$sites = $db->fetchAll(
"SELECT s.* FROM sites s
INNER JOIN user_site_access usa ON s.id = usa.site_id
WHERE s.is_active = 1 AND usa.user_id = ?
ORDER BY s.name",
[$_SESSION['user_id']]
);
}
} else {
// Öffentlicher Zugriff - nur Sites mit public_access
$sites = $db->fetchAll("SELECT * FROM sites WHERE is_active = 1 AND public_access = 1 ORDER BY name");
}
// Voucher erstellen
// WICHTIG: create_voucher kommt jetzt aus einem hidden input, nicht vom Button (disabled Buttons werden teils nicht gesendet)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_voucher'])) {
if (!$publicAccess && !$auth->isLoggedIn()) {
$error = 'Sie müssen angemeldet sein';
} elseif ($auth->isLoggedIn() && !$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = 'Ungültiges Sicherheits-Token';
} else {
try {
$siteId = (int)($_POST['site_id'] ?? 0);
$voucherName = trim((string)($_POST['voucher_name'] ?? ''));
$maxUses = (int)($_POST['max_uses'] ?? 0);
$sendEmail = isset($_POST['send_email']) && !empty($_POST['recipient_email']);
$recipientEmail = trim((string)($_POST['recipient_email'] ?? ''));
// Validierung
if (empty($voucherName)) {
throw new Exception('Bitte geben Sie einen Voucher-Namen ein');
}
if ($maxUses < 1 || $maxUses > 10) {
throw new Exception('Anzahl der Geräte muss zwischen 1 und 10 liegen');
}
if ($siteId <= 0) {
throw new Exception('Bitte wählen Sie einen Standort');
}
if ($sendEmail && !filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) {
throw new Exception('Ungültige E-Mail-Adresse');
}
// Site-Zugriff prüfen
if ($auth->isLoggedIn() && !$auth->hasAccessToSite($siteId)) {
throw new Exception('Keine Berechtigung für diese Site');
}
// Site-Daten abrufen
$site = $db->fetchOne("SELECT * FROM sites WHERE id = ? AND is_active = 1", [$siteId]);
if (!$site) {
throw new Exception('Site nicht gefunden');
}
// Voucher-Namen formatieren
$datum = date('Y-m-d');
$fullVoucherName = $datum . '_' . $voucherName;
// UniFi Controller initialisieren
$controller = new UniFiController(
$site['unifi_controller_url'],
$site['unifi_username'],
$site['unifi_password'],
$site['site_id']
);
// Voucher erstellen
$voucher = $controller->createVoucher($fullVoucherName, $maxUses, 480);
if (!is_array($voucher) || empty($voucher['formatted_code']) || empty($voucher['code'])) {
throw new Exception('UniFi hat keinen gültigen Voucher zurückgegeben');
}
$voucherCode = $voucher['formatted_code'];
// In Datenbank speichern
$userId = $auth->isLoggedIn() ? ($_SESSION['user_id'] ?? null) : null;
$db->execute(
"INSERT INTO vouchers (site_id, user_id, voucher_code, voucher_name, max_uses, expire_minutes, unifi_voucher_id)
VALUES (?, ?, ?, ?, ?, 480, ?)",
[$siteId, $userId, $voucher['code'], $fullVoucherName, $maxUses, ($voucher['unifi_id'] ?? null)]
);
// Voucher-Daten für Druck speichern
$expiryTimestamp = time() + (480 * 60);
$voucherData = [
'code' => $voucherCode,
'site_name' => $site['name'],
'max_uses' => $maxUses,
'expiry_date' => date('d.m.Y', $expiryTimestamp),
'expiry_time' => date('H:i', $expiryTimestamp)
];
// E-Mail versenden falls gewünscht
if ($sendEmail && !empty($recipientEmail)) {
if ($mailer->sendVoucherEmail($recipientEmail, $voucherCode, $site['name'], $maxUses)) {
$success .= ' E-Mail wurde erfolgreich versendet!';
} else {
$success .= ' (E-Mail konnte nicht versendet werden)';
}
}
$voucherCreated = true;
$success = 'Voucher erfolgreich erstellt!' . ($sendEmail ? ' E-Mail wurde versendet.' : '');
} catch (Exception $e) {
$error = 'Fehler: ' . $e->getMessage();
}
}
}
$currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null;
// Auto-select wenn nur eine Site
$autoSelectSite = (count($sites) === 1) ? $sites[0]['id'] : 0;
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($appTitle) ?></title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.header {
max-width: 1200px;
margin: 0 auto 30px;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.15);
backdrop-filter: blur(10px);
padding: 15px 25px;
border-radius: 15px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.header-logo { max-height: 40px; }
.header-title {
color: white;
font-size: 20px;
font-weight: 600;
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.user-info { color: white; font-size: 14px; }
.btn-header {
background: white;
color: #667eea;
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
font-size: 14px;
transition: all 0.3s;
border: none;
cursor: pointer;
}
.btn-header:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 40px;
}
.logo {
max-width: 250px;
display: block;
margin: 0 auto 30px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.alert {
padding: 14px;
border-radius: 10px;
margin-bottom: 25px;
font-size: 14px;
}
.alert-error {
background: #fee;
border: 1px solid #fcc;
color: #c33;
}
.alert-success {
background: #efe;
border: 1px solid #cfc;
color: #3c3;
}
.form-group { margin-bottom: 20px; }
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
}
input[type="text"],
input[type="number"],
input[type="email"],
select {
width: 100%;
padding: 14px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
transition: all 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
width: 100%;
padding: 16px;
background: #667eea;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.voucher-result {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 15px;
text-align: center;
margin-bottom: 25px;
}
.voucher-code {
font-size: 32px;
font-weight: bold;
letter-spacing: 2px;
margin: 20px 0;
font-family: 'Courier New', monospace;
}
.voucher-info {
font-size: 14px;
opacity: 0.9;
margin-top: 15px;
}
.instruction-box {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-top: 25px;
}
.instruction-box h3 {
color: #333;
margin-bottom: 10px;
font-size: 16px;
}
.instruction-box p {
color: #666;
line-height: 1.6;
font-size: 14px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.3;
}
/* Verbessertes E-Mail Option Design */
.email-option {
background: #f8f9fa;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
transition: all 0.3s ease;
}
.email-option.active {
background: linear-gradient(135deg, #e7f3ff 0%, #f0e7ff 100%);
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
.email-checkbox-wrapper {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
margin-bottom: 0;
}
.email-checkbox-wrapper input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
margin: 0;
accent-color: #667eea;
}
.email-checkbox-wrapper label {
margin: 0;
cursor: pointer;
font-size: 16px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.email-input-wrapper {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: all 0.3s ease;
margin-top: 0;
}
.email-input-wrapper.show {
max-height: 200px;
opacity: 1;
margin-top: 15px;
}
.email-input-wrapper input {
background: white;
border: 2px solid #e0e0e0;
}
.email-input-wrapper input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.print-button {
background: white;
color: #667eea;
border: 2px solid #667eea;
margin-top: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.print-button:hover {
background: #667eea;
color: white;
}
.print-button svg {
width: 20px;
height: 20px;
}
/* Print Styles */
@media print {
body * { visibility: hidden; }
#printArea, #printArea * { visibility: visible; }
#printArea {
position: absolute;
left: 0;
top: 0;
width: 100%;
background: white;
}
.btn, .header, form, .no-print { display: none !important; }
}
</style>
</head>
<body>
<?php if ($currentUser): ?>
<div class="header no-print">
<div class="header-left">
<div class="header-title">👋 Hallo, <?= htmlspecialchars($currentUser['name']) ?></div>
</div>
<div class="header-right">
<?php if ($auth->isAdmin()): ?>
<a href="admin/" class="btn-header">⚙️ Administration</a>
<?php endif; ?>
<a href="logout.php" class="btn-header">Abmelden</a>
</div>
</div>
<?php elseif ($publicAccess): ?>
<div class="header no-print">
<div class="header-left">
<div class="header-title"><?= htmlspecialchars($appTitle) ?></div>
</div>
<div class="header-right">
<a href="login.php" class="btn-header">
<span style="margin-right: 5px;">🔐</span> Anmelden
</a>
</div>
</div>
<?php endif; ?>
<div class="container">
<?php if ($logoUrl && !$voucherCreated): ?>
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
<?php endif; ?>
<h1><?= htmlspecialchars($appTitle) ?></h1>
<?php if ($error): ?>
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($voucherCreated): ?>
<div id="printArea">
<div class="voucher-result">
<div style="font-size: 18px; margin-bottom: 10px;"> Ihr Zugangs-Code</div>
<div class="voucher-code"><?= htmlspecialchars($voucherCode) ?></div>
<div class="voucher-info">
Der Code ist 8 Stunden ab Erstellung gültig
</div>
</div>
<?php if ($instructionHeader || $instructionText): ?>
<div class="instruction-box">
<?php if ($instructionHeader): ?>
<h3><?= htmlspecialchars($instructionHeader) ?></h3>
<?php endif; ?>
<?php if ($instructionText): ?>
<div><?= $instructionText ?></div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<form method="get" class="no-print">
<button type="submit" class="btn">Weiteren Code erstellen</button>
</form>
<button onclick="window.print()" class="btn print-button no-print">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
Code ausdrucken
</button>
<?php elseif (empty($sites)): ?>
<div class="empty-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
</svg>
<p>Keine verfügbaren Sites gefunden.<br>
<?php if ($auth->isAdmin()): ?>
<a href="admin/" style="color: #667eea;">Klicken Sie hier, um Sites anzulegen</a>
<?php else: ?>
Bitte kontaktieren Sie Ihren Administrator.
<?php endif; ?>
</p>
</div>
<?php else: ?>
<form method="post" id="voucherForm">
<!-- FIX: Trigger-Feld kommt nicht mehr vom Submit-Button -->
<input type="hidden" name="create_voucher" value="1">
<?php if ($auth->isLoggedIn()): ?>
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<?php endif; ?>
<div class="form-group">
<label for="voucher_name">Voucher-Name *</label>
<input type="text" id="voucher_name" name="voucher_name"
placeholder="z.B. Besprechung UL, Vertretername Firma XY" required>
</div>
<div class="form-group">
<label for="max_uses">Wie viele Geräte dürfen sich einloggen? *</label>
<input type="number" id="max_uses" name="max_uses"
min="1" max="10" value="1" required>
</div>
<div class="form-group">
<label for="site_id">Standort *</label>
<select id="site_id" name="site_id" required>
<?php if (count($sites) > 1): ?>
<option value="">Bitte wählen...</option>
<?php endif; ?>
<?php foreach ($sites as $site): ?>
<option value="<?= (int)$site['id'] ?>" <?= ($autoSelectSite == $site['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($site['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="email-option" id="email-option-box">
<div class="email-checkbox-wrapper">
<input type="checkbox" id="send_email" name="send_email" onchange="toggleEmailField()">
<label for="send_email">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Code per E-Mail versenden
</label>
</div>
<div class="email-input-wrapper" id="email_field">
<label for="recipient_email">E-Mail-Adresse des Empfängers</label>
<input type="email" id="recipient_email" name="recipient_email" placeholder="gast@example.com">
</div>
</div>
<!-- Button ohne name=create_voucher, damit disabled keinen Einfluss hat -->
<button type="submit" class="btn" id="submitBtn">
Voucher erstellen
</button>
</form>
<?php if ($instructionHeader || $instructionText): ?>
<div class="instruction-box">
<?php if ($instructionHeader): ?>
<h3><?= htmlspecialchars($instructionHeader) ?></h3>
<?php endif; ?>
<?php if ($instructionText): ?>
<div><?= $instructionText ?></div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<script>
function toggleEmailField() {
const checkbox = document.getElementById('send_email');
const emailField = document.getElementById('email_field');
const emailBox = document.getElementById('email-option-box');
const emailInput = document.getElementById('recipient_email');
if (!checkbox || !emailField || !emailBox || !emailInput) return;
if (checkbox.checked) {
emailField.classList.add('show');
emailBox.classList.add('active');
emailInput.required = true;
setTimeout(() => emailInput.focus(), 300);
} else {
emailField.classList.remove('show');
emailBox.classList.remove('active');
emailInput.required = false;
}
}
// Initialzustand (z.B. nach Browser-Autofill)
document.addEventListener('DOMContentLoaded', () => {
toggleEmailField();
});
// Form Submit mit Loading State + Double-Submit-Schutz
document.getElementById('voucherForm')?.addEventListener('submit', function(e) {
const btn = document.getElementById('submitBtn');
if (!btn) return;
// falls schon disabled: Double-Submit verhindern
if (btn.disabled) {
e.preventDefault();
return;
}
btn.disabled = true;
btn.innerHTML = '<span style="display: inline-block; animation: spin 1s linear infinite;">⏳</span> Erstelle Voucher...';
});
</script>
<style>
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</body>
</html>