1038 lines
42 KiB
PHP
1038 lines
42 KiB
PHP
<?php
|
||
/**
|
||
* install/index.php
|
||
*
|
||
* Eigenständiger Installations-Assistent (unabhängig von includes/bootstrap.php,
|
||
* da config/config.php zu diesem Zeitpunkt ja erst erstellt wird).
|
||
*
|
||
* Nach erfolgreicher Installation sollte dieses Verzeichnis aus
|
||
* Sicherheitsgründen vom Server gelöscht oder zumindest gesperrt werden.
|
||
*/
|
||
|
||
// Sicherheits-Header und Sitzungs-Cookie des Assistenten
|
||
header('X-Frame-Options: DENY');
|
||
header('X-Content-Type-Options: nosniff');
|
||
header('Referrer-Policy: no-referrer');
|
||
header('Cache-Control: no-store');
|
||
header('X-Robots-Tag: noindex, nofollow');
|
||
$installHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || strtolower(trim(explode(',', (string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''))[0])) === 'https';
|
||
session_set_cookie_params(['lifetime' => 0, 'path' => '/', 'secure' => $installHttps, 'httponly' => true, 'samesite' => 'Lax']);
|
||
session_start();
|
||
|
||
define('ROOT_DIR', dirname(__DIR__));
|
||
define('LOCK_FILE', __DIR__ . '/.installed');
|
||
require_once ROOT_DIR . '/database/sqlite_schema.php';
|
||
|
||
/** Gleiche Regeln wie im Programm (validate_password_strength), damit das Admin-Konto nicht schwächer ist als alle anderen. */
|
||
function install_password_error(string $p): ?string
|
||
{
|
||
if (strlen($p) < 10) return 'Das Admin-Passwort muss mindestens 10 Zeichen lang sein.';
|
||
if (!preg_match('/[A-Z]/', $p)) return 'Das Admin-Passwort muss mindestens einen Großbuchstaben enthalten.';
|
||
if (!preg_match('/[a-z]/', $p)) return 'Das Admin-Passwort muss mindestens einen Kleinbuchstaben enthalten.';
|
||
if (!preg_match('/[0-9]/', $p)) return 'Das Admin-Passwort muss mindestens eine Ziffer enthalten.';
|
||
if (!preg_match('/[^A-Za-z0-9]/', $p)) return 'Das Admin-Passwort muss mindestens ein Sonderzeichen enthalten.';
|
||
return null;
|
||
}
|
||
|
||
/** Verständliche Meldung zu einem Datenbankfehler (mit dem technischen Text dahinter). */
|
||
function install_db_error(Throwable $e): string
|
||
{
|
||
$code = (int) ($e instanceof PDOException ? ($e->errorInfo[1] ?? $e->getCode()) : 0);
|
||
$hint = match (true) {
|
||
$code === 1045 => 'Benutzername oder Passwort der Datenbank stimmen nicht.',
|
||
$code === 1049 => 'Die Datenbank existiert nicht. Legen Sie sie zuerst (leer) an, z. B. im Hosting-Bereich.',
|
||
$code === 1044 => 'Der Datenbank-Benutzer hat keine Rechte auf diese Datenbank.',
|
||
$code === 2002 || $code === 2006 => 'Der Datenbank-Server ist nicht erreichbar. Prüfen Sie Host (und Port, z. B. localhost:3306).',
|
||
$code === 2054 => 'Der Datenbank-Server verlangt eine Anmeldemethode, die diese PHP-Version nicht unterstützt.',
|
||
default => 'Prüfen Sie Host, Datenbankname, Benutzer und Passwort.',
|
||
};
|
||
return 'Datenbankverbindung fehlgeschlagen. ' . $hint . ' (Technisch: ' . $e->getMessage() . ')';
|
||
}
|
||
|
||
/**
|
||
* Schreibt die Konfiguration so, dass sie sicher gilt: auch wenn schon eine (alte, z. B. mitgelieferte) config.php liegt,
|
||
* die dem Webserver nicht gehört oder nicht beschreibbar ist, und mit Löschen des PHP-Zwischenspeichers (OPcache),
|
||
* damit nicht weiter die alte Version geladen wird.
|
||
*/
|
||
function install_write_config(string $path, string $content): bool
|
||
{
|
||
$ok = @file_put_contents($path, $content) !== false;
|
||
if (!$ok && is_file($path)) {
|
||
@chmod($path, 0644);
|
||
$ok = @file_put_contents($path, $content) !== false;
|
||
if (!$ok && @unlink($path)) $ok = @file_put_contents($path, $content) !== false; // Datei gehört einem anderen Benutzer, der Ordner aber nicht
|
||
}
|
||
if ($ok) {
|
||
clearstatcache(true, $path);
|
||
if (function_exists('opcache_invalidate')) @opcache_invalidate($path, true);
|
||
}
|
||
return $ok;
|
||
}
|
||
|
||
/** Liest die geschriebene Konfiguration frisch ein und prüft, dass genau die eingegebenen Daten darin stehen. Gibt null oder eine Fehlermeldung zurück. */
|
||
function install_verify_config(string $path, array $expected): ?string
|
||
{
|
||
if (function_exists('opcache_invalidate')) @opcache_invalidate($path, true);
|
||
$onDisk = (string) @file_get_contents($path);
|
||
if (!str_contains($onDisk, var_export((string) $expected['app_key'], true))) return 'Die Konfigurationsdatei enthält nicht die neuen Daten. Prüfen Sie die Schreibrechte auf config/config.php.';
|
||
$loaded = @include $path;
|
||
$db = is_array($loaded) ? (array) ($loaded['db'] ?? []) : [];
|
||
foreach (['driver', 'host', 'name', 'user', 'pass'] as $k) {
|
||
if ((string) ($db[$k] ?? '') !== (string) $expected['db'][$k]) {
|
||
return 'Die Konfiguration wurde geschrieben, PHP liefert aber noch eine ältere Version (Zwischenspeicher/OPcache des Servers). Laden Sie PHP neu (Hosting-Oberfläche) oder warten Sie einige Minuten und starten Sie die Installation erneut.';
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function h(?string $s): string
|
||
{
|
||
return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
// URL der aktuellen Installation automatisch erkennen
|
||
$detectedUrl = (function(): string {
|
||
$scheme = $GLOBALS['installHttps'] ? 'https' : 'http';
|
||
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';
|
||
// install/ vom Pfad entfernen → Root-URL
|
||
$path = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
|
||
$path = preg_replace('#/install$#', '', $path);
|
||
return rtrim($scheme . '://' . $host . $path, '/');
|
||
})();
|
||
|
||
/**
|
||
* Fehlt die Sperrdatei (z. B. weil Dateien neu hochgeladen wurden), darf der Assistent trotzdem keine bestehende
|
||
* Installation überschreiben: Gibt es schon eine Konfiguration mit Datenbank und Mitarbeiterkonten, gilt sie als installiert.
|
||
*/
|
||
$existingInstallation = (function (): bool {
|
||
$cfgFile = ROOT_DIR . '/config/config.php';
|
||
if (!is_file($cfgFile)) return false;
|
||
try {
|
||
$cfg = (array) (require $cfgFile);
|
||
$db = (array) ($cfg['db'] ?? []);
|
||
if (($db['driver'] ?? 'sqlite') === 'mysql') {
|
||
$pdo = new PDO('mysql:host=' . ($db['host'] ?? 'localhost') . ';dbname=' . ($db['name'] ?? '') . ';charset=utf8mb4', (string) ($db['user'] ?? ''), (string) ($db['pass'] ?? ''), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 3]);
|
||
} else {
|
||
$path = (string) ($db['sqlite_path'] ?? '');
|
||
if ($path === '' || !is_file($path)) return false;
|
||
$pdo = new PDO('sqlite:' . $path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||
}
|
||
return (int) $pdo->query('SELECT COUNT(*) FROM staff')->fetchColumn() > 0;
|
||
} catch (Throwable $e) {
|
||
return false; // keine nutzbare Datenbank: Erstinstallation ist in Ordnung
|
||
}
|
||
})();
|
||
$alreadyInstalled = file_exists(LOCK_FILE) || $existingInstallation;
|
||
$errors = [];
|
||
$success = false;
|
||
$manualConfig = null;
|
||
|
||
// ---------------------------------------------------------------
|
||
// Anforderungsprüfung
|
||
// ---------------------------------------------------------------
|
||
// Ordner anlegen, die für den Betrieb nötig sind (falls beim Hochladen leer weggelassen)
|
||
foreach (['uploads', 'backups', 'plugins', 'app_downloads', 'config'] as $__dir) {
|
||
if (!is_dir(ROOT_DIR . '/' . $__dir)) @mkdir(ROOT_DIR . '/' . $__dir, 0755, true);
|
||
}
|
||
$checks = [
|
||
['label' => 'PHP-Version 8.2 oder neuer (gefunden: ' . PHP_VERSION . ')', 'ok' => version_compare(PHP_VERSION, '8.2.0', '>=')],
|
||
['label' => 'PDO-Erweiterung', 'ok' => extension_loaded('pdo')],
|
||
['label' => 'PDO MySQL-Treiber (für Produktivbetrieb)', 'ok' => extension_loaded('pdo_mysql'), 'optional' => true],
|
||
['label' => 'PDO SQLite-Treiber (optional, für lokale Tests)', 'ok' => extension_loaded('pdo_sqlite'), 'optional' => true],
|
||
['label' => 'Mindestens ein Datenbanktreiber (MySQL oder SQLite)', 'ok' => extension_loaded('pdo_mysql') || extension_loaded('pdo_sqlite')],
|
||
['label' => 'mbstring-Erweiterung', 'ok' => extension_loaded('mbstring')],
|
||
['label' => 'openssl-Erweiterung (Verschlüsselung der Zugangsdaten, Signaturen)', 'ok' => extension_loaded('openssl')],
|
||
['label' => 'json-Erweiterung', 'ok' => extension_loaded('json')],
|
||
['label' => 'curl-Erweiterung (optional: Webhooks, Plugin-Katalog, Updates)', 'ok' => extension_loaded('curl'), 'optional' => true],
|
||
['label' => 'zip-Erweiterung (optional: Plugins, Themes, Update per ZIP, Backups)', 'ok' => extension_loaded('zip'), 'optional' => true],
|
||
['label' => 'IMAP-Erweiterung (optional, nur für E-Mail-zu-Ticket)', 'ok' => extension_loaded('imap'), 'optional' => true],
|
||
['label' => 'Verzeichnis config/ ist beschreibbar', 'ok' => is_writable(ROOT_DIR . '/config')],
|
||
['label' => 'Verzeichnis uploads/ ist beschreibbar', 'ok' => is_writable(ROOT_DIR . '/uploads')],
|
||
['label' => 'Verzeichnis database/ ist beschreibbar (nur für SQLite nötig)', 'ok' => is_writable(ROOT_DIR . '/database'), 'optional' => true],
|
||
['label' => 'Verzeichnis backups/ ist beschreibbar (für Backups)', 'ok' => is_writable(ROOT_DIR . '/backups'), 'optional' => true],
|
||
['label' => 'HTTPS (empfohlen: Anmeldedaten sonst unverschlüsselt)', 'ok' => $installHttps, 'optional' => true],
|
||
];
|
||
$requiredOk = true;
|
||
foreach ($checks as $c) {
|
||
if (empty($c['optional']) && !$c['ok']) {
|
||
$requiredOk = false;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------
|
||
// Installation durchführen
|
||
// ---------------------------------------------------------------
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$alreadyInstalled && $requiredOk) {
|
||
|
||
if (empty($_SESSION['install_csrf']) || !hash_equals($_SESSION['install_csrf'], $_POST['csrf_token'] ?? '')) {
|
||
$errors[] = 'Ungültige Anfrage (Sitzung abgelaufen). Bitte die Seite neu laden und erneut versuchen.';
|
||
} else {
|
||
$driver = ($_POST['driver'] ?? '') === 'mysql' ? 'mysql' : 'sqlite';
|
||
$siteName = trim($_POST['site_name'] ?? '') ?: 'DeskHub Support';
|
||
$siteUrl = rtrim(trim($_POST['site_url'] ?? ''), '/');
|
||
$adminName = trim($_POST['admin_name'] ?? '');
|
||
$adminEmail = trim($_POST['admin_email'] ?? '');
|
||
$adminPass = $_POST['admin_password'] ?? '';
|
||
$adminPass2 = $_POST['admin_password2'] ?? '';
|
||
|
||
if ($siteUrl === '') $errors[] = 'Bitte die URL der Installation angeben (z.B. https://support.firma.de).';
|
||
if ($adminName === '') $errors[] = 'Bitte einen Namen für das Admin-Konto angeben.';
|
||
if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) $errors[] = 'Bitte eine gültige Admin-E-Mail-Adresse angeben.';
|
||
if (($pwError = install_password_error($adminPass)) !== null) $errors[] = $pwError;
|
||
if ($adminPass !== $adminPass2) $errors[] = 'Die beiden Passwörter stimmen nicht überein.';
|
||
if (!extension_loaded('pdo_' . $driver)) $errors[] = "Die PHP-Erweiterung pdo_{$driver} ist nicht installiert.";
|
||
|
||
$dbConfig = [
|
||
'driver' => $driver,
|
||
'host' => 'localhost',
|
||
'name' => 'deskhub',
|
||
'user' => 'deskhub_user',
|
||
'pass' => 'change_me',
|
||
'sqlite_path' => ROOT_DIR . '/database/deskhub.sqlite',
|
||
];
|
||
|
||
if ($driver === 'mysql') {
|
||
$dbConfig['host'] = trim($_POST['db_host'] ?? 'localhost') ?: 'localhost';
|
||
$dbConfig['name'] = trim($_POST['db_name'] ?? '');
|
||
$dbConfig['user'] = trim($_POST['db_user'] ?? '');
|
||
$dbConfig['pass'] = $_POST['db_pass'] ?? '';
|
||
if ($dbConfig['name'] === '' || $dbConfig['user'] === '') {
|
||
$errors[] = 'Bitte Datenbankname und Datenbankbenutzer angeben.';
|
||
}
|
||
}
|
||
|
||
$pdo = null;
|
||
if (!$errors) {
|
||
try {
|
||
if ($driver === 'mysql') {
|
||
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $dbConfig['host'], $dbConfig['name']);
|
||
$pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass']);
|
||
} else {
|
||
// Nur bei gewähltem SQLite wird eine Datenbank-Datei angelegt, und nur für diese Installation:
|
||
// Bricht sie ab, wird die halbfertige Datei am Ende des Aufrufs wieder entfernt.
|
||
@unlink($dbConfig['sqlite_path']);
|
||
$sqlitePath = $dbConfig['sqlite_path'];
|
||
register_shutdown_function(function () use (&$success, &$pdo, &$stmt, $sqlitePath) {
|
||
if ($success) return;
|
||
// Verbindung und Statements schließen, sonst lässt sich die Datei (vor allem unter Windows) nicht löschen
|
||
$stmt = null;
|
||
$pdo = null;
|
||
gc_collect_cycles();
|
||
foreach ([$sqlitePath, $sqlitePath . '-wal', $sqlitePath . '-shm'] as $leftover) @unlink($leftover);
|
||
});
|
||
$pdo = new PDO('sqlite:' . $dbConfig['sqlite_path']);
|
||
$pdo->exec('PRAGMA foreign_keys = ON;');
|
||
}
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
} catch (Throwable $e) {
|
||
$errors[] = install_db_error($e);
|
||
}
|
||
}
|
||
|
||
if (!$errors && $pdo) {
|
||
try {
|
||
$sql = file_get_contents(ROOT_DIR . '/database/schema.sql');
|
||
if ($driver === 'sqlite') {
|
||
$sql = convert_schema_to_sqlite($sql);
|
||
}
|
||
$pdo->exec($sql);
|
||
|
||
// Vordefiniertes Demo-Admin-Konto entfernen und durch das hier
|
||
// eingegebene, echte Admin-Konto ersetzen.
|
||
$pdo->prepare('DELETE FROM staff WHERE email IN (?, ?)')->execute(['admin@example.com', $adminEmail]);
|
||
$stmt = $pdo->prepare(
|
||
'INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)'
|
||
);
|
||
$stmt->execute([$adminName, $adminEmail, password_hash($adminPass, PASSWORD_DEFAULT), 'admin', 1, date('Y-m-d H:i:s')]);
|
||
|
||
// ── Initiale Einstellungen in die DB seeden ───────────────────────
|
||
// Alle Kerneinstellungen sofort in der settings-Tabelle anlegen,
|
||
// damit sie Updates überleben (config.php wird beim Update nie
|
||
// überschrieben, aber so sind die Werte auch bei Neuinstallationen
|
||
// sofort über get_setting() abrufbar).
|
||
// Sicherer, zufälliger App-Key – direkt hier generieren, damit er
|
||
// sowohl in die DB (überlebt Updates) als auch in config.php geschrieben wird.
|
||
$appKey = bin2hex(random_bytes(32));
|
||
|
||
$installSettings = [
|
||
'app_key' => $appKey, // ← in DB: überlebt Updates
|
||
'display_name' => $siteName,
|
||
'site_url' => $siteUrl,
|
||
'timezone' => 'Europe/Berlin',
|
||
'smtp_from_email' => $adminEmail,
|
||
'smtp_from_name' => $siteName,
|
||
'mail_enabled' => '0',
|
||
'helpdesk_status' => 'online',
|
||
'kb_enabled' => '1',
|
||
'ticket_ratings_enabled' => '1',
|
||
'staff_session_timeout' => '30',
|
||
'retention_enabled' => '0',
|
||
'retention_months' => '24',
|
||
'ip_anonymize' => '0',
|
||
'upload_allowed_extensions' => 'png, jpg, jpeg, gif, pdf, txt, log, docx, xlsx, zip',
|
||
'upload_max_size_mb' => '10',
|
||
];
|
||
$upsertSql = $driver === 'sqlite'
|
||
? 'INSERT OR REPLACE INTO settings (setting_key, setting_value) VALUES (?, ?)'
|
||
: 'INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)';
|
||
foreach ($installSettings as $k => $v) {
|
||
$pdo->prepare($upsertSql)->execute([$k, $v]);
|
||
}
|
||
|
||
// Alle vorhandenen Migrationsdateien sofort als "eingespielt" markieren,
|
||
// da das Schema gerade frisch erstellt wurde.
|
||
$migrTable = $driver === 'sqlite'
|
||
? "CREATE TABLE IF NOT EXISTS schema_migrations (migration VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT (datetime('now')))"
|
||
: "CREATE TABLE IF NOT EXISTS schema_migrations (migration VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
||
$pdo->exec($migrTable);
|
||
$insertMigr = $driver === 'sqlite'
|
||
? 'INSERT OR IGNORE INTO schema_migrations (migration) VALUES (?)'
|
||
: 'INSERT IGNORE INTO schema_migrations (migration) VALUES (?)';
|
||
foreach (glob(ROOT_DIR . '/database/migrations/*.sql') ?: [] as $mf) {
|
||
$pdo->prepare($insertMigr)->execute([basename($mf)]);
|
||
}
|
||
|
||
} catch (Throwable $e) {
|
||
$errors[] = 'Fehler beim Einrichten der Datenbank: ' . $e->getMessage();
|
||
}
|
||
}
|
||
|
||
if (!$errors) {
|
||
$configArray = [
|
||
'app_key' => $appKey,
|
||
'db' => $dbConfig,
|
||
'site' => [
|
||
'name' => $siteName,
|
||
'url' => $siteUrl,
|
||
'admin_email' => $adminEmail,
|
||
'timezone' => 'Europe/Berlin',
|
||
],
|
||
'mail' => [
|
||
'enabled' => false,
|
||
'from_email' => $adminEmail,
|
||
'from_name' => $siteName,
|
||
],
|
||
'imap' => [
|
||
'enabled' => false,
|
||
'mailbox' => '{imap.example.com:993/imap/ssl}INBOX',
|
||
'user' => 'support@example.com',
|
||
'pass' => 'change_me',
|
||
'default_department_id' => 1,
|
||
'default_priority_id' => 2,
|
||
],
|
||
'uploads' => [
|
||
'max_size_bytes' => 10 * 1024 * 1024,
|
||
'allowed_extensions' => ['png','jpg','jpeg','gif','pdf','txt','log','docx','xlsx','zip'],
|
||
'path' => ROOT_DIR . '/uploads',
|
||
],
|
||
];
|
||
|
||
$php = "<?php\n/**\n * DeskHub – Konfiguration\n"
|
||
. " * Automatisch erstellt durch den Installations-Assistenten am " . date('d.m.Y H:i') . "\n"
|
||
. " * WICHTIG: app_key niemals ändern – verschlüsselte Werte in der DB werden sonst unlesbar.\n"
|
||
. " */\n\nreturn " . var_export($configArray, true) . ";\n";
|
||
|
||
$cfgPath = ROOT_DIR . '/config/config.php';
|
||
if (!install_write_config($cfgPath, $php)) {
|
||
$errors[] = 'config/config.php konnte nicht geschrieben werden. Die Datenbank ist eingerichtet. Speichern Sie den unten angezeigten Inhalt als config/config.php auf dem Server (oder geben Sie dem Webserver Schreibrechte auf den Ordner config/) und laden Sie die Seite neu.';
|
||
$manualConfig = $php;
|
||
} else {
|
||
@chmod($cfgPath, 0640); // enthält Schlüssel und Datenbank-Passwort
|
||
if (($verifyError = install_verify_config($cfgPath, $configArray)) !== null) {
|
||
$errors[] = $verifyError;
|
||
$manualConfig = $php;
|
||
} else {
|
||
// Programm-Migrationen (Schritte mit Code statt SQL) jetzt mit der fertigen Konfiguration einspielen: Fehler erscheinen hier
|
||
// und nicht erst beim ersten Seitenaufruf. Scheitert das, wird die Konfiguration wieder entfernt, damit ein neuer Versuch möglich ist.
|
||
try {
|
||
require_once ROOT_DIR . '/includes/db.php';
|
||
require_once ROOT_DIR . '/includes/functions.php';
|
||
get_db();
|
||
@file_put_contents(LOCK_FILE, date('Y-m-d H:i:s') . ' von ' . ($_SERVER['REMOTE_ADDR'] ?? 'CLI'));
|
||
$success = true;
|
||
} catch (Throwable $e) {
|
||
@unlink($cfgPath);
|
||
$errors[] = 'Fehler beim Einspielen der Datenbank-Änderungen: ' . $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($_SESSION['install_csrf'])) {
|
||
$_SESSION['install_csrf'] = bin2hex(random_bytes(32));
|
||
}
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>DeskHub – Installation</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--ink: #16202a;
|
||
--primary: #0f6f6a;
|
||
--primary-dk:#0b5853;
|
||
--primary-lt:#e6f4f3;
|
||
--accent: #d97706;
|
||
--danger: #dc2626;
|
||
--danger-lt: #fef2f2;
|
||
--success: #16a34a;
|
||
--success-lt:#f0fdf4;
|
||
--border: #dde3ec;
|
||
--muted: #64748b;
|
||
--surface: #ffffff;
|
||
--bg: #f1f5f9;
|
||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
--radius: 10px;
|
||
--shadow: 0 4px 24px rgba(15,23,42,.10), 0 1px 4px rgba(15,23,42,.06);
|
||
}
|
||
|
||
html, body {
|
||
min-height: 100vh;
|
||
font-family: var(--font);
|
||
font-size: 15px;
|
||
line-height: 1.55;
|
||
color: var(--ink);
|
||
background: var(--bg);
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* ── Layout ── */
|
||
.i-shell {
|
||
display: flex;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* ── Left brand panel ── */
|
||
.i-brand {
|
||
width: 340px;
|
||
flex-shrink: 0;
|
||
background: var(--ink);
|
||
background-image: radial-gradient(ellipse at 20% 50%, rgba(15,111,106,.35) 0%, transparent 65%),
|
||
radial-gradient(ellipse at 80% 10%, rgba(217,119,6,.15) 0%, transparent 55%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 48px 36px;
|
||
position: sticky;
|
||
top: 0;
|
||
height: 100vh;
|
||
}
|
||
|
||
.i-brand-logo {
|
||
font-size: 1.5rem;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
letter-spacing: -.03em;
|
||
margin-bottom: 8px;
|
||
}
|
||
.i-brand-logo span { color: var(--accent); }
|
||
|
||
.i-brand-tagline {
|
||
font-size: .82rem;
|
||
color: #94a3b8;
|
||
margin-bottom: 48px;
|
||
}
|
||
|
||
.i-brand-features {
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
flex: 1;
|
||
}
|
||
.i-brand-features li {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
}
|
||
.i-brand-feat-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
background: rgba(255,255,255,.08);
|
||
border: 1px solid rgba(255,255,255,.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 15px;
|
||
flex-shrink: 0;
|
||
margin-top: 1px;
|
||
}
|
||
.i-brand-feat-text strong {
|
||
display: block;
|
||
font-size: .82rem;
|
||
font-weight: 600;
|
||
color: #e2e8f0;
|
||
margin-bottom: 2px;
|
||
}
|
||
.i-brand-feat-text span {
|
||
font-size: .75rem;
|
||
color: #64748b;
|
||
}
|
||
|
||
.i-brand-footer {
|
||
font-size: .72rem;
|
||
color: #334155;
|
||
margin-top: 32px;
|
||
}
|
||
|
||
/* ── Right content area ── */
|
||
.i-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 48px 24px 64px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.i-box {
|
||
width: 100%;
|
||
max-width: 560px;
|
||
}
|
||
|
||
/* ── Section title ── */
|
||
.i-section-title {
|
||
font-size: .7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .08em;
|
||
color: var(--primary);
|
||
margin-bottom: 14px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 2px solid var(--primary-lt);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* ── Cards ── */
|
||
.i-card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 28px 28px 24px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.i-card-title {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--ink);
|
||
margin-bottom: 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.i-card-title .num {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
font-size: .72rem;
|
||
font-weight: 800;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ── Check list ── */
|
||
.i-checks { list-style: none; display: flex; flex-direction: column; gap: 0; }
|
||
.i-check-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 9px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: .875rem;
|
||
}
|
||
.i-check-row:last-child { border-bottom: none; }
|
||
.i-check-badge {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: .7rem;
|
||
font-weight: 800;
|
||
flex-shrink: 0;
|
||
}
|
||
.i-check-badge.ok { background: var(--success-lt); color: var(--success); }
|
||
.i-check-badge.err { background: var(--danger-lt); color: var(--danger); }
|
||
.i-check-badge.opt { background: #f1f5f9; color: var(--muted); }
|
||
|
||
.i-check-row.optional { opacity: .65; }
|
||
|
||
/* ── Form elements ── */
|
||
.i-field { margin-bottom: 16px; }
|
||
.i-field label {
|
||
display: block;
|
||
font-size: .8rem;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin-bottom: 5px;
|
||
}
|
||
.i-field input,
|
||
.i-field select {
|
||
width: 100%;
|
||
padding: 9px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 7px;
|
||
font-size: .9rem;
|
||
font-family: var(--font);
|
||
color: var(--ink);
|
||
background: #fff;
|
||
transition: border-color .15s, box-shadow .15s;
|
||
outline: none;
|
||
}
|
||
.i-field input:focus,
|
||
.i-field select:focus {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(15,111,106,.12);
|
||
}
|
||
.i-field input::placeholder { color: #b0bac6; }
|
||
.i-hint, .i-field .i-hint {
|
||
font-size: .74rem;
|
||
color: var(--muted);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.i-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||
|
||
/* ── DB driver tabs ── */
|
||
.i-driver-tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.i-driver-tab {
|
||
flex: 1;
|
||
border: 2px solid var(--border);
|
||
border-radius: 9px;
|
||
padding: 12px 14px;
|
||
cursor: pointer;
|
||
background: #fff;
|
||
transition: border-color .15s, background .15s;
|
||
text-align: left;
|
||
}
|
||
.i-driver-tab input[type=radio] { display: none; }
|
||
.i-driver-tab.active {
|
||
border-color: var(--primary);
|
||
background: var(--primary-lt);
|
||
}
|
||
.i-driver-tab-label {
|
||
font-size: .85rem;
|
||
font-weight: 700;
|
||
color: var(--ink);
|
||
margin-bottom: 2px;
|
||
}
|
||
.i-driver-tab-desc {
|
||
font-size: .73rem;
|
||
color: var(--muted);
|
||
}
|
||
.i-driver-tab.active .i-driver-tab-label { color: var(--primary-dk); }
|
||
|
||
/* ── Submit button ── */
|
||
.i-submit-btn {
|
||
width: 100%;
|
||
padding: 13px;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
font-size: .95rem;
|
||
font-weight: 700;
|
||
font-family: var(--font);
|
||
border: none;
|
||
border-radius: 9px;
|
||
cursor: pointer;
|
||
transition: background .15s, transform .1s;
|
||
margin-top: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
letter-spacing: .01em;
|
||
}
|
||
.i-submit-btn:hover { background: var(--primary-dk); }
|
||
.i-submit-btn:active { transform: scale(.99); }
|
||
|
||
/* ── Alerts ── */
|
||
.i-alert {
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
font-size: .875rem;
|
||
margin-bottom: 14px;
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
}
|
||
.i-alert.error { background: var(--danger-lt); border: 1px solid #fca5a5; color: #991b1b; }
|
||
.i-alert.success { background: var(--success-lt); border: 1px solid #86efac; color: #166534; }
|
||
.i-alert.warning { background: #fff7ed; border: 1px solid #fed7aa; color: #9a3412; }
|
||
.i-alert-icon { font-size: 1rem; line-height: 1.4; flex-shrink: 0; }
|
||
|
||
/* ── Success screen ── */
|
||
.i-success-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
background: var(--success-lt);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 2rem;
|
||
margin: 0 auto 20px;
|
||
}
|
||
.i-action-btns { display: flex; gap: 12px; margin-top: 20px; }
|
||
.i-btn-primary {
|
||
flex: 1;
|
||
padding: 11px;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
border-radius: 8px;
|
||
font-weight: 700;
|
||
font-size: .875rem;
|
||
text-align: center;
|
||
text-decoration: none;
|
||
transition: background .15s;
|
||
}
|
||
.i-btn-primary:hover { background: var(--primary-dk); color: #fff; text-decoration: none; }
|
||
.i-btn-secondary {
|
||
flex: 1;
|
||
padding: 11px;
|
||
background: #fff;
|
||
color: var(--ink);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
font-size: .875rem;
|
||
text-align: center;
|
||
text-decoration: none;
|
||
transition: background .15s;
|
||
}
|
||
.i-btn-secondary:hover { background: var(--bg); color: var(--ink); text-decoration: none; }
|
||
|
||
code {
|
||
font-family: var(--mono);
|
||
font-size: .82em;
|
||
background: #f1f5f9;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.i-divider {
|
||
border: none;
|
||
border-top: 1px solid var(--border);
|
||
margin: 20px 0;
|
||
}
|
||
|
||
/* ── Password strength ── */
|
||
.i-pw-bar {
|
||
height: 4px;
|
||
border-radius: 2px;
|
||
background: var(--border);
|
||
margin-top: 6px;
|
||
overflow: hidden;
|
||
}
|
||
.i-pw-fill {
|
||
height: 100%;
|
||
border-radius: 2px;
|
||
width: 0%;
|
||
transition: width .3s, background .3s;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.i-shell { flex-direction: column; }
|
||
.i-brand { width: 100%; height: auto; position: static; padding: 28px 24px; }
|
||
.i-brand-features { display: none; }
|
||
.i-content { padding: 28px 16px 48px; }
|
||
.i-row { grid-template-columns: 1fr; gap: 0; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="i-shell">
|
||
|
||
<!-- ── Brand Sidebar ── -->
|
||
<aside class="i-brand">
|
||
<div class="i-brand-logo">Desk<span>Hub</span></div>
|
||
<div class="i-brand-tagline">Helpdesk & Ticketsystem</div>
|
||
|
||
<ul class="i-brand-features">
|
||
<li>
|
||
<div class="i-brand-feat-icon">🎫</div>
|
||
<div class="i-brand-feat-text">
|
||
<strong>Ticket-Management</strong>
|
||
<span>Abteilungen, Prioritäten, SLA, Regeln</span>
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div class="i-brand-feat-icon">📧</div>
|
||
<div class="i-brand-feat-text">
|
||
<strong>E-Mail-Integration</strong>
|
||
<span>SMTP-Versand & IMAP-Abruf</span>
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div class="i-brand-feat-icon">👥</div>
|
||
<div class="i-brand-feat-text">
|
||
<strong>Kundenportal</strong>
|
||
<span>Self-Service, Wissensdatenbank</span>
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div class="i-brand-feat-icon">📊</div>
|
||
<div class="i-brand-feat-text">
|
||
<strong>Berichte & Statistiken</strong>
|
||
<span>SLA-Raten, Agenten-Performance</span>
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div class="i-brand-feat-icon">🔔</div>
|
||
<div class="i-brand-feat-text">
|
||
<strong>Benachrichtigungen</strong>
|
||
<span>Push, Discord, Telegram</span>
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div class="i-brand-feat-icon">🔒</div>
|
||
<div class="i-brand-feat-text">
|
||
<strong>Sicherheit</strong>
|
||
<span>2FA, verschlüsselte Zugangsdaten</span>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
|
||
<div class="i-brand-footer">DeskHub · Viper Network</div>
|
||
</aside>
|
||
|
||
<!-- ── Main content ── -->
|
||
<main class="i-content">
|
||
<div class="i-box">
|
||
|
||
<?php if ($alreadyInstalled): ?>
|
||
<!-- Already installed -->
|
||
<div class="i-card" style="text-align:center;padding:40px 28px">
|
||
<div class="i-success-icon">🔒</div>
|
||
<h2 style="margin-bottom:10px">Bereits installiert</h2>
|
||
<?php if (is_file(LOCK_FILE)): ?>
|
||
<p style="color:var(--muted);font-size:.9rem;margin-bottom:6px">Eingerichtet am <strong><?= h((string) file_get_contents(LOCK_FILE)) ?></strong></p>
|
||
<?php else: ?>
|
||
<p style="color:var(--muted);font-size:.9rem;margin-bottom:6px">Es gibt bereits eine Konfiguration mit Datenbank und Mitarbeiterkonten. Eine Neuinstallation würde diese Daten überschreiben und ist deshalb gesperrt.</p>
|
||
<?php endif; ?>
|
||
<div class="i-alert warning" style="margin-top:20px;text-align:left">
|
||
<span class="i-alert-icon">⚠️</span>
|
||
<span>Bitte das Verzeichnis <code>install/</code> vom Server löschen, um diese Seite zu deaktivieren.</span>
|
||
</div>
|
||
<div class="i-action-btns">
|
||
<a href="/login.php" class="i-btn-primary">Kundenportal</a>
|
||
<a href="/staff/login.php" class="i-btn-secondary">Mitarbeiter-Login</a>
|
||
</div>
|
||
<p style="font-size:.75rem;color:var(--muted);margin-top:16px;line-height:1.6">
|
||
Wirklich neu installieren? Erst ein Backup der Daten anlegen, dann <code>config/config.php</code>, <code>install/.installed</code> und die Datenbank (bei SQLite die Datei <code>database/deskhub.sqlite</code>) entfernen. Alle bisherigen Daten gehen dabei verloren.
|
||
</p>
|
||
</div>
|
||
|
||
<?php elseif ($success): ?>
|
||
<!-- Success screen -->
|
||
<div class="i-card" style="text-align:center;padding:40px 28px">
|
||
<div class="i-success-icon">✅</div>
|
||
<h2 style="margin-bottom:8px;color:var(--success)">Installation abgeschlossen!</h2>
|
||
<p style="color:var(--muted);font-size:.9rem">DeskHub wurde erfolgreich eingerichtet.</p>
|
||
<div class="i-action-btns">
|
||
<a href="/staff/login.php" class="i-btn-primary">→ Jetzt einloggen</a>
|
||
<a href="/login.php" class="i-btn-secondary">Kundenportal</a>
|
||
</div>
|
||
<hr class="i-divider">
|
||
<div class="i-alert warning" style="text-align:left">
|
||
<span class="i-alert-icon">🗑️</span>
|
||
<span><strong>Wichtig:</strong> Löschen Sie jetzt das Verzeichnis <code>install/</code> vom Server, damit niemand die Installation wiederholen kann.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<?php else: ?>
|
||
|
||
<!-- Step 1: Requirements -->
|
||
<div class="i-card">
|
||
<div class="i-card-title">
|
||
<span class="num">1</span> Systemvoraussetzungen
|
||
</div>
|
||
<ul class="i-checks">
|
||
<?php foreach ($checks as $c):
|
||
$cls = $c['ok'] ? 'ok' : (empty($c['optional']) ? 'err' : 'opt');
|
||
$ico = $c['ok'] ? '✓' : (empty($c['optional']) ? '✗' : '–');
|
||
?>
|
||
<li class="i-check-row <?= empty($c['optional']) ? '' : 'optional' ?>">
|
||
<span class="i-check-badge <?= $cls ?>"><?= $ico ?></span>
|
||
<span><?= h($c['label']) ?></span>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php if (!$requiredOk): ?>
|
||
<div class="i-alert error" style="margin-top:16px">
|
||
<span class="i-alert-icon">✗</span>
|
||
<span>Nicht alle Pflichtvoraussetzungen erfüllt. Bitte die rot markierten Punkte beheben und die Seite neu laden.</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if ($requiredOk): ?>
|
||
|
||
<?php foreach ($errors as $err): ?>
|
||
<div class="i-alert error"><span class="i-alert-icon">✗</span><span><?= h($err) ?></span></div>
|
||
<?php endforeach; ?>
|
||
<?php if ($manualConfig !== null): ?>
|
||
<div class="i-card">
|
||
<div class="i-card-title">Inhalt für config/config.php</div>
|
||
<div class="i-hint" style="margin-bottom:10px">Enthält Schlüssel und Datenbank-Passwort: nur auf dem Server speichern, nirgends weitergeben.</div>
|
||
<textarea readonly rows="14" style="width:100%;font-family:monospace;font-size:.78rem" onclick="this.select()"><?= h($manualConfig) ?></textarea>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Step 2: Configuration form -->
|
||
<div class="i-card">
|
||
<div class="i-card-title">
|
||
<span class="num">2</span> Konfiguration
|
||
</div>
|
||
|
||
<form method="post" id="install-form">
|
||
<input type="hidden" name="csrf_token" value="<?= h($_SESSION['install_csrf']) ?>">
|
||
|
||
<!-- DB driver -->
|
||
<div class="i-section-title">🗄️ Datenbank</div>
|
||
<div class="i-driver-tabs" id="driver-tabs">
|
||
<label class="i-driver-tab <?= ($_POST['driver'] ?? 'mysql') === 'mysql' ? 'active' : '' ?>" id="tab-mysql">
|
||
<input type="radio" name="driver" value="mysql" <?= ($_POST['driver'] ?? 'mysql') === 'mysql' ? 'checked' : '' ?>>
|
||
<div class="i-driver-tab-label">MySQL / MariaDB</div>
|
||
<div class="i-driver-tab-desc">Produktivbetrieb empfohlen</div>
|
||
</label>
|
||
<label class="i-driver-tab <?= ($_POST['driver'] ?? '') === 'sqlite' ? 'active' : '' ?>" id="tab-sqlite">
|
||
<input type="radio" name="driver" value="sqlite" <?= ($_POST['driver'] ?? '') === 'sqlite' ? 'checked' : '' ?>>
|
||
<div class="i-driver-tab-label">SQLite</div>
|
||
<div class="i-driver-tab-desc">Nur für lokale Tests</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div id="mysql-fields" style="display:<?= ($_POST['driver'] ?? 'mysql') === 'sqlite' ? 'none' : 'block' ?>">
|
||
<div class="i-row">
|
||
<div class="i-field">
|
||
<label for="db_host">Host</label>
|
||
<input type="text" id="db_host" name="db_host" value="<?= h($_POST['db_host'] ?? 'localhost') ?>" placeholder="localhost">
|
||
</div>
|
||
<div class="i-field">
|
||
<label for="db_name">Datenbankname</label>
|
||
<input type="text" id="db_name" name="db_name" value="<?= h($_POST['db_name'] ?? '') ?>" placeholder="deskhub">
|
||
</div>
|
||
</div>
|
||
<div class="i-row">
|
||
<div class="i-field">
|
||
<label for="db_user">Benutzer</label>
|
||
<input type="text" id="db_user" name="db_user" value="<?= h($_POST['db_user'] ?? '') ?>" placeholder="deskhub_user">
|
||
</div>
|
||
<div class="i-field">
|
||
<label for="db_pass">Passwort</label>
|
||
<input type="password" id="db_pass" name="db_pass" placeholder="••••••••">
|
||
</div>
|
||
</div>
|
||
<div class="i-hint" style="margin-bottom:16px">Die Datenbank muss bereits existieren (leer). Tabellen werden automatisch angelegt.</div>
|
||
</div>
|
||
|
||
<!-- Website -->
|
||
<div class="i-section-title" style="margin-top:8px">🌐 Website</div>
|
||
<div class="i-field">
|
||
<label for="site_name">Name der Installation</label>
|
||
<input type="text" id="site_name" name="site_name" value="<?= h($_POST['site_name'] ?? 'DeskHub Support') ?>" required>
|
||
</div>
|
||
<div class="i-field">
|
||
<label for="site_url">URL der Installation</label>
|
||
<input type="text" id="site_url" name="site_url" value="<?= h($_POST['site_url'] ?? $detectedUrl) ?>" placeholder="https://support.firma.de" required>
|
||
<div class="i-hint">Wird in E-Mail-Links verwendet. Kein abschließendes /</div>
|
||
</div>
|
||
|
||
<!-- Admin account -->
|
||
<div class="i-section-title" style="margin-top:8px">👤 Administrator-Konto</div>
|
||
<div class="i-row">
|
||
<div class="i-field">
|
||
<label for="admin_name">Name</label>
|
||
<input type="text" id="admin_name" name="admin_name" value="<?= h($_POST['admin_name'] ?? '') ?>" placeholder="Max Mustermann" required>
|
||
</div>
|
||
<div class="i-field">
|
||
<label for="admin_email">E-Mail</label>
|
||
<input type="email" id="admin_email" name="admin_email" autocapitalize="none" autocorrect="off" spellcheck="false" value="<?= h($_POST['admin_email'] ?? '') ?>" placeholder="admin@firma.de" required>
|
||
</div>
|
||
</div>
|
||
<div class="i-row">
|
||
<div class="i-field">
|
||
<label for="admin_password">Passwort</label>
|
||
<input type="password" id="admin_password" name="admin_password" placeholder="••••••••" required>
|
||
<div class="i-pw-bar"><div class="i-pw-fill" id="pw-fill"></div></div>
|
||
<div class="i-hint">Mindestens 10 Zeichen mit Groß- und Kleinbuchstaben, Ziffer und Sonderzeichen.</div>
|
||
</div>
|
||
<div class="i-field">
|
||
<label for="admin_password2">Passwort wiederholen</label>
|
||
<input type="password" id="admin_password2" name="admin_password2" placeholder="••••••••" required>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="i-submit-btn">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||
Installation starten
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
// Driver tab switching
|
||
document.querySelectorAll('.i-driver-tab input[type=radio]').forEach(function(radio) {
|
||
radio.addEventListener('change', function() {
|
||
document.querySelectorAll('.i-driver-tab').forEach(function(t) { t.classList.remove('active'); });
|
||
this.closest('.i-driver-tab').classList.add('active');
|
||
document.getElementById('mysql-fields').style.display = this.value === 'mysql' ? 'block' : 'none';
|
||
});
|
||
});
|
||
|
||
// Doppelklick verhindern: sonst startet die Installation zweimal
|
||
var installForm = document.getElementById('install-form');
|
||
if (installForm) installForm.addEventListener('submit', function() {
|
||
var b = installForm.querySelector('button[type=submit]');
|
||
if (b) { b.disabled = true; b.textContent = 'Wird installiert …'; }
|
||
});
|
||
|
||
// Password strength bar
|
||
var pwInput = document.getElementById('admin_password');
|
||
var pwFill = document.getElementById('pw-fill');
|
||
if (pwInput && pwFill) {
|
||
pwInput.addEventListener('input', function() {
|
||
var v = this.value, score = 0;
|
||
if (v.length >= 10) score++;
|
||
if (/[A-Z]/.test(v) && /[a-z]/.test(v)) score++;
|
||
if (/[0-9]/.test(v)) score++;
|
||
if (/[^A-Za-z0-9]/.test(v)) score++;
|
||
if (v.length >= 14) score++;
|
||
var colors = ['#ef4444','#f97316','#eab308','#22c55e','#16a34a'];
|
||
var widths = ['20%','40%','60%','80%','100%'];
|
||
pwFill.style.width = score > 0 ? widths[score-1] : '0%';
|
||
pwFill.style.background = score > 0 ? colors[score-1] : 'transparent';
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|