web - refactor core modules, harden authorization, simplify endpoints

This commit is contained in:
2026-08-29 11:15:54 +02:00
parent e1109a07ef
commit 79d857d686
71 changed files with 8269 additions and 9119 deletions
@@ -1,106 +1,96 @@
<?php /* Name: login.php
Beschreibung: Login-Seite (Authorisierung) */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
$sql = "SELECT
u.*,
l.password_hash AS password_hash
FROM
user_general_information AS u
INNER JOIN
user_login AS l ON u.user_id = l.user_id
WHERE
u.mail = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST["email"]]);
$user = $stmt->fetch();
if ($user) {
if (isset($user['password_hash']) && password_verify($_POST["password"], $user["password_hash"])) {
session_start();
session_regenerate_id();
$_SESSION["user_id"] = $user["user_id"];
header("Location: {$g_httpHomePath}");
exit;
}
}
$is_invalid = true;
}
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<script src="https://unpkg.com/just-validate@latest/dist/just-validate.production.min.js" defer></script>
<script src="validation.js" defer></script>
</head>
<body>
<div class="login_panel">
<?php if (isset($is_invalid) && $is_invalid): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastLogin">
<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">Ungültiger Login!</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastLogin').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php if (isset($_GET["message"]) && $_GET["message"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastPw">
<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"><path d="M20 6L9 17l-5-5"/></svg>
</div>
<span class="nak-toast__text">Passwort erfolgreich geändert. Sie können sich jetzt anmelden.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastPw').classList.add('nak-toast--visible');});</script>
<?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>Anmelden</h2>
<form method="post">
<div class="flexcolumn_center">
<input class="login_field" placeholder="E-Mailadresse" type="email" name="email" id="email"
value="<?= htmlspecialchars($_POST["email"] ?? "") ?>">
<input class="login_field" placeholder="Passwort" type="password" name="password" id="password">
<input class="login_button" type="submit" name="submit" value="Anmelden">
</div>
</form>
<div class="small_text" style="text-align:center;margin-top:24px;color:var(--gray-600);">
Sie haben noch keinen Account für das Intranet?<br>
<a href="signup.php">Registrieren</a><br>
<br>
Passwort vergessen?<br>
<a href="reset-password.php">Passwort zurücksetzen</a><br>
<br>
Probleme bei der Anmeldung?<br>
<a href="#" onclick="openContactModal(); return false;">Admin kontaktieren</a>
</div>
</div><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
<?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);
}
$isInvalid = false;
$email = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim((string) ($_POST['email'] ?? ''));
$loginPassword = (string) ($_POST['password'] ?? '');
if (!authorization_csrf_is_valid($_POST['csrf_token'] ?? null)) {
$isInvalid = true;
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL) || $loginPassword === '') {
$isInvalid = true;
} else {
$pdo = require $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/database.php';
$userId = authorization_authenticate($pdo, $email, $loginPassword);
if ($userId !== null) {
session_regenerate_id(true);
$_SESSION = ['user_id' => $userId];
authorization_redirect($g_httpHomePath);
}
$isInvalid = true;
}
}
$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 ($isInvalid): ?>
<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">Ungültige E-Mail-Adresse oder ungültiges Passwort.</span>
</div>
<?php endif; ?>
<?php if (($_GET['message'] ?? '') === '1'): ?>
<div class="nak-toast nak-toast--success 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"><path d="M20 6L9 17l-5-5"/></svg>
</div>
<span class="nak-toast__text">Passwort erfolgreich geändert. Sie können sich jetzt anmelden.</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>Anmelden</h2>
<form method="post" autocomplete="on">
<input type="hidden" name="csrf_token" value="<?= authorization_html($csrfToken) ?>">
<div class="flexcolumn_center">
<input class="login_field" placeholder="E-Mailadresse" type="email" name="email" id="email" value="<?= authorization_html($email) ?>" required autocomplete="username">
<input class="login_field" placeholder="Passwort" type="password" name="password" id="password" required autocomplete="current-password">
<input class="login_button" type="submit" value="Anmelden">
</div>
</form>
<div class="small_text" style="text-align:center;margin-top:24px;color:var(--gray-600);">
Sie haben noch keinen Account für das Intranet?<br>
<a href="signup.php">Registrieren</a><br><br>
Passwort vergessen?<br>
<a href="reset-password.php">Passwort zurücksetzen</a><br><br>
Probleme bei der Anmeldung?<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>
@@ -1,13 +1,22 @@
<?php /* Name: loginout.php
Beschreibung: Logout-Seite */
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/parameters.php';
require_once __DIR__ . '/util/authorization_helpers.php';
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
authorization_start_session();
authorization_send_security_headers();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'] ?? 'Lax',
]);
}
session_destroy();
header("Location: {$g_httpLoginPath}");
exit();
authorization_redirect($g_httpLoginPath, 302);
@@ -1,223 +0,0 @@
<?php /* Name: reset-password-process.php
Beschreibung: Verarbeitung des Passwort-Resets (3 Schritte)
- Schritt 1: OTP generieren und per SMS + Mail senden
- Schritt 2: OTP verifizieren → Reset-Token erzeugen
- Schritt 3: Neues Passwort setzen mit Reset-Token */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// PostgreSQL-Zeitzone auf PHP-Zeitzone setzen
$pdo->exec("SET timezone = 'Europe/Berlin'");
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: reset-password.php");
exit;
}
$action = $_POST["action"] ?? '';
// =============================================
// SCHRITT 1: OTP anfordern
// =============================================
if ($action === "request_otp") {
$email = trim($_POST["email"] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
header("Location: reset-password.php?error=1");
exit;
}
// Nutzer suchen (muss registriert sein = user_login existiert)
$stmt = $pdo->prepare("SELECT ugi.user_id, ugi.phone, ugi.first_name FROM user_general_information ugi INNER JOIN user_login ul ON ugi.user_id = ul.user_id WHERE ugi.mail = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if (!$user) {
header("Location: reset-password.php?error=1");
exit;
}
if (empty($user['phone'])) {
header("Location: reset-password.php?error=2");
exit;
}
// OTP generieren (6-stellig)
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
// Ablaufzeit als UNIX-Timestamp speichern (vermeidet Timezone-Probleme)
$expires_timestamp = time() + 600; // 10 Minuten
$expires_at = date('Y-m-d H:i:s', $expires_timestamp);
// Alte OTPs für diesen Nutzer invalidieren
$stmt = $pdo->prepare("UPDATE password_reset_otp SET used = 1 WHERE user_id = ? AND used = 0");
$stmt->execute([$user['user_id']]);
// Neues OTP speichern
$stmt = $pdo->prepare("INSERT INTO password_reset_otp (user_id, otp_code, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$user['user_id'], $otp, $expires_at]);
// SMS senden (wie im Seelsorgeplaner)
$phone_full = "+49" . $user['phone'];
$messageText = "Intranet NAK Reinickendorf\n\nIhr Bestaetigungscode: {$otp}\n\nDer Code ist 10 Minuten gueltig.";
$smsData = array(
"phone" => $phone_full,
"message" => $messageText
);
$options = array(
'http' => array(
'header' => "Content-Type: application/json; charset=utf-8",
'method' => 'POST',
'content' => json_encode($smsData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'timeout' => 10,
),
);
$context = stream_context_create($options);
$smsResult = @file_get_contents(rtrim(intranet_env('SMS_GATEWAY_URL'), '/') . '/send_sms', false, $context);
if ($smsResult === FALSE) {
header("Location: reset-password.php?error=4");
exit;
}
// OTP zusätzlich per Mail senden
$greeting = !empty($user['first_name']) ? "Hallo {$user['first_name']}" : "Hallo";
$mailBetreff = "Ihr Bestätigungscode";
$mailNachricht = "{$greeting},\n\n";
$mailNachricht .= "Sie haben das Zurücksetzen Ihres Passworts angefordert.\n\n";
$mailNachricht .= "Ihr Bestätigungscode lautet: {$otp}\n\n";
$mailNachricht .= "Der Code ist 10 Minuten gültig.\n\n";
$mailNachricht .= "Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.";
send_intranet_mail($email, $mailBetreff, $mailNachricht);
// Telefonnummer maskieren für Anzeige (nur letzte 3 Ziffern zeigen)
$phone_masked = str_repeat('*', max(0, strlen($user['phone']) - 3)) . substr($user['phone'], -3);
header("Location: reset-password.php?step=verify&uid={$user['user_id']}&phone=" . urlencode($phone_masked));
exit;
}
// =============================================
// SCHRITT 2: OTP verifizieren → Reset-Token erzeugen
// =============================================
if ($action === "verify_otp") {
$user_id = (int)($_POST["user_id"] ?? 0);
$otp_code = trim($_POST["otp_code"] ?? '');
if ($user_id === 0 || strlen($otp_code) !== 6) {
header("Location: reset-password.php?error=3");
exit;
}
// OTP prüfen – Vergleich mit PHP-Zeit statt DB NOW()
$now = date('Y-m-d H:i:s');
$stmt = $pdo->prepare("SELECT id FROM password_reset_otp WHERE user_id = ? AND otp_code = ? AND used = 0 AND expires_at > ?");
$stmt->execute([$user_id, $otp_code, $now]);
$otpRecord = $stmt->fetch();
if (!$otpRecord) {
header("Location: reset-password.php?step=verify&uid={$user_id}&error=3");
exit;
}
// OTP als verwendet markieren
$stmt = $pdo->prepare("UPDATE password_reset_otp SET used = 1 WHERE id = ?");
$stmt->execute([$otpRecord['id']]);
// Einmal-Token für Passwort-Änderung generieren (in Session speichern)
$reset_token = bin2hex(random_bytes(32));
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$_SESSION['reset_token'] = $reset_token;
$_SESSION['reset_user_id'] = $user_id;
$_SESSION['reset_token_expires'] = time() + 600; // 10 Minuten
header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token));
exit;
}
// =============================================
// SCHRITT 3: Neues Passwort setzen (nach OTP-Verifizierung)
// =============================================
if ($action === "set_password") {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$reset_token = $_POST["reset_token"] ?? '';
$new_password = $_POST["new_password"] ?? '';
$new_password_confirm = $_POST["new_password_confirm"] ?? '';
// Token validieren
if (empty($reset_token)
|| !isset($_SESSION['reset_token'])
|| $reset_token !== $_SESSION['reset_token']
|| !isset($_SESSION['reset_token_expires'])
|| $_SESSION['reset_token_expires'] < time()) {
// Session-Daten aufräumen
unset($_SESSION['reset_token'], $_SESSION['reset_user_id'], $_SESSION['reset_token_expires']);
header("Location: reset-password.php?error=7");
exit;
}
$user_id = (int)$_SESSION['reset_user_id'];
// Passwort-Validierung
if (strlen($new_password) < 8 || !preg_match("/[a-z]/i", $new_password) || !preg_match("/[0-9]/", $new_password)) {
header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token) . "&error=5");
exit;
}
if ($new_password !== $new_password_confirm) {
header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token) . "&error=6");
exit;
}
// Neues Passwort setzen
$password_hash = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE user_login SET password_hash = ? WHERE user_id = ?");
$stmt->execute([$password_hash, $user_id]);
// User-Daten laden für Mail und Vermerk
$stmt = $pdo->prepare("SELECT mail, first_name, last_name FROM user_general_information WHERE user_id = ?");
$stmt->execute([$user_id]);
$resetUserData = $stmt->fetch();
if ($resetUserData) {
$loginUrl = "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/authorization/login.php";
$greeting = !empty($resetUserData['first_name']) ? "Hallo {$resetUserData['first_name']}" : "Hallo";
$mailBetreff = "Passwort erfolgreich geändert";
$mailNachricht = "{$greeting},\n\n";
$mailNachricht .= "Ihr Passwort für das Intranet der NAK Reinickendorf wurde erfolgreich geändert.\n\n";
$mailNachricht .= "Sie können sich ab sofort mit Ihrem neuen Passwort anmelden.";
send_intranet_mail($resetUserData['mail'], $mailBetreff, $mailNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt anmelden", $loginUrl);
// Vermerk in erledigte Tasks
$userName = trim(($resetUserData['first_name'] ?? '') . ' ' . ($resetUserData['last_name'] ?? ''));
if (empty($userName)) $userName = $resetUserData['mail'];
$taskSubject = "Passwort selbst zurückgesetzt: {$userName}";
$taskMessage = "{$userName} ({$resetUserData['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, $resetUserData['mail'], $taskSubject, $taskMessage]);
}
// Session-Daten aufräumen
unset($_SESSION['reset_token'], $_SESSION['reset_user_id'], $_SESSION['reset_token_expires']);
header("Location: login.php?message=1");
exit;
}
header("Location: reset-password.php");
exit;
?>
@@ -1,159 +1,324 @@
<?php /* Name: reset-password.php
Beschreibung: Passwort-Reset per SMS OTP (3 Schritte)
- Schritt 1: E-Mail eingeben → OTP per SMS
- Schritt 2: OTP-Code verifizieren
- Schritt 3: Neues Passwort setzen */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
$step = $_GET["step"] ?? "email";
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.login_button { position: relative; }
.login_button.is-loading {
color: transparent !important;
pointer-events: none;
}
.login_button.is-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 22px;
margin: -11px 0 0 -11px;
border: 3px solid rgba(255,255,255,0.35);
border-top-color: #FFFFFF;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="login_panel">
<?php if (isset($_GET["error"])): ?>
<?php
$errorMessages = [
1 => "E-Mail-Adresse nicht gefunden oder nicht registriert.",
2 => "Keine Telefonnummer hinterlegt. Bitte wenden Sie sich an den Administrator.",
3 => "Ungültiger oder abgelaufener Code.",
4 => "SMS konnte nicht gesendet werden. Bitte versuchen Sie es erneut.",
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."
];
$errorNum = (int)$_GET["error"];
$errorText = $errorMessages[$errorNum] ?? "Unbekannter Fehler.";
?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<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"><?= htmlspecialchars($errorText) ?></span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php if (isset($_GET["success"]) && $_GET["success"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastOk">
<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"><path d="M20 6L9 17l-5-5"/></svg></div>
<span class="nak-toast__text">Passwort erfolgreich geändert. Sie können sich jetzt anmelden.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastOk').classList.add('nak-toast--visible');});</script>
<?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"): ?>
<!-- SCHRITT 1: E-Mail eingeben -->
<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. Ein Bestätigungscode wird an Ihre hinterlegte Telefonnummer und per Mail gesendet.</p>
<form action="reset-password-process.php" method="post" id="otpRequestForm">
<input type="hidden" name="action" value="request_otp">
<label for="email">E-Mailadresse</label>
<div class="item">
<input class="login_field" type="email" id="email" name="email" required>
</div>
<button class="login_button" type="submit" id="requestBtn">Code anfordern</button>
</form>
<script>
document.getElementById('otpRequestForm').addEventListener('submit', function() {
var btn = document.getElementById('requestBtn');
btn.classList.add('is-loading');
btn.disabled = true;
});
</script>
<?php elseif ($step === "verify"): ?>
<!-- SCHRITT 2: Nur OTP-Code eingeben -->
<?php $phone_masked = htmlspecialchars($_GET["phone"] ?? "***"); ?>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Ein 6-stelliger Code wurde an die Nummer +49 <?= $phone_masked ?> und an Ihre E-Mail-Adresse gesendet.</p>
<form action="reset-password-process.php" method="post">
<input type="hidden" name="action" value="verify_otp">
<input type="hidden" name="user_id" value="<?= (int)($_GET["uid"] ?? 0) ?>">
<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"): ?>
<!-- SCHRITT 3: Neues Passwort setzen (nur nach verifiziertem OTP) -->
<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 action="reset-password-process.php" method="post">
<input type="hidden" name="action" value="set_password">
<input type="hidden" name="reset_token" value="<?= htmlspecialchars($_GET["token"] ?? '') ?>">
<label for="new_password">Neues Passwort</label>
<div class="item">
<input class="login_field" type="password" id="new_password" name="new_password" required>
</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>
</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><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
<?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>
@@ -1,97 +0,0 @@
<?php /* Name: self-signup-process.php
Beschreibung: Verarbeitung der Selbstregistrierung
- Validierung der Eingaben
- Speicherung in pending_registrations
- Mail an Admin (samuel.bias@aol.de)
- Eintrag in admin_messages */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// Validierung
if (empty($_POST["first_name"])) {
die("Vorname ist erforderlich.");
}
if (empty($_POST["last_name"])) {
die("Nachname ist erforderlich.");
}
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
die("Gültige E-Mail-Adresse erforderlich.");
}
if (strlen($_POST["password"]) < 8) {
die("Passwort muss mindestens 8 Zeichen lang sein.");
}
if (!preg_match("/[a-z]/i", $_POST["password"])) {
die("Passwort muss mindestens einen Buchstaben enthalten.");
}
if (!preg_match("/[0-9]/", $_POST["password"])) {
die("Passwort muss mindestens eine Zahl enthalten.");
}
if ($_POST["password"] !== $_POST["password_confirmation"]) {
die("Passwörter stimmen nicht überein.");
}
$email = trim($_POST["email"]);
$first_name = trim($_POST["first_name"]);
$last_name = trim($_POST["last_name"]);
// Prüfen ob E-Mail bereits existiert (in user_general_information oder pending_registrations)
$checkSql = "SELECT COUNT(*) FROM user_general_information WHERE mail = ?";
$stmt = $pdo->prepare($checkSql);
$stmt->execute([$email]);
$count1 = $stmt->fetchColumn();
$checkSql2 = "SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'";
$stmt2 = $pdo->prepare($checkSql2);
$stmt2->execute([$email]);
$count2 = $stmt2->fetchColumn();
if ($count1 > 0 || $count2 > 0) {
header("Location: self-signup.php?error=2");
exit;
}
// Passwort hashen und Token generieren
$password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
$token = bin2hex(random_bytes(32));
// In pending_registrations speichern
$insertSql = "INSERT INTO pending_registrations (first_name, last_name, email, password_hash, token) VALUES (?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($insertSql);
if (!$stmt) {
header("Location: self-signup.php?error=1");
exit;
}
if (!$stmt->execute([$first_name, $last_name, $email, $password_hash, $token])) {
header("Location: self-signup.php?error=1");
exit;
}
$pending_id = $pdo->lastInsertId();
// Admin-Nachricht in Inbox speichern
$subject = "Neue Registrierungsanfrage: {$first_name} {$last_name}";
$message_text = "Neue Selbstregistrierung:\n\nName: {$first_name} {$last_name}\nE-Mail: {$email}\n\nBitte im Adminpanel freischalten.";
$data_json = json_encode(['pending_id' => $pending_id, 'token' => $token]);
$inboxSql = "INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, data_json) VALUES ('registration', ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($inboxSql);
$sender_name = "{$first_name} {$last_name}";
$stmt->execute([$sender_name, $email, $subject, $message_text, $data_json]);
// Mail an Admin-Team (Admin + Technik)
$admin_email = ADMIN_EMAIL;
$mail_betreff = "Intranet: Neue Registrierungsanfrage von {$first_name} {$last_name}";
$mail_nachricht = "Hallo Admin,\n\n";
$mail_nachricht .= "Es liegt eine neue Registrierungsanfrage vor:\n\n";
$mail_nachricht .= "Name: {$first_name} {$last_name}\n";
$mail_nachricht .= "E-Mail: {$email}\n\n";
$mail_nachricht .= "Bitte melden Sie sich im Adminpanel an, um die Anfrage zu bearbeiten.\n\n";
$mail_nachricht .= "-- Intranet NAK Reinickendorf";
send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht);
// Erfolg
header("Location: self-signup.php?success=1");
exit;
?>
@@ -1,101 +0,0 @@
<?php /* Name: self-signup.php
Beschreibung: Selbstregistrierung - Nutzer kann sich eigenständig registrieren.
Admin erhält Mail + Inbox-Nachricht zur Freischaltung. */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<script src="just-validate.js" defer></script>
<script src="validation.js" defer></script>
</head>
<body>
<div class="login_panel">
<?php if (isset($_GET["error"])): ?>
<?php if ($_GET["error"] == 1): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<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">Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php elseif ($_GET["error"] == 2): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<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">Diese E-Mailadresse ist bereits registriert oder es liegt bereits eine Anfrage vor.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php endif; ?>
<?php if (isset($_GET["success"]) && $_GET["success"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastOk">
<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"><path d="M20 6L9 17l-5-5"/></svg></div>
<span class="nak-toast__text">Ihre Registrierungsanfrage wurde gesendet. Sie erhalten eine Benachrichtigung, sobald Ihr Konto freigeschaltet wurde.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastOk').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<div class="login_content">
<div class="nak-logo-link" style="display:flex;align-items:flex-end;gap:12px;text-decoration:none;margin-bottom:24px;">
<img src="/intranet_repo/projects/_central/img/nak-logo.svg" alt="NAK Logo" style="width:52px;height:52px;flex-shrink:0;">
<div style="display:flex;flex-direction:column;justify-content:flex-end;line-height:1.2;">
<div style="font-family:var(--font-primary);font-weight:500;font-size:0.875rem;color:var(--gray-900);white-space:nowrap;">Neuapostolische Kirche</div>
<div style="font-family:var(--font-primary);font-weight:300;font-size:0.75rem;color:var(--gray-600);white-space:nowrap;">Berlin-Reinickendorf</div>
</div>
</div>
<h2>Registrierung beantragen</h2>
<h3 class="no_margins" align="center">Ihr Antrag wird von einem Administrator geprüft und freigeschaltet.</h3>
<form action="self-signup-process.php" method="post" id="signup" novalidate>
<label for="first_name"><br>Vorname</label>
<div class="item">
<input class="login_field" type="text" id="first_name" name="first_name" required>
</div>
<label for="last_name">Nachname</label>
<div class="item">
<input class="login_field" type="text" id="last_name" name="last_name" required>
</div>
<label for="email">E-Mailadresse</label>
<div class="item">
<input class="login_field" type="email" id="email" name="email" required>
</div>
<label for="password">Passwort</label>
<div class="item">
<input class="login_field" type="password" id="password" name="password" required>
</div>
<label for="password_confirmation">Passwort wiederholen</label>
<div class="item">
<input class="login_field" type="password" id="password_confirmation" name="password_confirmation" required>
</div>
<input class="login_button" type="submit" value="Registrierung beantragen">
</form>
<div class="small_text" align="center">
Sie haben bereits einen Account?
<br>
<a href="login.php">Anmelden</a>
<br><br>
Sie wurden bereits vom Admin vorregistriert?
<br>
<a href="signup.php">Direkt registrieren</a>
</div>
</div><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -1,118 +0,0 @@
<?php /* Name: signup-process.php
Beschreibung: Vereinte Registrierungs-Verarbeitung
- Prüft ob Nutzer vorregistriert ist → direkte Registrierung
- Wenn nicht → automatisch Antrag in pending_registrations + Mail an Admin */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// === Validierung ===
if (empty($_POST["first_name"])) {
die("Vorname ist erforderlich.");
}
if (empty($_POST["last_name"])) {
die("Nachname ist erforderlich.");
}
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
die("Gültige E-Mail-Adresse erforderlich.");
}
if (empty($_POST["phone"])) {
die("Telefonnummer ist erforderlich.");
}
if (strlen($_POST["password"]) < 8) {
die("Passwort muss mindestens 8 Zeichen lang sein.");
}
if (!preg_match("/[a-z]/i", $_POST["password"])) {
die("Passwort muss mindestens einen Buchstaben enthalten.");
}
if (!preg_match("/[0-9]/", $_POST["password"])) {
die("Passwort muss mindestens eine Zahl enthalten.");
}
if ($_POST["password"] !== $_POST["password_confirmation"]) {
die("Passwörter stimmen nicht überein.");
}
$email = trim($_POST["email"]);
$first_name = trim($_POST["first_name"]);
$last_name = trim($_POST["last_name"]);
$phone = preg_replace('/[^0-9]/', '', trim($_POST["phone"]));
$password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
// === Prüfung 1: Ist der Nutzer vom Admin vorregistriert? ===
$stmt = $pdo->prepare("SELECT user_id FROM user_general_information WHERE mail = ?");
$stmt->execute([$email]);
$user_id = $stmt->fetchColumn();
if ($user_id) {
// Nutzer ist vorregistriert – prüfen ob bereits Login existiert
$stmt = $pdo->prepare("SELECT COUNT(*) FROM user_login WHERE user_id = ?");
$stmt->execute([$user_id]);
$loginExists = $stmt->fetchColumn();
if ($loginExists > 0) {
// Bereits vollständig registriert
header("Location: signup.php?error=2");
exit;
}
// Direkte Registrierung durchführen
$stmt = $pdo->prepare("INSERT INTO user_login (user_id, password_hash) VALUES (?, ?)");
if ($stmt->execute([$user_id, $password_hash])) {
// Namen und Telefonnummer aktualisieren
$stmt = $pdo->prepare("UPDATE user_general_information SET first_name=?, last_name=?, phone=? WHERE user_id=?");
$stmt->execute([$first_name, $last_name, $phone, $user_id]);
header("Location: login.php");
exit;
} else {
header("Location: signup.php?error=1");
exit;
}
}
// === Prüfung 2: Liegt bereits ein Antrag vor? ===
$stmt = $pdo->prepare("SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'");
$stmt->execute([$email]);
$pendingCount = $stmt->fetchColumn();
if ($pendingCount > 0) {
header("Location: signup.php?error=2");
exit;
}
// === Antrag stellen (Nutzer nicht vorregistriert) ===
$token = bin2hex(random_bytes(32));
$stmt = $pdo->prepare("INSERT INTO pending_registrations (first_name, last_name, email, phone, password_hash, token) VALUES (?, ?, ?, ?, ?, ?)");
if (!$stmt->execute([$first_name, $last_name, $email, $phone, $password_hash, $token])) {
header("Location: signup.php?error=1");
exit;
}
$pending_id = $pdo->lastInsertId();
// Admin-Nachricht in Inbox speichern
$subject = "Neue Registrierungsanfrage: {$first_name} {$last_name}";
$message_text = "Neue Registrierungsanfrage:\n\nName: {$first_name} {$last_name}\nE-Mail: {$email}\n\nBitte im Adminpanel freischalten.";
$data_json = json_encode(['pending_id' => $pending_id, 'token' => $token]);
$stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, data_json) VALUES ('registration', ?, ?, ?, ?, ?)");
$sender_name = "{$first_name} {$last_name}";
$stmt->execute([$sender_name, $email, $subject, $message_text, $data_json]);
// Mail an Admin-Team (Admin + Technik)
$mail_betreff = "Neue Registrierungsanfrage von {$first_name} {$last_name}";
$mail_nachricht = "Es liegt eine neue Registrierungsanfrage vor:\n\n";
$mail_nachricht .= "Name: {$first_name} {$last_name}\n";
$mail_nachricht .= "E-Mail: {$email}\n";
$mail_nachricht .= "Telefon: {$phone}\n\n";
$mail_nachricht .= "Bitte melden Sie sich im Adminpanel an, um die Anfrage zu bearbeiten.";
send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht, "Im Adminpanel bearbeiten", "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/adminpanel/admin_inbox.php");
// Erfolg – gleiche Meldung wie bei direkter Registrierung (Nutzer merkt keinen Unterschied)
header("Location: signup.php?success=1");
exit;
?>
@@ -1,111 +1,136 @@
<?php /* Name: signup.php
Beschreibung: Registrierungsformular (vereint)
- Wenn Nutzer vorregistriert: direkte Registrierung
- Wenn nicht: Antrag wird automatisch gestellt
- Verwendet signup-process.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';
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/parameters.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/functions.php';
authorization_start_session();
authorization_send_security_headers();
if (isset($_SESSION['user_id'])) {
authorization_redirect($g_httpHomePath, 302);
}
$form = authorization_registration_input([]);
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form = authorization_registration_input($_POST);
if (!authorization_csrf_is_valid($_POST['csrf_token'] ?? null)) {
$error = 'Die Anfrage konnte nicht bestätigt werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.';
} else {
$error = authorization_registration_error($form);
}
if ($error === null) {
try {
$pdo = require $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/database.php';
$result = authorization_register($pdo, $form);
if ($result['status'] === 'pending') {
try {
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/send_mail.php';
$mailSubject = "Neue Registrierungsanfrage von {$form['first_name']} {$form['last_name']}";
$mailMessage = "Es liegt eine neue Registrierungsanfrage vor:\n\n"
. "Name: {$form['first_name']} {$form['last_name']}\n"
. "E-Mail: {$form['email']}\n"
. "Telefon: {$form['phone']}\n\n"
. 'Bitte melden Sie sich im Adminpanel an, um die Anfrage zu bearbeiten.';
send_admin_team_mail(
$pdo,
$mailSubject,
$mailMessage,
'Im Adminpanel bearbeiten',
authorization_public_url('/intranet_repo/projects/adminpanel/admin_inbox.php')
);
} catch (Throwable $e) {
// Die Anfrage ist bereits atomar gespeichert; Mailfehler dürfen keine Doppelregistrierung provozieren.
error_log('authorization signup notification failed: ' . $e->getMessage());
}
}
authorization_redirect('signup.php?success=1');
} catch (Throwable $e) {
error_log('authorization signup failed: ' . $e->getMessage());
$error = 'Die Registrierung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut.';
}
}
}
$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'; ?>
<!-- Quelle für just-validate.js: https://unpkg.com/just-validate@latest/dist/just-validate.production.min.js -->
<script src="just-validate.js" defer></script>
<script src="validation.js" defer></script>
<?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="just-validate.js" defer></script>
<script src="/intranet_repo/projects/_central/scripts/authorization.js" defer></script>
</head>
<body>
<div class="login_panel">
<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; ?>
<?php if (isset($_GET["error"])): ?>
<?php if ($_GET["error"] == 1): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<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">Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php elseif ($_GET["error"] == 2): ?>
<div class="nak-toast nak-toast--error nak-toast--inline" id="nakToastErr">
<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">Diese E-Mailadresse ist bereits registriert oder es liegt bereits eine Anfrage vor.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastErr').classList.add('nak-toast--visible');});</script>
<?php endif; ?>
<?php endif; ?>
<?php if (($_GET['success'] ?? '') === '1'): ?>
<div class="nak-toast nak-toast--success 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"><path d="M20 6L9 17l-5-5"/></svg></div>
<span class="nak-toast__text">Ihre Angaben wurden verarbeitet. Falls Ihr Konto direkt aktiviert werden konnte, können Sie sich anmelden. Andernfalls erhalten Sie nach der Prüfung eine Benachrichtigung.</span>
</div>
<?php endif; ?>
<?php if (isset($_GET["success"]) && $_GET["success"] == 1): ?>
<div class="nak-toast nak-toast--success nak-toast--inline" id="nakToastOk">
<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"><path d="M20 6L9 17l-5-5"/></svg></div>
<span class="nak-toast__text">Ihre Registrierung wurde beantragt. Sie erhalten eine Benachrichtigung, sobald Ihr Konto freigeschaltet wurde.</span>
</div>
<script>requestAnimationFrame(function(){document.getElementById('nakToastOk').classList.add('nak-toast--visible');});</script>
<?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>Registrieren</h2>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Erstellen Sie Ihren persönlichen Zugang zum Intranet.</p>
<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>Registrieren</h2>
<p style="text-align:center;color:var(--gray-600);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);">Erstellen Sie Ihren persönlichen Zugang zum Intranet.</p>
<form action="signup-process.php" method="post" id="signup" novalidate>
<label for="first_name">Vorname</label>
<div class="item">
<input class="login_field" type="text" id="first_name" name="first_name">
</div>
<label for="last_name">Nachname</label>
<div class="item">
<input class="login_field" type="text" id="last_name" name="last_name">
</div>
<label for="email">E-Mailadresse</label>
<div class="item">
<input class="login_field" type="email" id="email" name="email">
</div>
<form method="post" id="signup" novalidate autocomplete="on">
<input type="hidden" name="csrf_token" value="<?= authorization_html($csrfToken) ?>">
<label for="phone">Telefonnummer</label>
<div class="item">
<div style="display:flex;align-items:stretch;width:100%;max-width:100%;margin-bottom:10px;box-sizing:border-box;">
<div class="input-prefix" style="border-radius:0;border:1px solid var(--gray-300);border-right:none;margin:0;padding:14px 12px;font-weight:500;box-shadow:none;width:auto;background:var(--gray-200);color:var(--gray-700);display:flex;align-items:center;flex-shrink:0;">+49</div>
<input class="login_field" type="tel" id="phone" name="phone" style="border-radius:0;border-left:0;margin:0;flex:1;width:auto;min-width:0;">
</div>
</div>
<label for="password">Passwort</label>
<div class="item">
<input class="login_field" type="password" id="password" name="password">
</div>
<label for="password_confirmation">Passwort wiederholen</label>
<div class="item">
<input class="login_field" type="password" id="password_confirmation" name="password_confirmation">
</div>
<input class="login_button" type="submit" value="Registrieren">
</form>
<div class="small_text" align="center">
Sie haben bereits einen Account?
<br>
<a href="login.php">Anmelden</a><br>
<br>
Probleme bei der Registrierung?<br>
<a href="#" onclick="openContactModal(); return false;">Admin kontaktieren</a>
</div>
</div><!-- EOD: login_content -->
</div><!-- EOD: login_panel -->
<label for="first_name">Vorname</label>
<div class="item"><input class="login_field" type="text" id="first_name" name="first_name" value="<?= authorization_html($form['first_name']) ?>" maxlength="100" required autocomplete="given-name"></div>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
<label for="last_name">Nachname</label>
<div class="item"><input class="login_field" type="text" id="last_name" name="last_name" value="<?= authorization_html($form['last_name']) ?>" maxlength="100" required autocomplete="family-name"></div>
<label for="email">E-Mailadresse</label>
<div class="item"><input class="login_field" type="email" id="email" name="email" value="<?= authorization_html($form['email']) ?>" maxlength="254" required autocomplete="email"></div>
<label for="phone">Telefonnummer</label>
<div class="item">
<div style="display:flex;align-items:stretch;width:100%;max-width:100%;margin-bottom:10px;box-sizing:border-box;">
<div class="input-prefix" style="border-radius:0;border:1px solid var(--gray-300);border-right:none;margin:0;padding:14px 12px;font-weight:500;box-shadow:none;width:auto;background:var(--gray-200);color:var(--gray-700);display:flex;align-items:center;flex-shrink:0;">+49</div>
<input class="login_field" type="tel" id="phone" name="phone" value="<?= authorization_html($form['phone']) ?>" maxlength="20" style="border-radius:0;border-left:0;margin:0;flex:1;width:auto;min-width:0;" required autocomplete="tel-national">
</div>
</div>
<label for="password">Passwort</label>
<div class="item"><input class="login_field" type="password" id="password" name="password" required autocomplete="new-password"></div>
<label for="password_confirmation">Passwort wiederholen</label>
<div class="item"><input class="login_field" type="password" id="password_confirmation" name="password_confirmation" required autocomplete="new-password"></div>
<input class="login_button" type="submit" value="Registrieren">
</form>
<div class="small_text" align="center">
Sie haben bereits einen Account?<br>
<a href="login.php">Anmelden</a><br><br>
Probleme bei der Registrierung?<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>
</html>
@@ -0,0 +1,313 @@
<?php
const AUTH_RESET_TTL_SECONDS = 600;
const AUTH_RESET_MAX_ATTEMPTS = 5;
const AUTH_DUMMY_PASSWORD_HASH = '$2y$12$7v2tTTu3R6UkMcX7N9FvJeetK1LEkkkFe/qbj./2EdWVKKT0LS7RS';
function authorization_start_session(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
function authorization_send_security_headers(): void
{
header('Cache-Control: no-store, max-age=0');
header('Pragma: no-cache');
header('Referrer-Policy: no-referrer');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
}
function authorization_html($value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function authorization_csrf_token(): string
{
authorization_start_session();
if (empty($_SESSION['authorization_csrf'])) {
$_SESSION['authorization_csrf'] = bin2hex(random_bytes(32));
}
return (string) $_SESSION['authorization_csrf'];
}
function authorization_csrf_is_valid($postedToken): bool
{
authorization_start_session();
$sessionToken = $_SESSION['authorization_csrf'] ?? '';
return is_string($postedToken)
&& is_string($sessionToken)
&& $sessionToken !== ''
&& hash_equals($sessionToken, $postedToken);
}
function authorization_redirect(string $location, int $status = 303): never
{
header('Location: ' . $location, true, $status);
exit;
}
function authorization_password_error(string $password, ?string $confirmation = null): ?string
{
if (strlen($password) < 8 || !preg_match('/[a-z]/i', $password) || !preg_match('/[0-9]/', $password)) {
return 'Passwort muss mindestens 8 Zeichen, einen Buchstaben und eine Zahl enthalten.';
}
if ($confirmation !== null && !hash_equals($password, $confirmation)) {
return 'Passwörter stimmen nicht überein.';
}
return null;
}
function authorization_normalize_phone(string $phone): string
{
return preg_replace('/\D+/', '', trim($phone)) ?? '';
}
function authorization_registration_input(array $input): array
{
return [
'first_name' => trim((string) ($input['first_name'] ?? '')),
'last_name' => trim((string) ($input['last_name'] ?? '')),
'email' => trim((string) ($input['email'] ?? '')),
'phone' => authorization_normalize_phone((string) ($input['phone'] ?? '')),
'password' => (string) ($input['password'] ?? ''),
'password_confirmation' => (string) ($input['password_confirmation'] ?? ''),
];
}
function authorization_text_length(string $value): int
{
return function_exists('mb_strlen') ? mb_strlen($value) : strlen($value);
}
function authorization_registration_error(array $input): ?string
{
if ($input['first_name'] === '' || authorization_text_length($input['first_name']) > 100) {
return 'Bitte geben Sie einen gültigen Vornamen ein.';
}
if ($input['last_name'] === '' || authorization_text_length($input['last_name']) > 100) {
return 'Bitte geben Sie einen gültigen Nachnamen ein.';
}
if (strlen($input['email']) > 254 || !filter_var($input['email'], FILTER_VALIDATE_EMAIL)) {
return 'Bitte geben Sie eine gültige E-Mail-Adresse ein.';
}
if (!preg_match('/^[1-9][0-9]{5,14}$/', $input['phone'])) {
return 'Bitte geben Sie eine gültige Telefonnummer ohne führende 0 ein.';
}
return authorization_password_error($input['password'], $input['password_confirmation']);
}
function authorization_authenticate(PDO $pdo, string $email, string $password): ?int
{
$stmt = $pdo->prepare(
'SELECT u.user_id, l.password_hash '
. 'FROM user_general_information u '
. 'INNER JOIN user_login l ON l.user_id = u.user_id '
. 'WHERE LOWER(u.mail) = LOWER(?) '
. 'LIMIT 1'
);
$stmt->execute([trim($email)]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
$storedHash = is_array($user) && is_string($user['password_hash'] ?? null)
? $user['password_hash']
: AUTH_DUMMY_PASSWORD_HASH;
$valid = password_verify($password, $storedHash);
if (!$valid || !is_array($user)) {
return null;
}
$userId = (int) $user['user_id'];
if ($userId <= 0) {
return null;
}
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$update = $pdo->prepare('UPDATE user_login SET password_hash = ? WHERE user_id = ?');
$update->execute([$newHash, $userId]);
}
return $userId;
}
/**
* @return array{status:string,pending_id?:int,token?:string}
*/
function authorization_register(PDO $pdo, array $input): array
{
$passwordHash = password_hash($input['password'], PASSWORD_DEFAULT);
$email = $input['email'];
$pdo->beginTransaction();
try {
// Serialisiert parallele Registrierungsversuche derselben E-Mail ohne neues Schema.
$lock = $pdo->prepare('SELECT pg_advisory_xact_lock(hashtext(LOWER(?)))');
$lock->execute([$email]);
$stmt = $pdo->prepare(
'SELECT user_id FROM user_general_information '
. 'WHERE LOWER(mail) = LOWER(?) LIMIT 1 FOR UPDATE'
);
$stmt->execute([$email]);
$userId = (int) ($stmt->fetchColumn() ?: 0);
if ($userId > 0) {
$stmt = $pdo->prepare('SELECT 1 FROM user_login WHERE user_id = ? LIMIT 1');
$stmt->execute([$userId]);
if ($stmt->fetchColumn()) {
$pdo->commit();
return ['status' => 'existing'];
}
$stmt = $pdo->prepare('INSERT INTO user_login (user_id, password_hash) VALUES (?, ?)');
$stmt->execute([$userId, $passwordHash]);
$stmt = $pdo->prepare(
'UPDATE user_general_information '
. 'SET first_name = ?, last_name = ?, phone = ? WHERE user_id = ?'
);
$stmt->execute([$input['first_name'], $input['last_name'], $input['phone'], $userId]);
$pdo->commit();
return ['status' => 'activated'];
}
$stmt = $pdo->prepare(
"SELECT id FROM pending_registrations WHERE LOWER(email) = LOWER(?) AND status = 'pending' LIMIT 1 FOR UPDATE"
);
$stmt->execute([$email]);
if ($stmt->fetchColumn()) {
$pdo->commit();
return ['status' => 'existing'];
}
// Token bleibt für Schema-/Admin-Kompatibilität erhalten, wird aber nicht mehr in admin_messages dupliziert.
$token = bin2hex(random_bytes(32));
$stmt = $pdo->prepare(
'INSERT INTO pending_registrations '
. '(first_name, last_name, email, phone, password_hash, token) '
. 'VALUES (?, ?, ?, ?, ?, ?)'
);
$stmt->execute([
$input['first_name'],
$input['last_name'],
$email,
$input['phone'],
$passwordHash,
$token,
]);
$pendingId = (int) $pdo->lastInsertId();
$subject = "Neue Registrierungsanfrage: {$input['first_name']} {$input['last_name']}";
$message = "Neue Registrierungsanfrage:\n\n"
. "Name: {$input['first_name']} {$input['last_name']}\n"
. "E-Mail: {$email}\n\n"
. 'Bitte im Adminpanel freischalten.';
$dataJson = json_encode(['pending_id' => $pendingId], JSON_THROW_ON_ERROR);
$senderName = trim($input['first_name'] . ' ' . $input['last_name']);
$stmt = $pdo->prepare(
"INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, data_json) "
. "VALUES ('registration', ?, ?, ?, ?, ?)"
);
$stmt->execute([$senderName, $email, $subject, $message, $dataJson]);
$pdo->commit();
return ['status' => 'pending', 'pending_id' => $pendingId];
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}
function authorization_public_url(string $path): string
{
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/runtime_env.php';
$host = intranet_env('INTRANET_FQDN');
return 'https://' . $host . '/' . ltrim($path, '/');
}
function authorization_clear_reset_state(): void
{
authorization_start_session();
unset(
$_SESSION['auth_reset_user_id'],
$_SESSION['auth_reset_started_at'],
$_SESSION['auth_reset_attempts'],
$_SESSION['auth_reset_authorized_until']
);
}
function authorization_reset_request_is_active(): bool
{
authorization_start_session();
$startedAt = (int) ($_SESSION['auth_reset_started_at'] ?? 0);
return $startedAt > 0 && ($startedAt + AUTH_RESET_TTL_SECONDS) >= time();
}
function authorization_reset_is_authorized(): bool
{
authorization_start_session();
$userId = (int) ($_SESSION['auth_reset_user_id'] ?? 0);
$authorizedUntil = (int) ($_SESSION['auth_reset_authorized_until'] ?? 0);
return $userId > 0 && $authorizedUntil >= time();
}
function authorization_masked_reset_message(): string
{
return 'Wenn ein passendes Konto vorhanden ist, wurde ein 6-stelliger Bestätigungscode an die hinterlegten Kontaktwege gesendet.';
}
function authorization_send_reset_sms(string $phone, string $otp): bool
{
if ($phone === '') {
return false;
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/runtime_env.php';
$payload = json_encode([
'phone' => '+49' . $phone,
'message' => "Intranet NAK Reinickendorf\n\nIhr Bestaetigungscode: {$otp}\n\nDer Code ist 10 Minuten gueltig.",
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($payload === false) {
return false;
}
$context = stream_context_create([
'http' => [
'header' => "Content-Type: application/json; charset=utf-8\r\n",
'method' => 'POST',
'content' => $payload,
'timeout' => 10,
'ignore_errors' => true,
],
]);
try {
$gatewayUrl = intranet_env('SMS_GATEWAY_URL');
} catch (RuntimeException $e) {
error_log('SMS gateway unavailable: ' . $e->getMessage());
return false;
}
$result = @file_get_contents(rtrim($gatewayUrl, '/') . '/send_sms', false, $context);
if ($result === false) {
return false;
}
$statusLine = $http_response_header[0] ?? '';
return preg_match('/\s2\d\d\s/', $statusLine) === 1;
}
@@ -1,40 +0,0 @@
<?php /* Name: validate-email.php
Beschreibung: Validierung der E-Mailadresse
- Wird innerhalb von validation.js aufgerufen
- Prüft ob E-Mail bereits vollständig registriert oder
ein offener Antrag vorliegt */
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
header("Content-Type: application/json");
if (!isset($_GET["email"]) || empty(trim($_GET["email"]))) {
echo json_encode(["available" => false]);
exit;
}
$email = trim($_GET["email"]);
$is_available = true;
// Prüfung 1: Gibt es einen vollständigen Login für diese E-Mail?
$stmt = $pdo->prepare("SELECT u.user_id FROM user_general_information u INNER JOIN user_login l ON u.user_id = l.user_id WHERE u.mail = ?");
$stmt->execute([$email]);
if ($stmt->fetch()) {
$is_available = false;
}
// Prüfung 2: Liegt bereits ein offener Antrag vor?
if ($is_available) {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'");
$stmt->execute([$email]);
$pendingCount = $stmt->fetchColumn();
if ($pendingCount > 0) {
$is_available = false;
}
}
echo json_encode(["available" => $is_available]);
exit;
?>
@@ -1,74 +0,0 @@
const validation = new JustValidate("#signup");
validation
.addField("#first_name", [
{
rule: "required",
errorMessage: "Pflichtfeld"
}
])
.addField("#last_name", [
{
rule: "required",
errorMessage: "Pflichtfeld"
}
])
.addField("#email", [
{
rule: "required",
errorMessage: "Pflichtfeld"
},
{
rule: "email",
errorMessage: "Ungültig"
},
{
validator: (value) => () => {
return fetch("validate-email.php?email=" + encodeURIComponent(value))
.then(function(response) {
return response.json();
})
.then(function(json) {
return json.available;
});
},
errorMessage: "Bereits registriert"
}
])
.addField("#phone", [
{
rule: "required",
errorMessage: "Pflichtfeld"
},
{
rule: "minLength",
value: 6,
errorMessage: "Ungültige Telefonnummer"
},
{
validator: (value) => {
return /^[1-9][0-9\s\-\/()]{4,}$/.test(value);
},
errorMessage: "Ohne führende 0, nur Ziffern"
}
])
.addField("#password", [
{
rule: "required",
errorMessage: "Pflichtfeld"
},
{
rule: "password",
errorMessage: "Zu leicht"
}
])
.addField("#password_confirmation", [
{
validator: (value, fields) => {
return value === fields["#password"].elem.value;
},
errorMessage: "Passwörter müssen übereinstimmen"
}
])
.onSuccess((event) => {
document.getElementById("signup").submit();
});