Unifi-Voucher-Tool/install.php
Friederich Loheide 498c7e28e0
Some checks failed
CI / PHP Lint (push) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run
CI / PHP Lint (pull_request) Has been cancelled
CI / PHP Lint-1 (pull_request) Has been cancelled
CI / Unit Tests & Static Analysis (pull_request) Has been cancelled
Redesign: gemeinsames Design-System für Frontend und Backend
Frontend, Login/Installer/Updater und der komplette Admin-Bereich nutzen
jetzt ein einziges Stylesheet (assets/global.css) statt pro Seite
dupliziertem Inline-CSS.

Design-System
- Tokens für Flächen, Text, Linien, Marke, Status, Radien, Schatten und
  Layout-Maße; Dark Mode ausschließlich über Tokens (keine !important-
  Overrides mehr)
- Komponenten: Buttons, Formularfelder, Cards, Tabellen, Badges, Alerts,
  Tabs, Pagination, Modals, Toasts, Statistik-Kacheln, Empty States
- Schrift Inter mit System-Fallback

Oberfläche
- Admin-Shell neu: durchgehende Sidebar mit Marke, gruppierter Navigation
  und Benutzerbereich; schlanke Topbar mit Breadcrumb
- Dashboard: ruhige KPI-Kacheln mit Icon-Chips, Charts an Theme-Farben
  gekoppelt
- Öffentliche Voucher-Seite: App-Topbar, klare Formularstruktur und
  Ticket-Darstellung des erstellten Codes inkl. QR-Code
- Login/Passwort/2FA: zweispaltiges Auth-Layout bzw. Fokus-Karten
- Updater und Wartungsmodus im gleichen Look (Wartungsseite bleibt
  bewusst eigenständig ohne externe Abhängigkeiten)
- Emoji-Icons in der UI durch Font-Awesome-Icons ersetzt

Nebenbei behoben
- Falscher SRI-Hash blockierte qrcode.min.js – QR-Codes wurden auf der
  Voucher-Seite und bei der 2FA-Einrichtung nie gerendert
- assets/global.css wurde in mehreren Admin-Seiten über einen falschen
  Pfad eingebunden ($adminBase = '' statt '../')
- TinyMCE lädt ohne API-Key jetzt die GPL-Variante von cdnjs – kein
  "valid API key required"-Banner mehr im Einstellungs-Editor
- Tabellen in Cards scrollen horizontal statt zu überlaufen

Screenshots in docs/screenshots neu erstellt, README aktualisiert (neuer
Abschnitt "Design-System", Version 2.5.0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:33:52 +00:00

383 lines
No EOL
17 KiB
PHP
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.

<?php
session_start();
// Prüfen ob bereits installiert.
// Wenn eine config.php existiert, darf der Installer NICHT mehr ohne Weiteres
// erreichbar sein sonst koennte jeder die Konfiguration ueberschreiben und
// einen neuen Admin anlegen. Reinstall ist nur fuer angemeldete Admins erlaubt.
if (file_exists(__DIR__ . '/config.php')) {
if (!isset($_GET['reinstall'])) {
die('System bereits installiert. Eine Neuinstallation ist nur fuer angemeldete Administratoren ueber install.php?reinstall=1 moeglich.');
}
// Reinstall angefordert -> Admin-Authentifizierung erzwingen
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/includes/Database.php';
require_once __DIR__ . '/includes/Auth.php';
try {
$reinstallAuth = new Auth();
if (!$reinstallAuth->isAdmin()) {
die('Neuinstallation nicht erlaubt: Bitte zuerst als Administrator <a href="login.php">anmelden</a>.');
}
} catch (Exception $e) {
die('Neuinstallation nicht moeglich (Konfigurationsfehler).');
}
}
$step = isset($_POST['step']) ? (int)$_POST['step'] : 1;
$errors = [];
$success = false;
// Step 1: Datenbankverbindung testen
if ($step === 2 && $_SERVER['REQUEST_METHOD'] === 'POST') {
$db_host = $_POST['db_host'] ?? '';
$db_name = $_POST['db_name'] ?? '';
$db_user = $_POST['db_user'] ?? '';
$db_pass = $_POST['db_pass'] ?? '';
try {
$pdo = new PDO("mysql:host=$db_host;charset=utf8mb4", $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Datenbank erstellen falls nicht vorhanden
$pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("USE `$db_name`");
// Tabellen erstellen
$sql = file_get_contents(__DIR__ . '/database.sql');
$pdo->exec($sql);
$_SESSION['install_db'] = [
'host' => $db_host,
'name' => $db_name,
'user' => $db_user,
'pass' => $db_pass
];
} catch (PDOException $e) {
$errors[] = "Datenbankfehler: " . $e->getMessage();
$step = 1;
}
}
// Step 2: Admin-Account erstellen
if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') {
$admin_email = filter_var($_POST['admin_email'] ?? '', FILTER_VALIDATE_EMAIL);
$admin_name = $_POST['admin_name'] ?? '';
$admin_password = $_POST['admin_password'] ?? '';
$admin_password_confirm = $_POST['admin_password_confirm'] ?? '';
if (!$admin_email) {
$errors[] = "Ungültige E-Mail-Adresse";
$step = 2;
} elseif (strlen($admin_password) < 8) {
$errors[] = "Passwort muss mindestens 8 Zeichen lang sein";
$step = 2;
} elseif ($admin_password !== $admin_password_confirm) {
$errors[] = "Passwörter stimmen nicht überein";
$step = 2;
} else {
$_SESSION['install_admin'] = [
'email' => $admin_email,
'name' => $admin_name,
'password' => password_hash($admin_password, PASSWORD_DEFAULT)
];
}
}
// Step 3: Allgemeine Einstellungen
if ($step === 4 && $_SERVER['REQUEST_METHOD'] === 'POST') {
$app_title = $_POST['app_title'] ?? 'UniFi Voucher System';
$logo_url = $_POST['logo_url'] ?? '';
$instruction_header = $_POST['instruction_header'] ?? '';
$instruction_text = $_POST['instruction_text'] ?? '';
$public_access = isset($_POST['public_access']) ? 1 : 0;
// Microsoft 365 OAuth (optional)
$m365_client_id = $_POST['m365_client_id'] ?? '';
$m365_client_secret = $_POST['m365_client_secret'] ?? '';
$m365_tenant_id = $_POST['m365_tenant_id'] ?? '';
$_SESSION['install_settings'] = [
'app_title' => $app_title,
'logo_url' => $logo_url,
'instruction_header' => $instruction_header,
'instruction_text' => $instruction_text,
'public_access' => $public_access,
'm365_client_id' => $m365_client_id,
'm365_client_secret' => $m365_client_secret,
'm365_tenant_id' => $m365_tenant_id
];
}
// Step 4: Installation abschließen
if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$db = $_SESSION['install_db'];
$admin = $_SESSION['install_admin'];
$settings = $_SESSION['install_settings'];
$pdo = new PDO("mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4", $db['user'], $db['pass']);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Admin-User erstellen
$stmt = $pdo->prepare("INSERT INTO users (email, name, password_hash, is_admin, is_active) VALUES (?, ?, ?, 1, 1)");
$stmt->execute([$admin['email'], $admin['name'], $admin['password']]);
// Settings speichern
$settingsData = [
'app_title' => $settings['app_title'],
'logo_url' => $settings['logo_url'],
'instruction_header' => $settings['instruction_header'],
'instruction_text' => $settings['instruction_text'],
'public_access' => $settings['public_access'],
'm365_client_id' => $settings['m365_client_id'],
'm365_client_secret' => $settings['m365_client_secret'],
'm365_tenant_id' => $settings['m365_tenant_id']
];
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
foreach ($settingsData as $key => $value) {
$stmt->execute([$key, $value]);
}
// config.php erstellen
$configContent = "<?php\n";
$configContent .= "// UniFi Voucher Management System - Configuration\n\n";
$configContent .= "define('DB_HOST', '{$db['host']}');\n";
$configContent .= "define('DB_NAME', '{$db['name']}');\n";
$configContent .= "define('DB_USER', '{$db['user']}');\n";
$configContent .= "define('DB_PASS', '" . addslashes($db['pass']) . "');\n\n";
$configContent .= "// Anwendungs-Schluessel fuer Verschluesselung-at-rest (z.B. UniFi-Passwoerter)\n";
$configContent .= "// NICHT aendern, sonst koennen bestehende verschluesselte Werte nicht mehr gelesen werden.\n";
$configContent .= "define('APP_KEY', '" . base64_encode(random_bytes(32)) . "');\n\n";
$configContent .= "// Sitzungs-Einstellungen\n";
$configContent .= "define('SESSION_LIFETIME', 3600); // 1 Stunde\n\n";
$configContent .= "// Zeitzone\n";
$configContent .= "date_default_timezone_set('Europe/Berlin');\n";
file_put_contents(__DIR__ . '/config.php', $configContent);
// .htaccess erstellen (ohne Rewrite Rules die Probleme machen)
$htaccess = "# UniFi Voucher System\n\n";
$htaccess .= "# Security\n";
$htaccess .= "<FilesMatch \"(config\\.php|database\\.sql|install\\.php|test\\.php|m365_debug\\.php|\\.md)$\">\n";
$htaccess .= " Order Allow,Deny\n";
$htaccess .= " Deny from all\n";
$htaccess .= "</FilesMatch>\n\n";
$htaccess .= "DirectoryIndex index.php\n";
file_put_contents(__DIR__ . '/.htaccess', $htaccess);
$success = true;
// Session-Daten löschen
unset($_SESSION['install_db'], $_SESSION['install_admin'], $_SESSION['install_settings']);
} catch (Exception $e) {
$errors[] = "Fehler bei der Installation: " . $e->getMessage();
$step = 4;
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UniFi Voucher System - Installation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/global.css">
<style>
/* Installer-spezifisch: Fortschrittsanzeige und Abschnitte */
.install-head { display: flex; align-items: center; gap: 14px; margin-bottom: 4px; }
.progress { position: relative; display: flex; justify-content: space-between; margin: 26px 0 28px; }
.progress::before {
content: ''; position: absolute; top: 14px; left: 14px; right: 14px;
height: 2px; background: var(--border-color); z-index: 0;
}
.progress-step {
position: relative; z-index: 1;
width: 28px; height: 28px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: var(--bg-card); border: 1px solid var(--border-color);
color: var(--text-muted); font-size: 12px; font-weight: 650;
}
.progress-step.active { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: var(--ring); }
.progress-step.completed { background: var(--success); border-color: var(--success); color: #fff; }
.section {
padding: 18px;
margin-bottom: 20px;
background: var(--bg-subtle);
border: 1px solid var(--border-color);
border-radius: var(--r-md);
}
.section h3 { font-size: 14px; margin-bottom: 14px; }
.error, .success {
padding: 12px 15px; margin-bottom: 20px;
border-radius: var(--r-md); font-size: 13.5px;
border: 1px solid var(--danger-border); background: var(--danger-soft); color: var(--danger);
}
.success { border-color: var(--success-border); background: var(--success-soft); color: var(--success); }
form > h3 { font-size: 15px; margin-bottom: 16px; }
</style>
</head>
<body class="app-body focus-page">
<div class="focus-card card">
<div class="install-head">
<span class="focus-icon"><i class="fas fa-rocket"></i></span>
<div>
<h1 style="font-size:20px;">UniFi Voucher System</h1>
<p class="sub">Installation in fünf Schritten</p>
</div>
</div>
<div class="progress">
<div class="progress-step <?= $step >= 1 ? 'completed' : '' ?>">1</div>
<div class="progress-step <?= $step >= 2 ? 'completed' : ($step === 1 ? 'active' : '') ?>">2</div>
<div class="progress-step <?= $step >= 3 ? 'completed' : ($step === 2 ? 'active' : '') ?>">3</div>
<div class="progress-step <?= $step >= 4 ? 'completed' : ($step === 3 ? 'active' : '') ?>">4</div>
<div class="progress-step <?= $step >= 5 ? 'active' : '' ?>">5</div>
</div>
<?php if (!empty($errors)): ?>
<div class="error">
<?php foreach ($errors as $error): ?>
<div><?= htmlspecialchars($error) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div class="success">
<strong>✓ Installation erfolgreich abgeschlossen!</strong><br>
Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden.
</div>
<a href="index.php" class="btn btn-primary btn-lg btn-block">Zum Login</a>
<?php elseif ($step === 1): ?>
<form method="post">
<input type="hidden" name="step" value="2">
<h3>Schritt 1: Datenbank-Konfiguration</h3>
<div class="form-group">
<label>Datenbank-Host</label>
<input type="text" name="db_host" value="localhost" required>
<div class="help-text">Meist "localhost"</div>
</div>
<div class="form-group">
<label>Datenbankname</label>
<input type="text" name="db_name" value="unifi_voucher" required>
<div class="help-text">Name der Datenbank (wird erstellt falls nicht vorhanden)</div>
</div>
<div class="form-group">
<label>Datenbank-Benutzer</label>
<input type="text" name="db_user" required>
</div>
<div class="form-group">
<label>Datenbank-Passwort</label>
<input type="password" name="db_pass">
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
</form>
<?php elseif ($step === 2): ?>
<form method="post">
<input type="hidden" name="step" value="3">
<h3>Schritt 2: Administrator-Account</h3>
<div class="form-group">
<label>Name</label>
<input type="text" name="admin_name" required>
</div>
<div class="form-group">
<label>E-Mail</label>
<input type="email" name="admin_email" required>
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" name="admin_password" required minlength="8">
<div class="help-text">Mindestens 8 Zeichen</div>
</div>
<div class="form-group">
<label>Passwort bestätigen</label>
<input type="password" name="admin_password_confirm" required>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
</form>
<?php elseif ($step === 3): ?>
<form method="post">
<input type="hidden" name="step" value="4">
<h3>Schritt 3: Allgemeine Einstellungen</h3>
<div class="form-group">
<label>Anwendungs-Titel</label>
<input type="text" name="app_title" value="UniFi Voucher System" required>
</div>
<div class="form-group">
<label>Logo-URL (optional)</label>
<input type="text" name="logo_url" placeholder="https://example.com/logo.png">
</div>
<div class="form-group">
<label>Anleitung Überschrift</label>
<input type="text" name="instruction_header" value="So verwenden Sie Ihren Code">
</div>
<div class="form-group">
<label>Anleitung Text</label>
<textarea name="instruction_text">Verbinden Sie sich mit dem WLAN und geben Sie den Code auf der Anmeldeseite ein.</textarea>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" name="public_access" id="public_access">
<label for="public_access" style="margin: 0;">Öffentlicher Zugriff auf Code-Erstellung</label>
</div>
<div class="section">
<h3>Microsoft 365 Login (Optional)</h3>
<div class="form-group">
<label>Client ID</label>
<input type="text" name="m365_client_id">
</div>
<div class="form-group">
<label>Client Secret</label>
<input type="password" name="m365_client_secret">
</div>
<div class="form-group">
<label>Tenant ID</label>
<input type="text" name="m365_tenant_id">
</div>
<div class="help-text">Leer lassen, wenn M365-Login nicht verwendet werden soll</div>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
</form>
<?php elseif ($step === 4): ?>
<form method="post">
<input type="hidden" name="step" value="5">
<h3>Schritt 4: Installation abschließen</h3>
<p style="margin-bottom: 20px; color: #666;">
Klicken Sie auf "Installation abschließen", um die Einrichtung zu beenden.
Die Datenbank und alle notwendigen Dateien werden erstellt.
</p>
<button type="submit" class="btn btn-primary btn-lg btn-block">Installation abschließen</button>
</form>
<?php endif; ?>
</div>
</body>
</html>