Upload via GUI (258 Dateien)

This commit is contained in:
2026-10-16 19:12:18 +00:00
parent 56deade877
commit 6cced3beda
50 changed files with 11148 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Nach der Installation: gesamtes Verzeichnis sperren wenn .installed existiert
<Files ".installed">
Require all denied
</Files>
Options -Indexes
# Wenn .installed existiert, alle PHP-Ausführung blockieren
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}/install/.installed -f
RewriteRule ^ - [F,L]
</IfModule>
+910
View File
@@ -0,0 +1,910 @@
<?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.
*/
session_start();
define('ROOT_DIR', dirname(__DIR__));
define('LOCK_FILE', __DIR__ . '/.installed');
require_once ROOT_DIR . '/database/sqlite_schema.php';
function h(?string $s): string
{
return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8');
}
// URL der aktuellen Installation automatisch erkennen
$detectedUrl = (function(): string {
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? '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;
// ---------------------------------------------------------------
// Anforderungsprüfung
// ---------------------------------------------------------------
$checks = [
['label' => 'PHP-Version 8.1 oder neuer (gefunden: ' . PHP_VERSION . ')', 'ok' => version_compare(PHP_VERSION, '8.1.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' => 'mbstring-Erweiterung', 'ok' => extension_loaded('mbstring')],
['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', 'ok' => is_writable(ROOT_DIR . '/database')],
];
$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 (strlen($adminPass) < 8) $errors[] = 'Das Admin-Passwort muss mindestens 8 Zeichen lang sein.';
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 {
@unlink($dbConfig['sqlite_path']);
$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[] = 'Datenbankverbindung fehlgeschlagen: ' . $e->getMessage();
}
}
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->exec("DELETE FROM staff WHERE email = 'admin@example.com'");
$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";
if (file_put_contents(ROOT_DIR . '/config/config.php', $php) === false) {
$errors[] = 'config/config.php konnte nicht geschrieben werden. Bitte Schreibrechte des Verzeichnisses prüfen.';
} else {
file_put_contents(LOCK_FILE, date('Y-m-d H:i:s') . " von " . ($_SERVER['REMOTE_ADDR'] ?? 'CLI'));
$success = true;
}
}
}
}
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-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>
<p style="color:var(--muted);font-size:.9rem;margin-bottom:6px">
Eingerichtet am <strong><?= h(is_file(LOCK_FILE) ? (string) file_get_contents(LOCK_FILE) : "bereits vorhandener Konfiguration") ?></strong>
</p>
<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">
Zum Wiederholen: <code>install/.installed</code> löschen
</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; ?>
<!-- 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>
<p class="i-hint" style="margin-bottom:16px">Die Datenbank muss bereits existieren (leer) — Tabellen werden automatisch angelegt.</p>
</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" 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>
<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';
});
});
// 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 >= 8) score++;
if (v.length >= 12) score++;
if (/[A-Z]/.test(v)) score++;
if (/[0-9]/.test(v)) score++;
if (/[^A-Za-z0-9]/.test(v)) 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>