173 lines
5.6 KiB
PHP
173 lines
5.6 KiB
PHP
|
<?php
|
||
|
|
||
|
/**
|
||
|
* Handles password reset functionality including token generation and validation
|
||
|
*/
|
||
|
class PasswordReset {
|
||
|
private $db;
|
||
|
private const TOKEN_LENGTH = 32;
|
||
|
private const TOKEN_EXPIRY = 3600; // 1 hour
|
||
|
|
||
|
public function __construct($database) {
|
||
|
if ($database instanceof PDO) {
|
||
|
$this->db = $database;
|
||
|
} else {
|
||
|
$this->db = $database->getConnection();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a password reset request and sends email to user
|
||
|
*
|
||
|
* @param string $email User's email address
|
||
|
* @return array Status of the reset request
|
||
|
*/
|
||
|
public function requestReset($email) {
|
||
|
// Check if email exists
|
||
|
$query = $this->db->prepare("
|
||
|
SELECT u.id, um.email
|
||
|
FROM users u
|
||
|
JOIN users_meta um ON u.id = um.user_id
|
||
|
WHERE um.email = :email"
|
||
|
);
|
||
|
$query->bindParam(':email', $email);
|
||
|
$query->execute();
|
||
|
|
||
|
$user = $query->fetch(PDO::FETCH_ASSOC);
|
||
|
if (!$user) {
|
||
|
return ['success' => false, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
|
||
|
}
|
||
|
|
||
|
// Generate unique token
|
||
|
$token = bin2hex(random_bytes(self::TOKEN_LENGTH / 2));
|
||
|
$expires = time() + self::TOKEN_EXPIRY;
|
||
|
|
||
|
// Store token in database
|
||
|
$query = $this->db->prepare("
|
||
|
INSERT INTO user_password_reset (user_id, token, expires)
|
||
|
VALUES (:user_id, :token, :expires)"
|
||
|
);
|
||
|
$query->bindParam(':user_id', $user['id']);
|
||
|
$query->bindParam(':token', $token);
|
||
|
$query->bindParam(':expires', $expires);
|
||
|
|
||
|
if (!$query->execute()) {
|
||
|
return ['success' => false, 'message' => 'Failed to process reset request'];
|
||
|
}
|
||
|
|
||
|
// We need the config for the email details
|
||
|
global $config;
|
||
|
|
||
|
// Prepare the reset link
|
||
|
$scheme = $_SERVER['REQUEST_SCHEME'];
|
||
|
$domain = trim($config['domain'], '/');
|
||
|
$folder = trim($config['folder'], '/');
|
||
|
$folderPath = $folder !== '' ? "/$folder" : '';
|
||
|
$resetLink = "{$scheme}://{$domain}{$folderPath}/index.php?page=login&action=reset&token=" . urlencode($token);
|
||
|
|
||
|
// Send email with reset link
|
||
|
$to = $user['email'];
|
||
|
$subject = "{$config['site_name']} - Password reset request";
|
||
|
$message = "Dear user,\n\n";
|
||
|
$message .= "We received a request to reset your password for your {$config['site_name']} account.\n\n";
|
||
|
$message .= "To set a new password, please click the link below:\n\n";
|
||
|
$message .= $resetLink . "\n\n";
|
||
|
$message .= "This link will expire in 1 hour for security reasons.\n\n";
|
||
|
$message .= "If you did not request this password reset, please ignore this email. Your account remains secure.\n\n";
|
||
|
if (!empty($config['site_name'])) {
|
||
|
$message .= "Best regards,\n";
|
||
|
$message .= "The {$config['site_name']} team\n";
|
||
|
if (!empty($config['site_slogan'])) {
|
||
|
$message .= ":: {$config['site_slogan']} ::";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$headers = [
|
||
|
'From' => "noreply@{$config['domain']}",
|
||
|
'Reply-To' => "noreply@{$config['domain']}",
|
||
|
'X-Mailer' => 'PHP/' . phpversion()
|
||
|
];
|
||
|
|
||
|
if (!mail($to, $subject, $message, $headers)) {
|
||
|
return ['success' => false, 'message' => 'Failed to send reset email'];
|
||
|
}
|
||
|
|
||
|
return ['success' => true, 'message' => 'If this email exists in our system, you will receive reset instructions.'];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates a reset token and returns associated user ID if valid
|
||
|
*
|
||
|
* @param string $token Reset token
|
||
|
* @return array Validation result with user ID if successful
|
||
|
*/
|
||
|
public function validateToken($token) {
|
||
|
$now = time();
|
||
|
|
||
|
$query = $this->db->prepare("
|
||
|
SELECT user_id
|
||
|
FROM user_password_reset
|
||
|
WHERE token = :token
|
||
|
AND expires > :now
|
||
|
AND used = 0
|
||
|
");
|
||
|
|
||
|
$query->bindParam(':token', $token);
|
||
|
$query->bindParam(':now', $now);
|
||
|
$query->execute();
|
||
|
|
||
|
$result = $query->fetch(PDO::FETCH_ASSOC);
|
||
|
|
||
|
if (!$result) {
|
||
|
return ['valid' => false];
|
||
|
}
|
||
|
|
||
|
return ['valid' => true, 'user_id' => $result['user_id']];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Completes the password reset process
|
||
|
*
|
||
|
* @param string $token Reset token
|
||
|
* @param string $newPassword New password
|
||
|
* @return bool Whether reset was successful
|
||
|
*/
|
||
|
public function resetPassword($token, $newPassword) {
|
||
|
$validation = $this->validateToken($token);
|
||
|
if (!$validation['valid']) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Start transaction
|
||
|
$this->db->beginTransaction();
|
||
|
|
||
|
try {
|
||
|
// Update password
|
||
|
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
||
|
$query = $this->db->prepare(
|
||
|
"UPDATE user
|
||
|
SET password = :password
|
||
|
WHERE id = :user_id"
|
||
|
);
|
||
|
$query->bindParam(':password', $hashedPassword);
|
||
|
$query->bindParam(':user_id', $validation['user_id']);
|
||
|
$query->execute();
|
||
|
|
||
|
// Mark token as used
|
||
|
$query = $this->db->prepare(
|
||
|
"UPDATE user_password_reset
|
||
|
SET used = 1
|
||
|
WHERE token = :token"
|
||
|
);
|
||
|
$query->bindParam(':token', $token);
|
||
|
$query->execute();
|
||
|
|
||
|
$this->db->commit();
|
||
|
return true;
|
||
|
} catch (Exception $e) {
|
||
|
$this->db->rollBack();
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|