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 user u
 | |
|             JOIN user_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;
 | |
|         }
 | |
|     }
 | |
| }
 |