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; $beforeRow = $pdo->prepare('SELECT is_on_vacation, vacation_substitute_id FROM staff WHERE id = ?'); $beforeRow->execute([$staff['id']]); $beforeRow = $beforeRow->fetch() ?: ['is_on_vacation' => 0, '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'); // Offene Tickets an die Vertretung weitergeben bzw. nach dem Urlaub zurückholen [$handed, $back] = vacation_apply_change($pdo, (int) $staff['id'], [$beforeRow['is_on_vacation'], $beforeRow['vacation_substitute_id']], [$onVacation, $substituteId], !empty($_POST['hand_over'])); flash('success', 'Einstellungen gespeichert.' . ($handed ? ' ' . $handed . ' offene Tickets wurden an die Vertretung weitergegeben.' : '') . ($back ? ' ' . $back . ' Tickets sind wieder bei Ihnen.' : '')); 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'); } // ── Von allen Geräten abmelden ───────────────────────────────── if ($action === 'sign_out_everywhere') { $keep = ($_POST['scope'] ?? 'others') !== 'all'; $r = staff_sign_out_everywhere($pdo, (int) $staff['id'], (int) $staff['id'], $keep); if ($keep) { flash('success', 'Alle anderen Geräte sind abgemeldet. Beendete App-Anmeldungen: ' . $r['apps'] . '. Diese Sitzung bleibt angemeldet.'); redirect('/staff/profile.php?tab=security'); } staff_logout(); flash('success', 'Sie sind von allen Geräten abgemeldet. Bitte melden Sie sich neu an.'); redirect('/staff/login.php'); } // ── Gemerktes Gerät entfernen (Anmelde-Wächter) ──────────────── if ($action === 'forget_device') { $pdo->prepare('DELETE FROM login_devices WHERE id = ? AND staff_id = ?')->execute([(int) ($_POST['device_id'] ?? 0), $staff['id']]); flash('success', 'Gerät entfernt.'); redirect('/staff/profile.php?tab=security'); } // ── Passkey entfernen (das Passwort wird bestätigt) ─────────── if ($action === 'delete_passkey') { $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 { $del = $pdo->prepare('DELETE FROM staff_passkeys WHERE id = ? AND staff_id = ?'); $del->execute([(int) ($_POST['passkey_id'] ?? 0), $staff['id']]); if ($del->rowCount() > 0) { audit_log($staff['id'], 'Passkey entfernt'); login_guard_record($pdo, 'passkey_removed', (int) $staff['id'], (string) $staff['email'], 'web', 'Passkey vom Mitarbeiter selbst entfernt'); flash('success', 'Passkey entfernt.'); } } redirect('/staff/profile.php?tab=security'); } // ── 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'; ?>
Erhalte Desktop-Benachrichtigungen bei neuen Tickets und Antworten — auch wenn der Browser-Tab nicht aktiv ist. Funktioniert in Chrome, Firefox und Edge. Erfordert HTTPS.
Push-Benachrichtigungen sind auf diesem Server nicht verfügbar (OpenSSL-Erweiterung fehlt oder PHP zu alt).
Wird automatisch an Ihre ausgehenden Ticket-Antworten angehängt. Ist keine Mitarbeiter-Signatur gesetzt, greift die Abteilungs-Signatur.
Vorschau:
Um 2FA zu deaktivieren, bestätigen Sie mit Ihrem Passwort:
Schützen Sie Ihr Konto mit einem zeitbasierten Einmalcode (TOTP – kompatibel mit Google Authenticator, Authy, etc.).
= e(chunk_split($setupSecret, 4, ' ')) ?>
Mit einem Passkey melden Sie sich ohne Passwort an: per Fingerabdruck, Gesicht oder Geräte-PIN (Windows Hello, Handy, Sicherheitsschlüssel). Der Passkey ersetzt Passwort und 2FA-Code; Passwort und 2FA bleiben als Rückfall bestehen. Ein Administrator kann Ihre Passkeys bei Verlust entfernen.
Passkeys gelten für die Domain = e($pkRp['id']) ?> (die Adresse, unter der Sie DeskHub aufrufen).
| Name | Hinzugefügt | Zuletzt benutzt | |
|---|---|---|---|
| = e($k['name']) ?> | = e(date('d.m.Y H:i', strtotime((string) $k['created_at']))) ?> | = $k['last_used_at'] ? e(date('d.m.Y H:i', strtotime((string) $k['last_used_at']))) : 'noch nie' ?> |
Noch kein Passkey eingerichtet.
Beendet alle Anmeldungen in Browsern und in den Apps (Android, Windows-Programm, Chrome-Erweiterung) und stoppt die Push-Benachrichtigungen auf diesen Geräten. Sinnvoll, wenn ein Gerät verloren ging oder Sie sich irgendwo angemeldet gelassen haben. Ihr Passwort bleibt unverändert.
Von diesen Geräten haben Sie sich schon angemeldet. Meldet sich jemand von einem neuen Gerät oder Ort an, bekommen Sie eine Mail. Ein Gerät, das Sie nicht kennen, können Sie hier entfernen; ändern Sie dann auch Ihr Passwort.
Noch keine Geräte gemerkt.
| Gerät | Letzter Ort | Zuletzt gesehen | |
|---|---|---|---|
| = e($d['label']) ?> = $d['channel'] === 'app' ? 'App' : 'Browser' ?>, seit = e(date('d.m.Y', strtotime((string) $d['first_seen']))) ?> |
= e($d['place']) ?> = e($d['last_ip']) ?> |
= e(date('d.m.Y H:i', strtotime((string) $d['last_seen']))) ?> |
| = e(date('d.m.Y H:i', strtotime((string) $ev['created_at']))) ?> | = e($eventLabels[$ev['event']] ?? $ev['event']) ?> | = e($ev['device']) ?> | = e($ev['place']) ?> (= e($ev['ip']) ?>) |