dj_mix_hosting_software/forgot-password.php
Cody Cook 635b3ddcbc
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 5m27s
Address changes.
2025-02-22 17:20:19 -08:00

290 lines
13 KiB
PHP
Raw Permalink 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
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
$db = new Database($config);
// Ensure a CSRF token exists.
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Determine if we are in reset stage based on GET parameter.
$isResetStage = false;
$verification_code = "";
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['code'])) {
$verification_code = trim($_GET['code']);
$isResetStage = true;
}
// Process POST submissions.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Validate the CSRF token.
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
$_SESSION['error'] = "Invalid CSRF token.";
header("Location: forgot-password.php");
exit;
}
// If a verification code is provided, we are in reset mode.
if (isset($_POST['verification_code']) && !empty($_POST['verification_code'])) {
// Rate limiting for reset attempts.
if (!isset($_SESSION['attempts'])) {
$_SESSION['attempts'] = 0;
$_SESSION['first_attempt_time'] = time();
}
$_SESSION['attempts']++;
if ($_SESSION['attempts'] > 5 && (time() - $_SESSION['first_attempt_time']) < 900) { // 15 minutes
$_SESSION['error'] = "Too many attempts. Please try again later.";
header("Location: forgot-password.php?code=" . urlencode($_POST['verification_code']));
exit;
}
if (time() - $_SESSION['first_attempt_time'] >= 900) {
$_SESSION['attempts'] = 1;
$_SESSION['first_attempt_time'] = time();
}
// Process the reset.
$verification_code = trim($_POST['verification_code']);
$username = trim($_POST['username'] ?? '');
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (empty($verification_code) || empty($username) || empty($new_password) || empty($confirm_password)) {
$_SESSION['error'] = $locale['allFieldsRequired'];
header("Location: forgot-password.php?code=" . urlencode($verification_code));
exit;
}
if ($new_password !== $confirm_password) {
$_SESSION['error'] = $locale['passwordMismatch'];
header("Location: forgot-password.php?code=" . urlencode($verification_code));
exit;
}
if (!validate_password($new_password)) {
$_SESSION['error'] = $locale['passwordRequirements'];
header("Location: forgot-password.php?code=" . urlencode($verification_code));
exit;
}
// Look up the password reset record.
$stmt = $db->prepare("SELECT * FROM email_verifications WHERE verification_code = ? AND purpose = 'password_reset'");
$stmt->bind_param("s", $verification_code);
$stmt->execute();
$result = $stmt->get_result();
$record = $result->fetch_assoc();
$stmt->close();
if (!$record) {
$_SESSION['error'] = $locale['resetExpiredInvalid'];
header("Location: forgot-password.php?code=" . urlencode($verification_code));
exit;
}
// Check expiration.
$current_time = new DateTime();
$expires_at = new DateTime($record['expires_at']);
if ($current_time > $expires_at) {
$_SESSION['error'] = $locale['resetExpired'];
header("Location: forgot-password.php?code=" . urlencode($verification_code));
exit;
}
// Verify the username matches the record.
$stmt = $db->prepare("SELECT id, username FROM users WHERE id = ? AND username = ?");
$stmt->bind_param("is", $record['user_id'], $username);
$stmt->execute();
$userData = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$userData) {
$_SESSION['error'] = $locale['codeCredsInvalid'];
header("Location: forgot-password.php?code=" . urlencode($verification_code));
exit;
}
// Update the user's password.
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = $db->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->bind_param("si", $hashed_password, $userData['id']);
$stmt->execute();
$stmt->close();
// Remove the password reset record.
$stmt = $db->prepare("DELETE FROM email_verifications WHERE verification_code = ? AND purpose = 'password_reset'");
$stmt->bind_param("s", $verification_code);
$stmt->execute();
$stmt->close();
session_regenerate_id(true);
$_SESSION['success'] = $locale['passwordResetSuccess'];
header("Location: /login");
exit;
} else {
// Otherwise, we are processing a forgot password request.
$email = trim($_POST['email'] ?? '');
if (empty($email)) {
$_SESSION['error'] = $locale['enterEmailAddressPlease'];
header("Location: /forgot-password");
exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error'] = $locale['emailInvalid'];
header("Location: /forgot-password");
exit;
}
// Check if email exists in the system.
$stmt = $db->prepare("SELECT id, username FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
$userData = $result->fetch_assoc();
$stmt->close();
// Always display a success message (even if the email isnt registered) to avoid disclosing registered emails.
$_SESSION['success'] = $locale['passwordResetSent'];
if ($userData) {
$user_id = $userData['id'];
// Generate a password reset verification code valid for 15 minutes.
$verification_code = bin2hex(random_bytes(16));
$expires_at = date("Y-m-d H:i:s", strtotime("+15 minutes"));
// Insert a record for the password reset.
$stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at, purpose) VALUES (?, ?, ?, ?, 'password_reset')");
$stmt->bind_param("isss", $user_id, $email, $verification_code, $expires_at);
$stmt->execute();
$stmt->close();
// Send the password reset email via AWS SES.
$sesClient = new SesClient([
'version' => 'latest',
'region' => $config['aws']['ses']['region'],
'credentials' => [
'key' => $config['aws']['ses']['access_key'],
'secret' => $config['aws']['ses']['secret_key'],
]
]);
$sender_email = $config['aws']['ses']['sender_email'];
$recipient_email = $email;
$subject = "Password Reset Request";
$reset_link = $config['app']['url'] . "/forgot-password.php?code={$verification_code}";
$body_text = $locale['passwordResetRequested'] . "\n\n";
$body_text .= "{$reset_link}\n\n";
$body_text .= $locale['passwordResetUnrequested'];
try {
$sesClient->sendEmail([
'Destination' => [
'ToAddresses' => [$recipient_email],
],
'ReplyToAddresses' => [$sender_email],
'Source' => $sender_email,
'Message' => [
'Body' => [
'Text' => [
'Charset' => 'UTF-8',
'Data' => $body_text,
],
],
'Subject' => [
'Charset' => 'UTF-8',
'Data' => $subject,
],
],
]);
} catch (AwsException $e) {
// Log the error as needed.
}
}
header("Location: /forgot-password");
exit;
}
}
// Helper function to validate password strength.
function validate_password($password) {
if (strlen($password) < 8) return false;
if (!preg_match('/[A-Z]/', $password)) return false;
if (!preg_match('/[a-z]/', $password)) return false;
if (!preg_match('/[0-9]/', $password)) return false;
return true;
}
require_once 'includes/header.php';
?>
<section class="forgot-password-section py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<?php
if (isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">'
. htmlspecialchars($_SESSION['error']) .
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error']);
}
if (isset($_SESSION['success'])) {
echo '<div class="alert alert-success alert-dismissible fade show mb-4" role="alert">'
. htmlspecialchars($_SESSION['success']) .
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['success']);
}
?>
<?php if ($isResetStage): ?>
<!-- Reset Password Form -->
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="text-center mb-4"><?php echo $locale['passwordReset']; ?></h3>
<form action="/forgot-password.php" method="post" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<input type="hidden" name="verification_code" value="<?php echo htmlspecialchars($verification_code); ?>">
<div class="mb-3">
<label for="username" class="form-label"><?php echo $locale['enterYourUsername']; ?></label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="new_password" class="form-label"><?php echo $locale['newPassword']; ?></label>
<input type="password" class="form-control" id="new_password" name="new_password" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label"><?php echo $locale['confirmPassword']; ?></label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary w-100"><?php echo $locale['passwordReset']; ?></button>
</form>
</div>
</div>
<?php else: ?>
<!-- Forgot Password Request Form -->
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="text-center mb-4"><?php echo $locale['forgotPassword']; ?></h3>
<form action="/forgot-password.php" method="post" class="needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<div class="mb-3">
<label for="email" class="form-label"><?php echo $locale['emailaddressEnter']; ?></label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary w-100"><?php echo $locale['submit']; ?></button>
</form>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>