Files
intranet/web/intranet_repo/projects/authorization/reset-password.php
T

325 lines
16 KiB
PHP

<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/functions.php';
require_once __DIR__ . '/util/authorization_helpers.php';
authorization_start_session();
authorization_send_security_headers();
if (isset($_SESSION['user_id'])) {
authorization_redirect($g_httpHomePath, 302);
}
$allowedSteps = ['email', 'verify', 'newpassword'];
$step = (string) ($_GET['step'] ?? 'email');
if (!in_array($step, $allowedSteps, true)) {
$step = 'email';
}
$error = null;
$errorCode = (int) ($_GET['error'] ?? 0);
$errorMessages = [
1 => 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
3 => 'Ungültiger oder abgelaufener Bestätigungscode.',
5 => 'Passwort muss mindestens 8 Zeichen, einen Buchstaben und eine Zahl enthalten.',
6 => 'Passwörter stimmen nicht überein.',
7 => 'Sitzung abgelaufen. Bitte starten Sie den Vorgang erneut.',
];
if (isset($errorMessages[$errorCode])) {
$error = $errorMessages[$errorCode];
}
if ($step === 'verify' && !authorization_reset_request_is_active()) {
authorization_clear_reset_state();
authorization_redirect('reset-password.php?error=7', 302);
}
if ($step === 'newpassword' && !authorization_reset_is_authorized()) {
authorization_clear_reset_state();
authorization_redirect('reset-password.php?error=7', 302);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string) ($_POST['action'] ?? '');
if (!authorization_csrf_is_valid($_POST['csrf_token'] ?? null)) {
authorization_clear_reset_state();
authorization_redirect('reset-password.php?error=7');
}
$pdo = require $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/database.php';
$pdo->exec("SET timezone = 'Europe/Berlin'");
if ($action === 'request_otp') {
$email = trim((string) ($_POST['email'] ?? ''));
if (strlen($email) > 254 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
authorization_redirect('reset-password.php?error=1');
}
authorization_clear_reset_state();
$_SESSION['auth_reset_started_at'] = time();
$_SESSION['auth_reset_attempts'] = 0;
$_SESSION['auth_reset_user_id'] = 0;
$stmt = $pdo->prepare(
'SELECT ugi.user_id, ugi.phone, ugi.first_name, ugi.mail '
. 'FROM user_general_information ugi '
. 'INNER JOIN user_login ul ON ul.user_id = ugi.user_id '
. 'WHERE LOWER(ugi.mail) = LOWER(?) LIMIT 1'
);
$stmt->execute([$email]);
$resetUser = $stmt->fetch(PDO::FETCH_ASSOC);
if (is_array($resetUser)) {
$userId = (int) $resetUser['user_id'];
$otp = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$expiresAt = date('Y-m-d H:i:s', time() + AUTH_RESET_TTL_SECONDS);
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare('UPDATE password_reset_otp SET used = 1 WHERE user_id = ? AND used = 0');
$stmt->execute([$userId]);
$stmt = $pdo->prepare('INSERT INTO password_reset_otp (user_id, otp_code, expires_at) VALUES (?, ?, ?)');
$stmt->execute([$userId, $otp, $expiresAt]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('authorization reset OTP persistence failed: ' . $e->getMessage());
authorization_redirect('reset-password.php?step=verify&sent=1');
}
$mailSent = false;
try {
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/send_mail.php';
$greeting = !empty($resetUser['first_name']) ? 'Hallo ' . $resetUser['first_name'] : 'Hallo';
$mailMessage = $greeting . ",\n\n"
. "Sie haben das Zurücksetzen Ihres Passworts angefordert.\n\n"
. "Ihr Bestätigungscode lautet: {$otp}\n\n"
. "Der Code ist 10 Minuten gültig.\n\n"
. 'Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.';
$mailSent = send_intranet_mail((string) $resetUser['mail'], 'Ihr Bestätigungscode', $mailMessage);
} catch (Throwable $e) {
error_log('authorization reset mail delivery failed: ' . $e->getMessage());
}
$smsSent = authorization_send_reset_sms(authorization_normalize_phone((string) ($resetUser['phone'] ?? '')), $otp);
if ($mailSent || $smsSent) {
$_SESSION['auth_reset_user_id'] = $userId;
} else {
error_log('authorization reset delivery failed for user_id=' . $userId);
$stmt = $pdo->prepare('UPDATE password_reset_otp SET used = 1 WHERE user_id = ? AND used = 0');
$stmt->execute([$userId]);
}
}
// Immer dieselbe Antwort, damit aus dem Reset-Flow kein Kontostatus abgeleitet werden kann.
authorization_redirect('reset-password.php?step=verify&sent=1');
}
if ($action === 'verify_otp') {
if (!authorization_reset_request_is_active()) {
authorization_clear_reset_state();
authorization_redirect('reset-password.php?error=7');
}
$otp = trim((string) ($_POST['otp_code'] ?? ''));
$attempts = (int) ($_SESSION['auth_reset_attempts'] ?? 0) + 1;
$_SESSION['auth_reset_attempts'] = $attempts;
$userId = (int) ($_SESSION['auth_reset_user_id'] ?? 0);
if (!preg_match('/^[0-9]{6}$/', $otp) || $userId <= 0 || $attempts > AUTH_RESET_MAX_ATTEMPTS) {
if ($attempts >= AUTH_RESET_MAX_ATTEMPTS && $userId > 0) {
$stmt = $pdo->prepare('UPDATE password_reset_otp SET used = 1 WHERE user_id = ? AND used = 0');
$stmt->execute([$userId]);
authorization_clear_reset_state();
authorization_redirect('reset-password.php?error=7');
}
authorization_redirect('reset-password.php?step=verify&error=3');
}
$now = date('Y-m-d H:i:s');
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare(
'SELECT id FROM password_reset_otp '
. 'WHERE user_id = ? AND otp_code = ? AND used = 0 AND expires_at > ? '
. 'ORDER BY id DESC LIMIT 1 FOR UPDATE'
);
$stmt->execute([$userId, $otp, $now]);
$otpId = (int) ($stmt->fetchColumn() ?: 0);
if ($otpId <= 0) {
$pdo->rollBack();
authorization_redirect('reset-password.php?step=verify&error=3');
}
$stmt = $pdo->prepare('UPDATE password_reset_otp SET used = 1 WHERE id = ?');
$stmt->execute([$otpId]);
$pdo->commit();
session_regenerate_id(true);
$_SESSION['auth_reset_user_id'] = $userId;
$_SESSION['auth_reset_authorized_until'] = time() + AUTH_RESET_TTL_SECONDS;
unset($_SESSION['auth_reset_attempts']);
authorization_redirect('reset-password.php?step=newpassword');
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('authorization OTP verification failed: ' . $e->getMessage());
authorization_redirect('reset-password.php?step=verify&error=3');
}
}
if ($action === 'set_password') {
if (!authorization_reset_is_authorized()) {
authorization_clear_reset_state();
authorization_redirect('reset-password.php?error=7');
}
$newPassword = (string) ($_POST['new_password'] ?? '');
$confirmation = (string) ($_POST['new_password_confirm'] ?? '');
$passwordError = authorization_password_error($newPassword, $confirmation);
if ($passwordError !== null) {
$code = str_contains($passwordError, 'stimmen') ? 6 : 5;
authorization_redirect('reset-password.php?step=newpassword&error=' . $code);
}
$userId = (int) $_SESSION['auth_reset_user_id'];
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
$resetUser = null;
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare('UPDATE user_login SET password_hash = ? WHERE user_id = ?');
$stmt->execute([$passwordHash, $userId]);
if ($stmt->rowCount() !== 1) {
throw new RuntimeException('Reset target login no longer exists.');
}
$stmt = $pdo->prepare('UPDATE password_reset_otp SET used = 1 WHERE user_id = ? AND used = 0');
$stmt->execute([$userId]);
$stmt = $pdo->prepare('SELECT mail, first_name, last_name FROM user_general_information WHERE user_id = ?');
$stmt->execute([$userId]);
$resetUser = $stmt->fetch(PDO::FETCH_ASSOC);
if (!is_array($resetUser)) {
throw new RuntimeException('Reset target user no longer exists.');
}
$userName = trim(($resetUser['first_name'] ?? '') . ' ' . ($resetUser['last_name'] ?? ''));
if ($userName === '') {
$userName = (string) $resetUser['mail'];
}
$taskSubject = "Passwort selbst zurückgesetzt: {$userName}";
$taskMessage = "{$userName} ({$resetUser['mail']}) hat das eigene Passwort erfolgreich zurückgesetzt.";
$stmt = $pdo->prepare(
"INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) "
. "VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())"
);
$stmt->execute([$userName, $resetUser['mail'], $taskSubject, $taskMessage]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('authorization password reset failed: ' . $e->getMessage());
authorization_clear_reset_state();
authorization_redirect('reset-password.php?error=7');
}
try {
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/send_mail.php';
$loginUrl = authorization_public_url('/intranet_repo/projects/authorization/login.php');
$greeting = !empty($resetUser['first_name']) ? 'Hallo ' . $resetUser['first_name'] : 'Hallo';
$message = $greeting . ",\n\n"
. "Ihr Passwort für das Intranet der NAK Reinickendorf wurde erfolgreich geändert.\n\n"
. 'Sie können sich ab sofort mit Ihrem neuen Passwort anmelden.';
send_intranet_mail((string) $resetUser['mail'], 'Passwort erfolgreich geändert', $message, null, 'intranet@nak-reinickendorf.de', 'Jetzt anmelden', $loginUrl);
} catch (Throwable $e) {
error_log('authorization password reset notification failed: ' . $e->getMessage());
}
authorization_clear_reset_state();
session_regenerate_id(true);
authorization_redirect('login.php?message=1');
}
authorization_redirect('reset-password.php');
}
$csrfToken = authorization_csrf_token();
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/html_head.php'; ?>
<link rel="stylesheet" href="/intranet_repo/projects/_central/layouts/authorization.css">
<script src="/intranet_repo/projects/_central/scripts/authorization.js" defer></script>
</head>
<body>
<div class="login_panel">
<?php if ($error !== null): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" data-auth-toast>
<div class="nak-toast__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<span class="nak-toast__text"><?= authorization_html($error) ?></span>
</div>
<?php endif; ?>
<div class="login_content">
<div class="nak-logo-link auth-logo">
<img class="auth-logo__icon" src="/intranet_repo/projects/_central/img/nak-logo.svg" alt="NAK Logo">
<div class="auth-logo__text">
<div class="auth-logo__name">Neuapostolische Kirche</div>
<div class="auth-logo__gemeinde">Berlin-Reinickendorf</div>
</div>
</div>
<h2>Passwort zurücksetzen</h2>
<?php if ($step === 'email'): ?>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Geben Sie Ihre E-Mail-Adresse ein. Wenn ein passendes Konto vorhanden ist, senden wir einen Bestätigungscode an die hinterlegten Kontaktwege.</p>
<form method="post" id="otpRequestForm">
<input type="hidden" name="action" value="request_otp">
<input type="hidden" name="csrf_token" value="<?= authorization_html($csrfToken) ?>">
<label for="email">E-Mailadresse</label>
<div class="item"><input class="login_field" type="email" id="email" name="email" maxlength="254" required autocomplete="email"></div>
<button class="login_button" type="submit" id="requestBtn">Code anfordern</button>
</form>
<?php elseif ($step === 'verify'): ?>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);"><?= authorization_html(authorization_masked_reset_message()) ?></p>
<form method="post">
<input type="hidden" name="action" value="verify_otp">
<input type="hidden" name="csrf_token" value="<?= authorization_html($csrfToken) ?>">
<label for="otp_code">Bestätigungscode</label>
<div class="item"><input class="login_field" type="text" id="otp_code" name="otp_code" maxlength="6" inputmode="numeric" pattern="[0-9]{6}" placeholder="000000" required autocomplete="one-time-code" style="text-align:center;font-size:1.4em;letter-spacing:8px;"></div>
<input class="login_button" type="submit" value="Code bestätigen">
</form>
<?php elseif ($step === 'newpassword'): ?>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Ihr Code wurde bestätigt. Geben Sie nun Ihr neues Passwort ein.</p>
<form method="post">
<input type="hidden" name="action" value="set_password">
<input type="hidden" name="csrf_token" value="<?= authorization_html($csrfToken) ?>">
<label for="new_password">Neues Passwort</label>
<div class="item"><input class="login_field" type="password" id="new_password" name="new_password" required autocomplete="new-password"></div>
<label for="new_password_confirm">Neues Passwort wiederholen</label>
<div class="item"><input class="login_field" type="password" id="new_password_confirm" name="new_password_confirm" required autocomplete="new-password"></div>
<input class="login_button" type="submit" value="Passwort ändern">
</form>
<?php endif; ?>
<div class="small_text" align="center">
<br><a href="login.php">Zurück zur Anmeldung</a><br><br>
Probleme beim Zurücksetzen?<br>
<a href="#" onclick="openContactModal(); return false;">Admin kontaktieren</a>
</div>
</div>
</div>
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>