Files
deskhub/DeskHub-Webseite/staff/login.php
T
2026-11-05 21:21:52 +00:00

200 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
require_once __DIR__ . '/../includes/totp.php';
require_once __DIR__ . '/../includes/webauthn.php';
if (current_staff()) {
redirect('/staff/reports.php');
}
$errors = [];
$need2fa = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !csrf_valid()) {
// Die Anmeldeseite war so lange offen, dass die Sitzung auf dem Server inzwischen aufgeräumt wurde: neu anmelden statt Fehlerseite
$errors[] = 'Die Anmeldeseite war zu lange geöffnet und die Sitzung ist abgelaufen. Bitte melden Sie sich jetzt noch einmal an.';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'login';
if ($action === 'verify_totp') {
$pendingId = $_SESSION['totp_pending_staff_id'] ?? null;
$code = trim($_POST['totp_code'] ?? '');
if (!$pendingId) {
$errors[] = 'Sitzung abgelaufen. Bitte neu anmelden.';
} else {
$stmt = get_db()->prepare('SELECT * FROM staff WHERE id = ? AND is_active = 1');
$stmt->execute([$pendingId]);
$staff = $stmt->fetch();
if ($staff && !rate_limit_check('staff_totp_attempt', (string) $pendingId, login_guard_max_attempts(), login_guard_lock_minutes())) {
// Code-Raten bremsen: nach zu vielen Fehlversuchen muss das Passwort neu eingegeben werden
unset($_SESSION['totp_pending_staff_id']);
login_guard_locked((string) $staff['email'], 'web');
$errors[] = 'Zu viele falsche Codes. Bitte warten Sie einige Minuten und melden Sie sich neu an.';
} elseif ($staff && totp_verify($staff['totp_secret'], $code)) {
unset($_SESSION['totp_pending_staff_id']);
rate_limit_reset('staff_totp_attempt', (string) $pendingId);
staff_login($staff);
redirect('/staff/reports.php');
} else {
if ($staff) login_guard_failed($staff, (string) $staff['email'], 'web', 'Falscher 2FA-Code');
$errors[] = 'Ungültiger Code. Bitte erneut versuchen.';
$need2fa = true;
}
}
} else {
$email = trim($_POST['email'] ?? '');
$pass = $_POST['password'] ?? '';
if (!rate_limit_check('staff_login_attempt', client_ip(), 10, 15)) {
$errors[] = 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in einigen Minuten erneut.';
} elseif ($email && !rate_limit_check('staff_login_email', strtolower($email), login_guard_max_attempts(), login_guard_lock_minutes()) && !login_guard_known_device_for_email($email)) {
// Konto-basierte Sperre – verhindert Brute-Force über wechselnde IPs. Ein bekanntes Gerät des Kontos (Cookie von einer früheren Anmeldung)
// wird davon nicht ausgesperrt: Sonst könnte jeder, der die Adresse kennt, den echten Mitarbeiter mit Fehlversuchen dauerhaft aussperren.
// Die Sperre je IP-Adresse gilt auch für bekannte Geräte.
login_guard_locked($email, 'web');
$errors[] = 'Zu viele Anmeldeversuche für dieses Konto. Bitte warten Sie einige Minuten.';
} else {
$stmt = get_db()->prepare('SELECT * FROM staff WHERE LOWER(email) = LOWER(?) AND is_active = 1');
$stmt->execute([$email]);
$staff = $stmt->fetch();
if ($staff && password_verify($pass, $staff['password_hash'])) {
// Erfolgreicher Login – Zähler zurücksetzen
rate_limit_reset('staff_login_email', strtolower($email));
if ($staff['totp_enabled']) {
$_SESSION['totp_pending_staff_id'] = $staff['id'];
$need2fa = true;
} else {
staff_login($staff);
redirect('/staff/reports.php');
}
} else {
login_guard_failed($staff ?: null, $email, 'web', $staff ? 'Falsches Passwort' : 'Unbekannte E-Mail-Adresse');
$errors[] = 'E-Mail-Adresse oder Passwort ist falsch.';
}
}
}
}
$pageTitle = 'Mitarbeiter-Login';
$activePage = '';
// Staff-Header hat Sidebar – für Login-Seite eigenes schlankes HTML
$banner = get_setting('banner_path');
$logo = get_setting('logo_path');
$displayName = get_setting('display_name', config()['site']['name']);
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mitarbeiter-Login – <?= e($displayName) ?></title>
<link rel="stylesheet" href="/assets/css/style.css?v=<?= @filemtime(__DIR__ . "/../assets/css/style.css") ?: time() ?>">
<?php $customThemeStylesheet = active_custom_theme_stylesheet(); ?>
<?php if ($customThemeStylesheet): ?>
<link rel="stylesheet" href="<?= e($customThemeStylesheet) ?>">
<?php endif; ?>
<link rel="manifest" href="/manifest-staff.php">
<meta name="theme-color" content="#0f6f6a">
<?= site_icon_links() ?>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(function(){});}</script>
</head>
<body class="login-page staff-login-page"<?php if ($banner): ?> style="background-image:url('<?= e($banner) ?>'); background-size:cover; background-position:center"<?php endif; ?>>
<?= i18n_switcher_html('floating') ?>
<!-- Schlanker Header -->
<header class="staff-login-header">
<div class="staff-login-brand">
<?php if ($logo): ?>
<img src="<?= e($logo) ?>" alt="">
<?php endif; ?>
<?php if (!$logo || get_setting('logo_hide_name', '0') !== '1'): ?>
<span class="staff-login-brand-name"><?= e($displayName) ?></span>
<?php endif; ?>
</div>
<a href="/login.php" class="staff-login-back">← Zum Kundenportal</a>
</header>
<!-- Auth-Bereich -->
<div class="login-screen staff-login-screen">
<div class="auth-card login-card staff-login-card">
<h1 class="staff-login-title">Mitarbeiterbereich</h1>
<div class="sub">Anmeldung für Support-Mitarbeiter und Administratoren.</div>
<?php foreach ($errors as $err): ?>
<div class="flash error"><?= e($err) ?></div>
<?php endforeach; ?>
<?php foreach (get_flashes() as $f): ?>
<div class="flash <?= e($f['type']) ?>"><?= e($f['message']) ?></div>
<?php endforeach; ?>
<?php if ($need2fa): ?>
<p style="font-size:.88rem; color:#64748b; margin-bottom:14px">Bitte geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein.</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="verify_totp">
<label for="totp_code">Authenticator-Code</label>
<input type="text" id="totp_code" name="totp_code" inputmode="numeric" pattern="\d{6}" maxlength="6" required autofocus placeholder="000000" style="letter-spacing:.2em; font-size:1.3rem; text-align:center">
<div class="btn-row">
<button type="submit" class="btn login-submit">Verifizieren</button>
</div>
</form>
<p style="margin-top:14px; font-size:.8rem; text-align:center"><a href="/staff/login.php">← Zurück zum Login</a></p>
<?php else: ?>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="login">
<label for="email">E-Mail-Adresse</label>
<input type="email" id="email" name="email" required autofocus autocomplete="email" autocapitalize="none" autocorrect="off" spellcheck="false">
<label for="password">Passwort</label>
<input type="password" id="password" name="password" required autocomplete="current-password" autocapitalize="none" autocorrect="off" spellcheck="false">
<div style="text-align:right; margin-top:6px">
<a href="/staff/forgot_password.php" style="font-size:.8rem">Passwort vergessen?</a>
</div>
<div class="btn-row">
<button type="submit" class="btn login-submit">Anmelden</button>
</div>
</form>
<?php endif; ?>
<?php if (!$need2fa): ?>
<?php
$loginButtons = (string) plugin_filter('staff.login.buttons', '', []);
$passkeyOn = webauthn_enabled();
// Gleiche Gestaltung wie die Knöpfe der Anmelde-Plugins (zum Beispiel authentik): ein Block, gleiche Größe, Symbol vor der Beschriftung
$passkeyBtn = $passkeyOn ? '<button type="button" id="passkey-login" class="btn secondary" style="display:none;text-align:center;width:100%"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:-5px;margin-right:8px"><circle cx="8" cy="15" r="4"/><path d="M10.85 12.15 19 4"/><path d="m18 5 3 3"/><path d="m15 8 2 2"/></svg>Mit Passkey anmelden</button><div id="passkey-msg" class="flash error" style="display:none;margin:0"></div>' : '';
?>
<?php if ($passkeyBtn . $loginButtons !== ''): ?>
<div class="login-alt" id="login-alt" style="margin-top:18px;padding-top:16px;border-top:1px solid #e2e8f0;display:grid;gap:10px"><?= $passkeyBtn . $loginButtons ?></div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<!-- Schlanker Footer -->
<footer class="staff-login-footer">
<span>Copyright © <?= date('Y') ?> <?= e($displayName) ?> – Alle Rechte vorbehalten.</span>
</footer>
<script src="/assets/js/passkey.js?v=<?= @filemtime(__DIR__ . '/../assets/js/passkey.js') ?: time() ?>"></script>
<script>
(function () {
var btn = document.getElementById('passkey-login'), msg = document.getElementById('passkey-msg'), alt = document.getElementById('login-alt');
if (!btn) return;
if (!window.DeskHubPasskey || !DeskHubPasskey.supported()) {
// Browser ohne Passkeys: Knopf bleibt weg, der Block auch, wenn sonst nichts darin steht
if (alt && alt.querySelectorAll('a, button').length <= 1) alt.style.display = 'none';
return;
}
btn.style.display = 'block';
btn.addEventListener('click', function () {
msg.style.display = 'none';
btn.disabled = true;
DeskHubPasskey.login(<?= json_encode(csrf_token()) ?>).then(function (r) { location.href = r.redirect || '/staff/reports.php'; })
.catch(function (e) { msg.textContent = e.message; msg.style.display = 'block'; btn.disabled = false; });
});
})();
// Hält die Sitzung der offenen Anmeldeseite am Leben: Der Server räumt ungenutzte Sitzungen nach etwa 24 Minuten auf, und dann wäre das
// Anmeldeformular ungültig. Ein kurzer Abruf alle 8 Minuten genügt.
setInterval(function () { fetch('/staff/session_ping.php?check=1', { credentials: 'same-origin', cache: 'no-store' }).catch(function () {}); }, 8 * 60 * 1000);
</script>
</body>
</html>