535 lines
26 KiB
PHP
535 lines
26 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||
require_once __DIR__ . '/../includes/totp.php';
|
||
$staff = require_staff_login();
|
||
$pdo = get_db();
|
||
|
||
$errors = [];
|
||
$tab = $_GET['tab'] ?? 'account';
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
verify_csrf();
|
||
$action = $_POST['action'] ?? '';
|
||
|
||
// ── Konto-Daten ─────────────────────────────────────────────
|
||
if ($action === 'save_account') {
|
||
$name = trim($_POST['name'] ?? '');
|
||
$email = trim($_POST['email'] ?? '');
|
||
$phone = trim($_POST['phone'] ?? '');
|
||
|
||
if ($name === '') $errors[] = 'Bitte einen Namen angeben.';
|
||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Bitte eine gültige E-Mail-Adresse angeben.';
|
||
|
||
// E-Mail-Duplikat prüfen (andere Mitarbeiter)
|
||
if (!$errors) {
|
||
$dup = $pdo->prepare('SELECT COUNT(*) FROM staff WHERE LOWER(email) = LOWER(?) AND id != ?');
|
||
$dup->execute([$email, $staff['id']]);
|
||
if ((int)$dup->fetchColumn()) $errors[] = 'Diese E-Mail-Adresse wird bereits von einem anderen Mitarbeiter verwendet.';
|
||
}
|
||
|
||
if (!$errors) {
|
||
$pdo->prepare('UPDATE staff SET name=?, email=?, phone=? WHERE id=?')
|
||
->execute([$name, $email, $phone ?: null, $staff['id']]);
|
||
audit_log($staff['id'], 'Profil aktualisiert: Name/E-Mail/Telefon');
|
||
flash('success', 'Kontodaten gespeichert.');
|
||
redirect('/staff/profile.php?tab=account');
|
||
}
|
||
$tab = 'account';
|
||
}
|
||
|
||
// ── Passwort ─────────────────────────────────────────────────
|
||
if ($action === 'save_password') {
|
||
$current = $_POST['current_password'] ?? '';
|
||
$new = $_POST['new_password'] ?? '';
|
||
$confirm = $_POST['confirm_password'] ?? '';
|
||
|
||
if (!password_verify($current, $staff['password_hash'])) {
|
||
$errors[] = 'Das aktuelle Passwort ist falsch.';
|
||
}
|
||
$pwError = validate_password_strength($new);
|
||
if ($pwError) {
|
||
$errors[] = $pwError;
|
||
}
|
||
if ($new !== $confirm) {
|
||
$errors[] = 'Die Passwörter stimmen nicht überein.';
|
||
}
|
||
|
||
if (!$errors) {
|
||
$pdo->prepare('UPDATE staff SET password_hash=? WHERE id=?')
|
||
->execute([password_hash($new, PASSWORD_DEFAULT), $staff['id']]);
|
||
audit_log($staff['id'], 'Passwort geändert');
|
||
flash('success', 'Passwort geändert.');
|
||
redirect('/staff/profile.php?tab=account');
|
||
}
|
||
$tab = 'account';
|
||
}
|
||
|
||
// ── Avatar ────────────────────────────────────────────────────
|
||
if ($action === 'save_avatar') {
|
||
if (!empty($_FILES['avatar']['name'])) {
|
||
$allowed = ['jpg','jpeg','png','gif','webp'];
|
||
$ext = strtolower(pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION));
|
||
if (!in_array($ext, $allowed)) {
|
||
$errors[] = 'Erlaubte Bildformate: JPG, PNG, GIF, WEBP.';
|
||
} elseif ($_FILES['avatar']['size'] > 2 * 1024 * 1024) {
|
||
$errors[] = 'Maximale Dateigröße: 2 MB.';
|
||
} elseif (($imgErr = favicon_validate_file($_FILES['avatar']['tmp_name'], $ext)) !== null) {
|
||
$errors[] = $imgErr;
|
||
} else {
|
||
$dir = __DIR__ . '/../assets/avatars/';
|
||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||
// Alten Avatar löschen
|
||
if ($staff['avatar_path']) {
|
||
$old = __DIR__ . '/..' . $staff['avatar_path'];
|
||
if (file_exists($old)) unlink($old);
|
||
}
|
||
$filename = 'avatar_' . $staff['id'] . '_' . time() . '.' . $ext;
|
||
move_uploaded_file($_FILES['avatar']['tmp_name'], $dir . $filename);
|
||
$path = '/assets/avatars/' . $filename;
|
||
$pdo->prepare('UPDATE staff SET avatar_path=? WHERE id=?')->execute([$path, $staff['id']]);
|
||
flash('success', 'Avatar aktualisiert.');
|
||
redirect('/staff/profile.php?tab=account');
|
||
}
|
||
}
|
||
$tab = 'account';
|
||
}
|
||
|
||
if ($action === 'delete_avatar') {
|
||
if ($staff['avatar_path']) {
|
||
$old = __DIR__ . '/..' . $staff['avatar_path'];
|
||
if (file_exists($old)) unlink($old);
|
||
}
|
||
$pdo->prepare('UPDATE staff SET avatar_path=NULL WHERE id=?')->execute([$staff['id']]);
|
||
flash('success', 'Avatar entfernt.');
|
||
redirect('/staff/profile.php?tab=account');
|
||
}
|
||
|
||
// ── Urlaub/Abwesenheit ─────────────────────────────────────
|
||
if ($action === 'save_settings') {
|
||
$onVacation = !empty($_POST['is_on_vacation']) ? 1 : 0;
|
||
$substituteId = ($_POST['vacation_substitute_id'] ?? '') !== '' ? (int)$_POST['vacation_substitute_id'] : null;
|
||
$pdo->prepare('UPDATE staff SET is_on_vacation=?, vacation_substitute_id=? WHERE id=?')
|
||
->execute([$onVacation, $substituteId, $staff['id']]);
|
||
audit_log($staff['id'], $onVacation ? 'Abwesenheit aktiviert' : 'Abwesenheit deaktiviert');
|
||
flash('success', 'Einstellungen gespeichert.');
|
||
redirect('/staff/profile.php?tab=settings');
|
||
}
|
||
|
||
// ── Android-Push: welche Popups, stille Zeiten ─────────────────
|
||
if ($action === 'save_push_prefs') {
|
||
$ev = array_map('strval', (array) ($_POST['push_events'] ?? []));
|
||
$qf = trim((string) ($_POST['quiet_from'] ?? ''));
|
||
$qt = trim((string) ($_POST['quiet_to'] ?? ''));
|
||
if (($qf !== '' || $qt !== '') && (!fcm_time_valid($qf) || !fcm_time_valid($qt))) {
|
||
flash('error', 'Stille Zeiten bitte als Uhrzeit von/bis angeben (z. B. 22:00 und 06:00).');
|
||
redirect('/staff/profile.php?tab=account');
|
||
}
|
||
fcm_save_prefs($pdo, (int) $staff['id'], $ev, $qf, $qt, array_keys(fcm_event_types_for($staff)));
|
||
flash('success', 'Push-Einstellungen gespeichert.');
|
||
redirect('/staff/profile.php?tab=account');
|
||
}
|
||
|
||
// ── 2FA aktivieren ─────────────────────────────────────────────
|
||
if ($action === 'enable_2fa') {
|
||
$code = trim($_POST['totp_code'] ?? '');
|
||
$secret = $_SESSION['totp_setup_secret'] ?? '';
|
||
if (!$secret) {
|
||
flash('error', 'Sitzungsfehler. Bitte nochmals versuchen.');
|
||
} elseif (!totp_verify($secret, $code)) {
|
||
flash('error', 'Code ungültig. Bitte erneut versuchen.');
|
||
} else {
|
||
$pdo->prepare('UPDATE staff SET totp_secret=?, totp_enabled=1 WHERE id=?')->execute([$secret, $staff['id']]);
|
||
unset($_SESSION['totp_setup_secret']);
|
||
audit_log($staff['id'], 'Zwei-Faktor-Authentifizierung aktiviert');
|
||
flash('success', '2FA wurde erfolgreich aktiviert.');
|
||
}
|
||
redirect('/staff/profile.php?tab=security');
|
||
}
|
||
|
||
// ── 2FA deaktivieren ───────────────────────────────────────────
|
||
if ($action === 'disable_2fa') {
|
||
$pass = $_POST['confirm_password'] ?? '';
|
||
$me = $pdo->prepare('SELECT password_hash FROM staff WHERE id=?');
|
||
$me->execute([$staff['id']]);
|
||
$me = $me->fetch();
|
||
if (!password_verify($pass, $me['password_hash'])) {
|
||
flash('error', 'Falsches Passwort.');
|
||
} else {
|
||
$pdo->prepare('UPDATE staff SET totp_secret=NULL, totp_enabled=0 WHERE id=?')->execute([$staff['id']]);
|
||
audit_log($staff['id'], 'Zwei-Faktor-Authentifizierung deaktiviert');
|
||
flash('success', '2FA wurde deaktiviert.');
|
||
}
|
||
redirect('/staff/profile.php?tab=security');
|
||
}
|
||
|
||
// ── Signatur ──────────────────────────────────────────────────
|
||
if ($action === 'save_signature') {
|
||
$sig = sanitize_staff_html(trim($_POST['signature'] ?? ''));
|
||
$pdo->prepare('UPDATE staff SET signature=? WHERE id=?')->execute([$sig ?: null, $staff['id']]);
|
||
flash('success', 'Signatur gespeichert.');
|
||
redirect('/staff/profile.php?tab=signature');
|
||
}
|
||
}
|
||
|
||
// Aktuellen Stand neu laden (nach Redirect relevant wenn $_SESSION cached)
|
||
$staffRow = $pdo->prepare('SELECT * FROM staff WHERE id = ?');
|
||
$staffRow->execute([$staff['id']]);
|
||
$me = $staffRow->fetch();
|
||
|
||
$pageTitle = 'Mein Profil';
|
||
$activePage = 'profile';
|
||
require __DIR__ . '/_header.php';
|
||
?>
|
||
|
||
<div style="max-width:740px">
|
||
|
||
<h1>Mein Profil</h1>
|
||
|
||
<?php foreach ($errors as $err): ?>
|
||
<div class="flash error"><?= e($err) ?></div>
|
||
<?php endforeach; ?>
|
||
|
||
<!-- Tab-Navigation -->
|
||
<div style="display:flex; border-bottom:2px solid var(--border); margin-bottom:24px; gap:0">
|
||
<?php foreach (['account' => '👤 Konto', 'settings' => '⚙ Einstellungen', 'signature' => '✍ Signatur', 'security' => '🔒 Sicherheit'] as $t => $label): ?>
|
||
<a href="/staff/profile.php?tab=<?= $t ?>"
|
||
style="padding:10px 22px; font-size:.9rem; font-weight:600; text-decoration:none; border-bottom:3px solid <?= $tab===$t ? 'var(--primary)' : 'transparent' ?>; margin-bottom:-2px; color:<?= $tab===$t ? 'var(--primary)' : 'var(--muted)' ?>">
|
||
<?= $label ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<!-- ══ KONTO ══════════════════════════════════════════════════ -->
|
||
<?php if ($tab === 'account'): ?>
|
||
|
||
<div class="card">
|
||
<h3>Profilbild</h3>
|
||
<div style="display:flex; gap:20px; align-items:flex-start">
|
||
<div style="flex-shrink:0">
|
||
<?php if ($me['avatar_path']): ?>
|
||
<img src="<?= e($me['avatar_path']) ?>" alt="Avatar"
|
||
style="width:80px; height:80px; border-radius:50%; object-fit:cover; border:2px solid var(--border)">
|
||
<?php else: ?>
|
||
<div style="width:80px; height:80px; border-radius:50%; background:var(--primary-lt); display:flex; align-items:center; justify-content:center; font-size:2rem; border:2px solid var(--border)">
|
||
<?= mb_strtoupper(mb_substr($me['name'], 0, 1)) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div style="flex:1">
|
||
<form method="post" enctype="multipart/form-data">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_avatar">
|
||
<label for="avatar">Neues Bild hochladen (JPG, PNG, WEBP · max. 2 MB)</label>
|
||
<div style="display:flex; gap:8px; align-items:center; margin-top:6px">
|
||
<input type="file" id="avatar" name="avatar" accept="image/*" style="flex:1">
|
||
<button type="submit" class="btn secondary" style="white-space:nowrap">Hochladen</button>
|
||
</div>
|
||
</form>
|
||
<?php if ($me['avatar_path']): ?>
|
||
<form method="post" style="margin-top:8px">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="delete_avatar">
|
||
<button type="submit" style="background:none;border:none;color:var(--danger);font-size:.82rem;cursor:pointer;padding:0">Bild entfernen</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Kontodaten</h3>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_account">
|
||
<label for="name">Name</label>
|
||
<input type="text" id="name" name="name" value="<?= e($me['name']) ?>" required>
|
||
<label for="email">E-Mail-Adresse</label>
|
||
<input type="email" id="email" name="email" value="<?= e($me['email']) ?>" required>
|
||
<label for="phone">Telefonnummer (optional)</label>
|
||
<input type="text" id="phone" name="phone" value="<?= e($me['phone'] ?? '') ?>" placeholder="z.B. +49 89 123456">
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn">Speichern</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Passwort ändern</h3>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_password">
|
||
<label for="current_password">Aktuelles Passwort</label>
|
||
<input type="password" id="current_password" name="current_password" required autocomplete="current-password">
|
||
<div class="field-row">
|
||
<div>
|
||
<label for="new_password">Neues Passwort</label>
|
||
<input type="password" id="new_password" name="new_password" required autocomplete="new-password" minlength="10" placeholder="mind. 10 Zeichen, Groß-/Klein, Ziffer, Sonderzeichen">
|
||
</div>
|
||
<div>
|
||
<label for="confirm_password">Passwort wiederholen</label>
|
||
<input type="password" id="confirm_password" name="confirm_password" required autocomplete="new-password">
|
||
</div>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn">Passwort ändern</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Push-Benachrichtigungen -->
|
||
<?php
|
||
$pushVapidKey = '';
|
||
if (file_exists(__DIR__ . '/../includes/push.php')) {
|
||
require_once __DIR__ . '/../includes/push.php';
|
||
$__keys = get_or_create_vapid_keys();
|
||
$pushVapidKey = $__keys['public_key'] ?? '';
|
||
}
|
||
?>
|
||
<div class="card">
|
||
<h3>Browser-Push-Benachrichtigungen</h3>
|
||
<?php if ($pushVapidKey): ?>
|
||
<p class="muted" style="font-size:.84rem;margin-bottom:16px">
|
||
Erhalte Desktop-Benachrichtigungen bei neuen Tickets und Antworten — auch wenn der Browser-Tab nicht aktiv ist.
|
||
Funktioniert in Chrome, Firefox und Edge. Erfordert HTTPS.
|
||
</p>
|
||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px">
|
||
<span id="push-status-icon" style="font-size:1.4rem">🔕</span>
|
||
<div>
|
||
<div id="push-status-text" style="font-size:.85rem;font-weight:500">Deaktiviert</div>
|
||
<div id="push-status-sub" style="font-size:.76rem;color:var(--muted)">Klicken Sie auf Aktivieren um Benachrichtigungen zu erhalten</div>
|
||
</div>
|
||
<button type="button" id="push-toggle-btn" onclick="togglePushNotifications()"
|
||
class="btn secondary" style="margin-left:auto">
|
||
Aktivieren
|
||
</button>
|
||
</div>
|
||
<div style="padding-top:14px;border-top:1px solid var(--border-soft)">
|
||
<div style="font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin-bottom:8px">Benachrichtigungen erhalten bei:</div>
|
||
<div style="display:flex;flex-direction:column;gap:5px;font-size:.83rem;color:var(--ink-soft)">
|
||
<div>✅ Neues Ticket in meiner Abteilung</div>
|
||
<div>✅ Neue Antwort auf mir zugewiesenes Ticket</div>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="push-vapid-key" value="<?= e($pushVapidKey) ?>">
|
||
<?php else: ?>
|
||
<p class="muted" style="font-size:.84rem">
|
||
Push-Benachrichtigungen sind auf diesem Server nicht verfügbar (OpenSSL-Erweiterung fehlt oder PHP zu alt).
|
||
</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php $__pp = fcm_prefs($pdo, (int) $staff['id']); ?>
|
||
<div class="card">
|
||
<h3>📱 Android-App: Welche Popups?</h3>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_push_prefs">
|
||
<p class="muted" style="font-size:.84rem;margin-bottom:10px">Wähle, wofür die App ein Popup zeigen soll.</p>
|
||
<?php foreach (fcm_event_types_for($staff) as $__k => $__label): ?>
|
||
<label style="display:flex;align-items:center;gap:8px;margin:6px 0;font-weight:400"><input type="checkbox" name="push_events[]" value="<?= e($__k) ?>" style="width:auto" <?= in_array($__k, $__pp['events'], true) ? 'checked' : '' ?>> <?= e($__label) ?></label>
|
||
<?php endforeach; ?>
|
||
<div style="margin-top:12px;font-size:.84rem;font-weight:600">Stille Zeiten (keine Popups)</div>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-top:6px">
|
||
von <input type="time" name="quiet_from" value="<?= e($__pp['quiet_from']) ?>" style="width:auto"> bis <input type="time" name="quiet_to" value="<?= e($__pp['quiet_to']) ?>" style="width:auto">
|
||
</div>
|
||
<p class="muted" style="font-size:.78rem;margin-top:4px">Beide Felder leer = immer Popups. Die Zeit gilt nach der Serverzeit (<?= e(date_default_timezone_get()) ?>), auch über Mitternacht (z. B. 22:00 bis 06:00).</p>
|
||
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- ══ EINSTELLUNGEN ══════════════════════════════════════════ -->
|
||
<?php elseif ($tab === 'settings'): ?>
|
||
|
||
<div class="card">
|
||
<h3>Abwesenheit / Urlaub</h3>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_settings">
|
||
<?php
|
||
$allAgents = get_db()->query('SELECT id, name FROM staff WHERE is_active=1 ORDER BY name')->fetchAll();
|
||
?>
|
||
<label style="display:flex; align-items:flex-start; gap:12px; cursor:pointer; font-weight:400">
|
||
<input type="checkbox" name="is_on_vacation" value="1" style="width:auto; margin-top:3px; flex-shrink:0" <?= $me['is_on_vacation'] ? 'checked' : '' ?>>
|
||
<span>
|
||
<strong>Urlaubsmodus aktiv</strong><br>
|
||
<span class="muted" style="font-size:.84rem">Neue Tickets werden nicht automatisch diesem Konto zugewiesen, während der Urlaubsmodus aktiv ist. Bereits zugewiesene Tickets bleiben unverändert.</span>
|
||
</span>
|
||
</label>
|
||
<label style="margin-top:14px">Vertretung (optional)</label>
|
||
<select name="vacation_substitute_id">
|
||
<option value="">– keine Vertretung –</option>
|
||
<?php foreach ($allAgents as $a): if ($a['id'] == $me['id']) continue; ?>
|
||
<option value="<?= $a['id'] ?>" <?= ($me['vacation_substitute_id'] ?? '') == $a['id'] ? 'selected' : '' ?>><?= e($a['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<p class="muted" style="font-size:.8rem">Neue Tickets werden während Ihrer Abwesenheit automatisch dieser Person zugewiesen.</p>
|
||
<?php if ($me['is_on_vacation']): ?>
|
||
<div class="flash info" style="margin-top:8px">Urlaubsmodus ist aktuell <strong>aktiv</strong>. Sie erhalten keine automatischen Ticket-Zuweisungen.</div>
|
||
<?php endif; ?>
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn">Speichern</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Übersicht</h3>
|
||
<div style="display:flex; flex-direction:column; gap:12px; font-size:.9rem">
|
||
<div style="display:flex; gap:16px">
|
||
<span style="color:var(--muted); width:130px; flex-shrink:0">Rolle</span>
|
||
<span><?= $me['role'] === 'admin' ? '<span class="badge" style="background:var(--accent)">Administrator</span>' : '<span class="badge" style="background:var(--primary)">Mitarbeiter</span>' ?></span>
|
||
</div>
|
||
<div style="display:flex; gap:16px">
|
||
<span style="color:var(--muted); width:130px; flex-shrink:0">Mitglied seit</span>
|
||
<span><?= format_dt($me['created_at']) ?></span>
|
||
</div>
|
||
<?php
|
||
$ticketCount = $pdo->prepare('SELECT COUNT(*) FROM tickets WHERE assigned_staff_id = ?');
|
||
$ticketCount->execute([$me['id']]);
|
||
$myCount = $ticketCount->fetchColumn();
|
||
?>
|
||
<div style="display:flex; gap:16px">
|
||
<span style="color:var(--muted); width:130px; flex-shrink:0">Zugewiesene Tickets</span>
|
||
<span><a href="/staff/tickets.php?assigned=me"><?= (int)$myCount ?></a></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ SIGNATUR ════════════════════════════════════════════════ -->
|
||
<?php elseif ($tab === 'signature'): ?>
|
||
|
||
<div class="card">
|
||
<h3>E-Mail-Signatur</h3>
|
||
<p class="muted" style="margin-bottom:14px; font-size:.875rem">
|
||
Wird automatisch an Ihre ausgehenden Ticket-Antworten angehängt.
|
||
Ist keine Mitarbeiter-Signatur gesetzt, greift die Abteilungs-Signatur.
|
||
</p>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save_signature">
|
||
<textarea id="signature" name="signature" data-richtext data-image-upload-url="/staff/upload_signature_image.php" style="min-height:180px"><?= e($me['signature'] ?? '') ?></textarea>
|
||
<p class="muted" style="font-size:.8rem;margin-top:8px">Bilder einfügen mit 🖼️. Zum Verkleinern oder Vergrößern das Bild anklicken: Mit dem runden Anfasser ziehen oder 25 / 50 / 75 % / Original wählen.</p>
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn">Speichern</button>
|
||
<button type="button" class="btn secondary" id="sig-clear">Löschen</button>
|
||
</div>
|
||
</form>
|
||
<?php if ($me['signature']): ?>
|
||
<div style="margin-top:20px; padding-top:16px; border-top:1px solid var(--border-soft)">
|
||
<p class="muted" style="font-size:.78rem; margin-bottom:8px">Vorschau:</p>
|
||
<div style="border:1px solid var(--border); border-radius:8px; padding:14px 16px; font-size:.9rem; line-height:1.65">
|
||
<?= $me['signature'] ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<script>
|
||
document.getElementById('sig-clear').addEventListener('click', function () {
|
||
var ta = document.getElementById('signature');
|
||
var ed = ta.previousElementSibling;
|
||
if (ed && ed.classList.contains('richtext-editable')) ed.innerHTML = '';
|
||
ta.value = '';
|
||
});
|
||
</script>
|
||
|
||
<?php elseif ($tab === 'security'): ?>
|
||
<?php
|
||
require_once __DIR__ . '/../includes/totp.php';
|
||
$me2 = get_db()->prepare('SELECT totp_enabled, totp_secret, email FROM staff WHERE id=?');
|
||
$me2->execute([$staff['id']]);
|
||
$me2 = $me2->fetch();
|
||
if (!$me2['totp_enabled'] && empty($_SESSION['totp_setup_secret'])) {
|
||
$_SESSION['totp_setup_secret'] = totp_generate_secret();
|
||
}
|
||
$setupSecret = $_SESSION['totp_setup_secret'] ?? '';
|
||
?>
|
||
<div class="card">
|
||
<h3>🔒 Zwei-Faktor-Authentifizierung (2FA)</h3>
|
||
<?php if ($me2['totp_enabled']): ?>
|
||
<div class="flash info">2FA ist aktuell <strong>aktiv</strong>.</div>
|
||
<p class="muted" style="margin-top:14px">Um 2FA zu deaktivieren, bestätigen Sie mit Ihrem Passwort:</p>
|
||
<form method="post" style="margin-top:10px">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="disable_2fa">
|
||
<input type="password" name="confirm_password" placeholder="Aktuelles Passwort" required>
|
||
<div class="btn-row">
|
||
<button type="submit" class="btn secondary" style="border-color:var(--danger);color:var(--danger)" onclick="return confirm('2FA wirklich deaktivieren?')">2FA deaktivieren</button>
|
||
</div>
|
||
</form>
|
||
<?php else: ?>
|
||
<p class="muted">Schützen Sie Ihr Konto mit einem zeitbasierten Einmalcode (TOTP – kompatibel mit Google Authenticator, Authy, etc.).</p>
|
||
<ol style="margin:16px 0; padding-left:20px; line-height:2">
|
||
<li>Installieren Sie <strong>Google Authenticator</strong> oder <strong>Authy</strong></li>
|
||
<li>Scannen Sie den QR-Code oder geben Sie den Schlüssel manuell ein</li>
|
||
<li>Tragen Sie den 6-stelligen Code ein und klicken Sie „2FA aktivieren"</li>
|
||
</ol>
|
||
<div style="display:flex; gap:24px; align-items:flex-start; flex-wrap:wrap; margin:20px 0">
|
||
<img src="<?= e(totp_qr_url($setupSecret, $me2['email'])) ?>" alt="QR-Code" style="width:160px;height:160px;border:4px solid var(--border);border-radius:8px">
|
||
<div>
|
||
<div style="font-size:.78rem; color:var(--muted); margin-bottom:6px">Manueller Schlüssel:</div>
|
||
<code style="font-size:.95rem; letter-spacing:.1em; background:var(--bg-soft); padding:8px 12px; border-radius:6px; display:block"><?= e(chunk_split($setupSecret, 4, ' ')) ?></code>
|
||
</div>
|
||
</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="enable_2fa">
|
||
<label for="totp_code">Code aus der App</label>
|
||
<input type="text" id="totp_code" name="totp_code" inputmode="numeric" pattern="\d{6}" maxlength="6" required placeholder="000000" style="letter-spacing:.2em;font-size:1.2rem;max-width:160px">
|
||
<div class="btn-row"><button type="submit" class="btn">2FA aktivieren</button></div>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
// Push-Status auf Profilseite prüfen und Button aktualisieren
|
||
(function() {
|
||
var keyInput = document.getElementById('push-vapid-key');
|
||
var btn = document.getElementById('push-toggle-btn');
|
||
var statusText = document.getElementById('push-status-text');
|
||
var statusSub = document.getElementById('push-status-sub');
|
||
var statusIcon = document.getElementById('push-status-icon');
|
||
|
||
if (!keyInput) return; // Push-Karte nicht sichtbar
|
||
|
||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||
statusText.textContent = 'Nicht unterstützt';
|
||
statusSub.textContent = 'Ihr Browser unterstützt keine Push-Benachrichtigungen.';
|
||
statusIcon.textContent = '⚠️';
|
||
btn.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
function setActive(active) {
|
||
if (active) {
|
||
statusIcon.textContent = '🔔';
|
||
statusText.textContent = 'Aktiv';
|
||
statusText.style.color = '#22c55e';
|
||
statusSub.textContent = 'Sie erhalten Desktop-Benachrichtigungen';
|
||
btn.textContent = 'Deaktivieren';
|
||
btn.style.borderColor = '#ef4444';
|
||
btn.style.color = '#ef4444';
|
||
} else {
|
||
statusIcon.textContent = '🔕';
|
||
statusText.textContent = 'Deaktiviert';
|
||
statusText.style.color = '';
|
||
statusSub.textContent = 'Klicken Sie auf Aktivieren um Benachrichtigungen zu erhalten';
|
||
btn.textContent = 'Aktivieren';
|
||
btn.style.borderColor = '';
|
||
btn.style.color = '';
|
||
}
|
||
}
|
||
|
||
navigator.serviceWorker.ready.then(function(reg) {
|
||
reg.pushManager.getSubscription().then(function(sub) {
|
||
setActive(!!(sub && Notification.permission === 'granted'));
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<?php require __DIR__ . '/_footer.php'; ?>
|