From 27a4fed88f9eb0ef69ad11edd77d1583e3e03192 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sun, 31 Mar 2024 10:27:23 +0400 Subject: [PATCH 01/21] new table added and add "forget password" in page login --- resources/database/dump/cafe.sql | 68 ++++++++++++++++++++++++++++++-- src/views/Login.php | 25 ++++++------ 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/resources/database/dump/cafe.sql b/resources/database/dump/cafe.sql index e55d67f..7719a61 100644 --- a/resources/database/dump/cafe.sql +++ b/resources/database/dump/cafe.sql @@ -1,8 +1,8 @@ --- MySQL dump 10.19 Distrib 10.3.38-MariaDB, for debian-linux-gnu (x86_64) +-- MariaDB dump 10.19 Distrib 10.4.32-MariaDB, for Win64 (AMD64) -- -- Host: localhost Database: cafe -- ------------------------------------------------------ --- Server version 10.3.38-MariaDB-0ubuntu0.20.04.1 +-- Server version 10.4.32-MariaDB /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; @@ -178,14 +178,26 @@ DELIMITER ;; + + + + BEGIN + + + + DECLARE quantity_ordered INT; + + + + DECLARE product_id INT; @@ -194,10 +206,22 @@ BEGIN + + + + + + + + SET quantity_ordered = NEW.quantity; + + + + SET product_id = NEW.product_id; @@ -206,10 +230,22 @@ BEGIN + + + + + + + + UPDATE `product` SET stock_level = stock_level - quantity_ordered WHERE product_id = product_id; + + + + END */;; DELIMITER ; /*!50003 SET sql_mode = @saved_sql_mode */ ; @@ -217,6 +253,32 @@ DELIMITER ; /*!50003 SET character_set_results = @saved_cs_results */ ; /*!50003 SET collation_connection = @saved_col_connection */ ; +-- +-- Table structure for table `password_change_requests` +-- + +DROP TABLE IF EXISTS `password_change_requests`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `password_change_requests` ( + `pw_change_requests_id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `date` date NOT NULL, + `user_id` int(11) unsigned NOT NULL, + PRIMARY KEY (`pw_change_requests_id`), + KEY `pw_change` (`user_id`), + CONSTRAINT `pw_change` FOREIGN KEY (`user_id`) REFERENCES `client` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `password_change_requests` +-- + +LOCK TABLES `password_change_requests` WRITE; +/*!40000 ALTER TABLE `password_change_requests` DISABLE KEYS */; +/*!40000 ALTER TABLE `password_change_requests` ENABLE KEYS */; +UNLOCK TABLES; + -- -- Table structure for table `product` -- @@ -330,4 +392,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2024-03-27 10:47:31 +-- Dump completed on 2024-03-30 20:16:18 diff --git a/src/views/Login.php b/src/views/Login.php index 84f9aa3..15ea10b 100644 --- a/src/views/Login.php +++ b/src/views/Login.php @@ -20,19 +20,18 @@ -
- -
+
+ + Forgot Password? +
+ - - - + + + Don't have an account yet? Register @@ -44,4 +43,4 @@ document.addEventListener("DOMContentLoaded", function() { AOS.init(); }); - \ No newline at end of file + From f1cbbf91dd4f6989aa3ed69dc2e63fcbaec02fcb Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Thu, 18 Apr 2024 21:22:26 +0400 Subject: [PATCH 02/21] Implement reset password functionality : - I add a new table in tthe database named "password_change_requests" - Modify view login to add "Forgot Password" and add 2 files in views - I add one file in both controllers and Models and modify User Model to add some methods --- src/controllers/Resetpasswords.php | 158 +++++++++++++++++++++++++++++ src/models/ResetPassword.php | 42 ++++++++ src/models/User.php | 42 ++++++++ src/views/Create-new-password.php | 35 +++++++ src/views/Login.php | 3 +- src/views/Reset-password.php | 13 +++ 6 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 src/controllers/Resetpasswords.php create mode 100644 src/models/ResetPassword.php create mode 100644 src/views/Create-new-password.php create mode 100644 src/views/Reset-password.php diff --git a/src/controllers/Resetpasswords.php b/src/controllers/Resetpasswords.php new file mode 100644 index 0000000..6c11a7f --- /dev/null +++ b/src/controllers/Resetpasswords.php @@ -0,0 +1,158 @@ +mailer = $mailer; + $this->resetModel = $resetModel; + $this->userModel = $userModel; + } + + public function sendEmail() { + //Sanitize POST data + $_POST = array_map('htmlspecialchars', $_POST); + $usersEmail = trim($_POST['usersEmail']); + + if (empty($usersEmail)) { + $_SESSION['flash'] = "Please input email"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect("../reset-password.php"); + } + + if (!filter_var($usersEmail, FILTER_VALIDATE_EMAIL)) { + $_SESSION['flash'] = "Invalid email"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect("../reset-password.php"); + } + + // Will be used to query the user from the database + $selector = bin2hex(random_bytes(8)); + // Will be used for confirmation once the database entry has been matched + $token = random_bytes(32); + // URL will vary depending on where the website is being hosted from + $url = 'http://localhost/login/create-new-password.php?selector='.$selector.'&validator='.bin2hex($token); + // Expiration date will last for half an hour + $expires = date("U") + 1800; + + if (!$this->resetModel->deleteEmail($usersEmail)) { + die("There was an error"); + } + + $hashedToken = password_hash($token, PASSWORD_DEFAULT); + if (!$this->resetModel->insertToken($usersEmail, $selector, $hashedToken, $expires)) { + die("There was an error"); + } + + // Can Send Email Now + $subject = "Reset your password"; + $message = "

We received a password reset request.

"; + $message .= "

Here is your password reset link:

"; + $message .= "".$url.""; + + $this->mailer->sendMail($usersEmail, $subject, $message, $message); + + // Set flash message + $_SESSION['flash'] = "Check your email"; + $_SESSION['flash_class'] = 'form-message form-message-green'; + Utility::redirect("../reset-password.php"); + } + + public function resetPassword() { + // Sanitize POST data + $_POST = array_map('htmlspecialchars', $_POST); + $data = [ + 'selector' => trim($_POST['selector']), + 'validator' => trim($_POST['validator']), + 'pwd' => trim($_POST['pwd']), + 'pwd-repeat' => trim($_POST['pwd-repeat']) + ]; + $url = '../create-new-password.php?selector=' . $data['selector'] . '&validator=' . $data['validator']; + + if (empty($_POST['pwd']) || empty($_POST['pwd-repeat'])) { + $_SESSION['flash'] = "Please fill out all fields"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } elseif ($data['pwd'] != $data['pwd-repeat']) { + $_SESSION['flash'] = "Passwords do not match"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } elseif (strlen($data['pwd']) < 6) { + $_SESSION['flash'] = "Invalid password"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } + + $currentDate = date("U"); + if (!$row = $this->resetModel->resetPassword($data['selector'], $currentDate)) { + $_SESSION['flash'] = "Sorry. The link is no longer valid"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } + + $tokenBin = hex2bin($data['validator']); + $tokenCheck = password_verify($tokenBin, $row->pwdResetToken); + if (!$tokenCheck) { + $_SESSION['flash'] = "You need to re-Submit your reset request"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } + + $tokenEmail = $row->pwdResetEmail; + if (!$this->userModel->findUserByEmailOrUsername($tokenEmail, $tokenEmail)) { + $_SESSION['flash'] = "There was an error"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } + + $newPwdHash = password_hash($data['pwd'], PASSWORD_DEFAULT); + if (!$this->userModel->resetPassword($newPwdHash, $tokenEmail)) { + $_SESSION['flash'] = "There was an error"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } + + if (!$this->resetModel->deleteEmail($tokenEmail)) { + $_SESSION['flash'] = "There was an error"; + $_SESSION['flash_class'] = 'form-message form-message-red'; + Utility::redirect($url); + } + + $_SESSION['flash'] = "Password Updated"; + $_SESSION['flash_class'] = 'form-message form-message-green'; + Utility::redirect($url); + } + +} + +$mailer = new Mailer(); + +$init = new ResetPasswords($mailer, $resetModel, $userModel); + +// Ensure that the session is started +session_start(); + +// Ensure that user is sending a post request +if ($_SERVER['REQUEST_METHOD'] == 'POST') { + switch ($_POST['type']) { + case 'send': + $init->sendEmail(); + break; + case 'reset': + $init->resetPassword(); + break; + default: + Utility::redirect("../login.php"); + } +} else { + Utility::redirect("../login.php"); +} diff --git a/src/models/ResetPassword.php b/src/models/ResetPassword.php new file mode 100644 index 0000000..3ded258 --- /dev/null +++ b/src/models/ResetPassword.php @@ -0,0 +1,42 @@ +model = new Model(); + } + + public function deleteEmail($email) { + // Assuming user_id is associated with the email + $user = $this->model->first(['email' => $email], 'user'); + if ($user) { + $this->model->delete($user->user_id, 'password_change_requests', 'user_id'); + } + } + + public function insertToken($email, $date) { + // Assuming user_id is associated with the email + $user = $this->model->first(['email' => $email], 'user'); + if ($user) { + $data = [ + 'date' => $date, + 'user_id' => $user->user_id + ]; + $this->model->insert($data, 'password_change_requests'); + } + } + + public function resetPassword($userId) { + $data = [ + 'user_id' => $userId + ]; + return $this->model->first($data, 'password_change_requests'); + } +} +?> diff --git a/src/models/User.php b/src/models/User.php index e55c1d6..67d9bff 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -195,4 +195,46 @@ public function setUserID(int $new_id): void { $this->user_id = $new_id; } + + public function findUserByEmailOrUsername($email, $username) + { + $data = [ + 'usersUid' => $username, + 'usersEmail' => $email + ]; + return $this->first($data); // Use the 'first' method from the Model trait + } + + // Register User + public function register($data) + { + // Use the 'insert' method from the Model trait + return $this->insert($data); + } + + // Login user + public function login($nameOrEmail, $password) + { + $row = $this->findUserByEmailOrUsername($nameOrEmail, $nameOrEmail); + + if ($row === false) return false; + + $hashedPassword = $row->usersPwd; + if (password_verify($password, $hashedPassword)) { + return $row; + } else { + return false; + } + } + + // Reset Password + public function resetPassword($newPwdHash, $tokenEmail) + { + $data = [ + 'usersPwd' => $newPwdHash, + 'usersEmail' => $tokenEmail + ]; + // Use the 'update' method from the Model trait + return $this->update($tokenEmail, $data, $this->table, 'usersEmail'); + } } diff --git a/src/views/Create-new-password.php b/src/views/Create-new-password.php new file mode 100644 index 0000000..5aa8e16 --- /dev/null +++ b/src/views/Create-new-password.php @@ -0,0 +1,35 @@ + + + + + +

Enter New Password

+ +
+ + + + + + + +
+ + + \ No newline at end of file diff --git a/src/views/Login.php b/src/views/Login.php index 15ea10b..3fbf5e5 100644 --- a/src/views/Login.php +++ b/src/views/Login.php @@ -23,9 +23,8 @@
- Forgot Password?
diff --git a/src/views/Reset-password.php b/src/views/Reset-password.php new file mode 100644 index 0000000..90a9f36 --- /dev/null +++ b/src/views/Reset-password.php @@ -0,0 +1,13 @@ + + +

Reset Password

+ +
+ + + +
From 95139ebc79c6d4343eb0d761d6c982e3d347dee0 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sat, 20 Apr 2024 19:48:41 +0400 Subject: [PATCH 03/21] only one method in password controller and add 3 method to user model --- resources/database/dump/cafe.sql | 28 ++-- src/controllers/Password.php | 86 ++++++++++ src/controllers/Resetpasswords.php | 158 ------------------ src/models/ResetPassword.php | 42 ----- src/models/User.php | 64 +++---- src/views/Create-new-password.php | 35 ---- src/views/Login.php | 2 +- src/views/Newpassword.php | 16 ++ .../{Reset-password.php => ResetPassword.php} | 2 +- 9 files changed, 152 insertions(+), 281 deletions(-) create mode 100644 src/controllers/Password.php delete mode 100644 src/controllers/Resetpasswords.php delete mode 100644 src/models/ResetPassword.php delete mode 100644 src/views/Create-new-password.php create mode 100644 src/views/Newpassword.php rename src/views/{Reset-password.php => ResetPassword.php} (80%) diff --git a/resources/database/dump/cafe.sql b/resources/database/dump/cafe.sql index 7719a61..3b51857 100644 --- a/resources/database/dump/cafe.sql +++ b/resources/database/dump/cafe.sql @@ -254,29 +254,31 @@ DELIMITER ; /*!50003 SET collation_connection = @saved_col_connection */ ; -- --- Table structure for table `password_change_requests` +-- Table structure for table `password_change_request` -- -DROP TABLE IF EXISTS `password_change_requests`; +DROP TABLE IF EXISTS `password_change_request`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; -CREATE TABLE `password_change_requests` ( - `pw_change_requests_id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `date` date NOT NULL, +CREATE TABLE `password_change_request` ( + `request_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(11) unsigned NOT NULL, - PRIMARY KEY (`pw_change_requests_id`), - KEY `pw_change` (`user_id`), - CONSTRAINT `pw_change` FOREIGN KEY (`user_id`) REFERENCES `client` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE + `token_hash` varchar(255) NOT NULL, + `expiry_date` datetime NOT NULL, + `used` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`request_id`), + KEY `request_fk` (`user_id`), + CONSTRAINT `request_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Dumping data for table `password_change_requests` +-- Dumping data for table `password_change_request` -- -LOCK TABLES `password_change_requests` WRITE; -/*!40000 ALTER TABLE `password_change_requests` DISABLE KEYS */; -/*!40000 ALTER TABLE `password_change_requests` ENABLE KEYS */; +LOCK TABLES `password_change_request` WRITE; +/*!40000 ALTER TABLE `password_change_request` DISABLE KEYS */; +/*!40000 ALTER TABLE `password_change_request` ENABLE KEYS */; UNLOCK TABLES; -- @@ -392,4 +394,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2024-03-30 20:16:18 +-- Dump completed on 2024-04-20 18:34:37 diff --git a/src/controllers/Password.php b/src/controllers/Password.php new file mode 100644 index 0000000..17f6faa --- /dev/null +++ b/src/controllers/Password.php @@ -0,0 +1,86 @@ +view( + 'ResetPassword', + ['email' => $email], // Pass email data to the view + 'ResetPassword' + ); + + if (isset($_POST['pwd'], $_POST['pwd-repeat'], $_GET['id'])) { + $password = $_POST['pwd']; + $passwordRepeat = $_POST['pwd-repeat']; + $token = $_GET['id']; + + // Check if passwords match + if ($password === $passwordRepeat) { + // Hash the new password + $hashedPassword = password_hash($password, PASSWORD_DEFAULT); + + // Get user ID based on token + $userId = User::getUserIdByToken($token); + + if ($userId !== false) { + // Update user's password + User::updatePassword($userId, $hashedPassword); + + // Redirect to login page + Utility::redirect('login'); + } + } else { + // If passwords don't match or token is invalid, redirect to reset password page + Utility::redirect('resetpassword'); + } + } + + $this->view( + 'Newpassword', + ['pwd' => '', 'pwd-repeat' => ''], + 'Newpassword' + ); + + } +} diff --git a/src/controllers/Resetpasswords.php b/src/controllers/Resetpasswords.php deleted file mode 100644 index 6c11a7f..0000000 --- a/src/controllers/Resetpasswords.php +++ /dev/null @@ -1,158 +0,0 @@ -mailer = $mailer; - $this->resetModel = $resetModel; - $this->userModel = $userModel; - } - - public function sendEmail() { - //Sanitize POST data - $_POST = array_map('htmlspecialchars', $_POST); - $usersEmail = trim($_POST['usersEmail']); - - if (empty($usersEmail)) { - $_SESSION['flash'] = "Please input email"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect("../reset-password.php"); - } - - if (!filter_var($usersEmail, FILTER_VALIDATE_EMAIL)) { - $_SESSION['flash'] = "Invalid email"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect("../reset-password.php"); - } - - // Will be used to query the user from the database - $selector = bin2hex(random_bytes(8)); - // Will be used for confirmation once the database entry has been matched - $token = random_bytes(32); - // URL will vary depending on where the website is being hosted from - $url = 'http://localhost/login/create-new-password.php?selector='.$selector.'&validator='.bin2hex($token); - // Expiration date will last for half an hour - $expires = date("U") + 1800; - - if (!$this->resetModel->deleteEmail($usersEmail)) { - die("There was an error"); - } - - $hashedToken = password_hash($token, PASSWORD_DEFAULT); - if (!$this->resetModel->insertToken($usersEmail, $selector, $hashedToken, $expires)) { - die("There was an error"); - } - - // Can Send Email Now - $subject = "Reset your password"; - $message = "

We received a password reset request.

"; - $message .= "

Here is your password reset link:

"; - $message .= "".$url.""; - - $this->mailer->sendMail($usersEmail, $subject, $message, $message); - - // Set flash message - $_SESSION['flash'] = "Check your email"; - $_SESSION['flash_class'] = 'form-message form-message-green'; - Utility::redirect("../reset-password.php"); - } - - public function resetPassword() { - // Sanitize POST data - $_POST = array_map('htmlspecialchars', $_POST); - $data = [ - 'selector' => trim($_POST['selector']), - 'validator' => trim($_POST['validator']), - 'pwd' => trim($_POST['pwd']), - 'pwd-repeat' => trim($_POST['pwd-repeat']) - ]; - $url = '../create-new-password.php?selector=' . $data['selector'] . '&validator=' . $data['validator']; - - if (empty($_POST['pwd']) || empty($_POST['pwd-repeat'])) { - $_SESSION['flash'] = "Please fill out all fields"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } elseif ($data['pwd'] != $data['pwd-repeat']) { - $_SESSION['flash'] = "Passwords do not match"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } elseif (strlen($data['pwd']) < 6) { - $_SESSION['flash'] = "Invalid password"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } - - $currentDate = date("U"); - if (!$row = $this->resetModel->resetPassword($data['selector'], $currentDate)) { - $_SESSION['flash'] = "Sorry. The link is no longer valid"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } - - $tokenBin = hex2bin($data['validator']); - $tokenCheck = password_verify($tokenBin, $row->pwdResetToken); - if (!$tokenCheck) { - $_SESSION['flash'] = "You need to re-Submit your reset request"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } - - $tokenEmail = $row->pwdResetEmail; - if (!$this->userModel->findUserByEmailOrUsername($tokenEmail, $tokenEmail)) { - $_SESSION['flash'] = "There was an error"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } - - $newPwdHash = password_hash($data['pwd'], PASSWORD_DEFAULT); - if (!$this->userModel->resetPassword($newPwdHash, $tokenEmail)) { - $_SESSION['flash'] = "There was an error"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } - - if (!$this->resetModel->deleteEmail($tokenEmail)) { - $_SESSION['flash'] = "There was an error"; - $_SESSION['flash_class'] = 'form-message form-message-red'; - Utility::redirect($url); - } - - $_SESSION['flash'] = "Password Updated"; - $_SESSION['flash_class'] = 'form-message form-message-green'; - Utility::redirect($url); - } - -} - -$mailer = new Mailer(); - -$init = new ResetPasswords($mailer, $resetModel, $userModel); - -// Ensure that the session is started -session_start(); - -// Ensure that user is sending a post request -if ($_SERVER['REQUEST_METHOD'] == 'POST') { - switch ($_POST['type']) { - case 'send': - $init->sendEmail(); - break; - case 'reset': - $init->resetPassword(); - break; - default: - Utility::redirect("../login.php"); - } -} else { - Utility::redirect("../login.php"); -} diff --git a/src/models/ResetPassword.php b/src/models/ResetPassword.php deleted file mode 100644 index 3ded258..0000000 --- a/src/models/ResetPassword.php +++ /dev/null @@ -1,42 +0,0 @@ -model = new Model(); - } - - public function deleteEmail($email) { - // Assuming user_id is associated with the email - $user = $this->model->first(['email' => $email], 'user'); - if ($user) { - $this->model->delete($user->user_id, 'password_change_requests', 'user_id'); - } - } - - public function insertToken($email, $date) { - // Assuming user_id is associated with the email - $user = $this->model->first(['email' => $email], 'user'); - if ($user) { - $data = [ - 'date' => $date, - 'user_id' => $user->user_id - ]; - $this->model->insert($data, 'password_change_requests'); - } - } - - public function resetPassword($userId) { - $data = [ - 'user_id' => $userId - ]; - return $this->model->first($data, 'password_change_requests'); - } -} -?> diff --git a/src/models/User.php b/src/models/User.php index 67d9bff..5b1db6a 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -5,6 +5,8 @@ namespace Steamy\Model; use Steamy\Core\Model; +use Steamy\Core\Database; +use Steamy\Core\Mailer; abstract class User { @@ -196,45 +198,45 @@ public function setUserID(int $new_id): void $this->user_id = $new_id; } - public function findUserByEmailOrUsername($email, $username) + public static function getUserIdByEmail(string $email): ?int { - $data = [ - 'usersUid' => $username, - 'usersEmail' => $email - ]; - return $this->first($data); // Use the 'first' method from the Model trait + //Implement logic to fetch user ID by email from the database + $query = "SELECT user_id FROM user WHERE email = :email"; + $result = Database::query($query, ['email' => $email]); + return $result[0]['user_id'] ?? null; } - // Register User - public function register($data) + public static function savePasswordChangeRequest(int $userId, string $tokenHash, string $expiryDate) { - // Use the 'insert' method from the Model trait - return $this->insert($data); + //Implement logic to save password change request in the database + $query = "INSERT INTO password_change_request (user_id, token_hash, expiry_date) VALUES (:userId, :tokenHash, :expiryDate)"; + Database::query($query, ['userId' => $userId, 'tokenHash' => $tokenHash, 'expiryDate' => $expiryDate]); + } - // Login user - public function login($nameOrEmail, $password) + public static function sendResetEmail(string $email, string $resetLink) { - $row = $this->findUserByEmailOrUsername($nameOrEmail, $nameOrEmail); + //Implement logic to send reset email using Mailer class + $mailer = new Mailer(); + $subject = "Reset Your Password"; + $htmlMessage = "Click the link below to reset your password:
$resetLink"; + $plainMessage = "Click the link below to reset your password:\n$resetLink"; + $mailer->sendMail($email, $subject, $htmlMessage, $plainMessage); + } - if ($row === false) return false; + public static function getUserIdByToken(string $token): ?int +{ + //Implement logic to fetch user ID by token from the database + $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = false"; + $result = Database::query($query, ['token' => $token]); + return $result[0]['user_id'] ?? null; +} - $hashedPassword = $row->usersPwd; - if (password_verify($password, $hashedPassword)) { - return $row; - } else { - return false; - } - } +public static function updatePassword(int $userId, string $hashedPassword): void +{ + // Implement logic to update user's password in the database + $query = "UPDATE user SET password = :password WHERE user_id = :userId"; + Database::query($query, ['password' => $hashedPassword, 'userId' => $userId]); +} - // Reset Password - public function resetPassword($newPwdHash, $tokenEmail) - { - $data = [ - 'usersPwd' => $newPwdHash, - 'usersEmail' => $tokenEmail - ]; - // Use the 'update' method from the Model trait - return $this->update($tokenEmail, $data, $this->table, 'usersEmail'); - } } diff --git a/src/views/Create-new-password.php b/src/views/Create-new-password.php deleted file mode 100644 index 5aa8e16..0000000 --- a/src/views/Create-new-password.php +++ /dev/null @@ -1,35 +0,0 @@ - - - - - -

Enter New Password

- -
- - - - - - - -
- - - \ No newline at end of file diff --git a/src/views/Login.php b/src/views/Login.php index 3fbf5e5..64f6b03 100644 --- a/src/views/Login.php +++ b/src/views/Login.php @@ -23,7 +23,7 @@
diff --git a/src/views/Newpassword.php b/src/views/Newpassword.php new file mode 100644 index 0000000..89c54dd --- /dev/null +++ b/src/views/Newpassword.php @@ -0,0 +1,16 @@ + + +

Enter New Password

+ +
+ + + + + +
+ \ No newline at end of file diff --git a/src/views/Reset-password.php b/src/views/ResetPassword.php similarity index 80% rename from src/views/Reset-password.php rename to src/views/ResetPassword.php index 90a9f36..d63903e 100644 --- a/src/views/Reset-password.php +++ b/src/views/ResetPassword.php @@ -6,7 +6,7 @@

Reset Password

-
+ From 60a90cb210216ce1e169512734b9e1b150d19340 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sat, 20 Apr 2024 21:30:54 +0400 Subject: [PATCH 04/21] fix overflowing forgot password --- src/views/Login.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/views/Login.php b/src/views/Login.php index 5e28579..993b74c 100644 --- a/src/views/Login.php +++ b/src/views/Login.php @@ -22,8 +22,10 @@ required/> - Don't have an account yet? Register - Forgot Password? + + Register + Forgot Password +
From 50545decbf453fbb237c39c8027673fded38c2f7 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sat, 20 Apr 2024 21:51:02 +0400 Subject: [PATCH 05/21] refactor and fix bugs. remaining bugs come from User static methods --- src/controllers/Password.php | 101 +++++++++++++---------------------- src/views/ResetPassword.php | 11 ++-- 2 files changed, 42 insertions(+), 70 deletions(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index 17f6faa..748a507 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -1,86 +1,59 @@ view( - 'ResetPassword', - ['email' => $email], // Pass email data to the view - 'ResetPassword' - ); - - if (isset($_POST['pwd'], $_POST['pwd-repeat'], $_GET['id'])) { - $password = $_POST['pwd']; - $passwordRepeat = $_POST['pwd-repeat']; - $token = $_GET['id']; + // email is valid - // Check if passwords match - if ($password === $passwordRepeat) { - // Hash the new password - $hashedPassword = password_hash($password, PASSWORD_DEFAULT); + // Generate random token + $token = bin2hex(random_bytes(16)); // Generating a random token of length 32 bytes (hexadecimal format) - // Get user ID based on token - $userId = User::getUserIdByToken($token); + // Save information about request in the password_change_request table + $expiryDate = date('Y-m-d H:i:s', strtotime('+1 day')); // Expiry date set to 1 day from now + $tokenHash = password_hash($token, PASSWORD_BCRYPT); // Hashing the token for security + $userId = User::getUserIdByEmail($submitted_email); // Get user ID by email - if ($userId !== false) { - // Update user's password - User::updatePassword($userId, $hashedPassword); - - // Redirect to login page - Utility::redirect('login'); - } - } else { - // If passwords don't match or token is invalid, redirect to reset password page - Utility::redirect('resetpassword'); - } + if ($userId !== false) { + User::savePasswordChangeRequest($userId, $tokenHash, $expiryDate); + // Send email with reset link + $resetLink = ROOT . "/password/reset?id=$token"; + User::sendResetEmail($submitted_email, $resetLink); + } else { + Utility::redirect('login'); } + } - $this->view( - 'Newpassword', - ['pwd' => '', 'pwd-repeat' => ''], - 'Newpassword' - ); - + public function index(): void + { + if (isset($_POST['email']) && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) { + $this->handleEmailSubmission(); + } + // display form asking for user email + { + $this->view( + view_name: 'ResetPassword', + template_title: 'Reset Password' + ); + } } } diff --git a/src/views/ResetPassword.php b/src/views/ResetPassword.php index d63903e..300e4e6 100644 --- a/src/views/ResetPassword.php +++ b/src/views/ResetPassword.php @@ -4,10 +4,9 @@ ?> -

Reset Password

+

Reset Password

-
- - - -
+
+ + +
From 3029ab9fb6566e01c2b74604d6c7757c66eac998 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 21 Apr 2024 07:27:11 +0400 Subject: [PATCH 06/21] fix invalid database calls, remove mailing functions --- src/models/User.php | 51 +++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/models/User.php b/src/models/User.php index 12bf63a..b209961 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -5,8 +5,6 @@ namespace Steamy\Model; use Steamy\Core\Model; -use Steamy\Core\Database; -use Steamy\Core\Mailer; abstract class User { @@ -209,41 +207,36 @@ public static function getUserIdByEmail(string $email): ?int { //Implement logic to fetch user ID by email from the database $query = "SELECT user_id FROM user WHERE email = :email"; - $result = Database::query($query, ['email' => $email]); - return $result[0]['user_id'] ?? null; + $result = self::query($query, ['email' => $email]); + + if (!$result || count($result) == 0) { + return null; + } + return $result[0]->user_id; } - public static function savePasswordChangeRequest(int $userId, string $tokenHash, string $expiryDate) + public static function savePasswordChangeRequest(int $userId, string $tokenHash, string $expiryDate): void { //Implement logic to save password change request in the database - $query = "INSERT INTO password_change_request (user_id, token_hash, expiry_date) VALUES (:userId, :tokenHash, :expiryDate)"; - Database::query($query, ['userId' => $userId, 'tokenHash' => $tokenHash, 'expiryDate' => $expiryDate]); - + $query = "INSERT INTO password_change_request (user_id, token_hash, expiry_date) + VALUES (:userId, :tokenHash, :expiryDate)"; + self::query($query, ['userId' => $userId, 'tokenHash' => $tokenHash, 'expiryDate' => $expiryDate]); } - public static function sendResetEmail(string $email, string $resetLink) - { - //Implement logic to send reset email using Mailer class - $mailer = new Mailer(); - $subject = "Reset Your Password"; - $htmlMessage = "Click the link below to reset your password:
$resetLink"; - $plainMessage = "Click the link below to reset your password:\n$resetLink"; - $mailer->sendMail($email, $subject, $htmlMessage, $plainMessage); - } public static function getUserIdByToken(string $token): ?int -{ - //Implement logic to fetch user ID by token from the database - $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = false"; - $result = Database::query($query, ['token' => $token]); - return $result[0]['user_id'] ?? null; -} + { + //Implement logic to fetch user ID by token from the database + $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = false"; + $result = self::query($query, ['token' => $token]); + return $result[0]['user_id'] ?? null; + } -public static function updatePassword(int $userId, string $hashedPassword): void -{ - // Implement logic to update user's password in the database - $query = "UPDATE user SET password = :password WHERE user_id = :userId"; - Database::query($query, ['password' => $hashedPassword, 'userId' => $userId]); -} + public static function updatePassword(int $userId, string $hashedPassword): void + { + // Implement logic to update user's password in the database + $query = "UPDATE user SET password = :password WHERE user_id = :userId"; + self::query($query, ['password' => $hashedPassword, 'userId' => $userId]); + } } From 1d2e7325676280bc312ffe150348e5a4a9763ca3 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 21 Apr 2024 07:27:42 +0400 Subject: [PATCH 07/21] add sendResetEmail function --- src/controllers/Password.php | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index 748a507..4b7134d 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -4,7 +4,8 @@ namespace Steamy\Controller; -use Steamy\Core\Utility; +use PHPMailer\PHPMailer\Exception; +use Steamy\Core\Mailer; use Steamy\Model\User; use Steamy\Core\Controller; @@ -15,6 +16,19 @@ class Password { use Controller; + /** + * @throws Exception + */ + private function sendResetEmail(string $email, string $resetLink): void + { + //Implement logic to send reset email using Mailer class + $mailer = new Mailer(); + $subject = "Reset Your Password"; + $htmlMessage = "Click the link below to reset your password:
$resetLink"; + $plainMessage = "Click the link below to reset your password:\n$resetLink"; + $mailer->sendMail($email, $subject, $htmlMessage, $plainMessage); + } + private function handleEmailSubmission(): void { $submitted_email = filter_var($_POST['email'] ?? "", FILTER_VALIDATE_EMAIL); @@ -22,7 +36,6 @@ private function handleEmailSubmission(): void if (empty($submitted_email)) { return; } - // email is valid // Generate random token @@ -33,21 +46,24 @@ private function handleEmailSubmission(): void $tokenHash = password_hash($token, PASSWORD_BCRYPT); // Hashing the token for security $userId = User::getUserIdByEmail($submitted_email); // Get user ID by email - if ($userId !== false) { + if ($userId) { User::savePasswordChangeRequest($userId, $tokenHash, $expiryDate); - // Send email with reset link $resetLink = ROOT . "/password/reset?id=$token"; - User::sendResetEmail($submitted_email, $resetLink); + + try { + $this->sendResetEmail($submitted_email, $resetLink); + } catch (Exception $e) { + echo 'Mailer credentials invalid'; + } } else { - Utility::redirect('login'); + echo $submitted_email . " not in database"; } } public function index(): void { - if (isset($_POST['email']) && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) { - $this->handleEmailSubmission(); - } + $this->handleEmailSubmission(); + // display form asking for user email { $this->view( From ec6a25aa8be7c97d12c873901709969135cb1b7b Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 21 Apr 2024 10:22:35 +0400 Subject: [PATCH 08/21] turn off smtp debugging --- src/core/Mailer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Mailer.php b/src/core/Mailer.php index 9349f9d..a7851f1 100644 --- a/src/core/Mailer.php +++ b/src/core/Mailer.php @@ -33,7 +33,7 @@ public function __construct() //SMTP::DEBUG_OFF = off (for production use) //SMTP::DEBUG_CLIENT = client messages //SMTP::DEBUG_SERVER = client and server messages - $this->mail->SMTPDebug = SMTP::DEBUG_SERVER; + $this->mail->SMTPDebug = SMTP::DEBUG_OFF; //Set the hostname of the mail server $this->mail->Host = 'smtp.gmail.com'; From a637fa1ea7a01729b131473539bfa2b0bbad08fa Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sun, 21 Apr 2024 17:15:45 +0400 Subject: [PATCH 09/21] add a method in to reset password in password controller and modify action in Newpassword.php in view --- src/controllers/Password.php | 39 +++++++++++++++++++++++++++++++++++- src/core/Mailer.php | 2 +- src/views/Newpassword.php | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index 4b7134d..69855e2 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -8,6 +8,7 @@ use Steamy\Core\Mailer; use Steamy\Model\User; use Steamy\Core\Controller; +use Steamy\Core\Utility; /** * Displays form asking for email and handles email submission for password reset. @@ -48,10 +49,11 @@ private function handleEmailSubmission(): void if ($userId) { User::savePasswordChangeRequest($userId, $tokenHash, $expiryDate); - $resetLink = ROOT . "/password/reset?id=$token"; + $resetLink = ROOT . "/Newpassword?id=$token"; try { $this->sendResetEmail($submitted_email, $resetLink); + echo 'Please check your email. We have sent you an email with a link to change your password'; } catch (Exception $e) { echo 'Mailer credentials invalid'; } @@ -60,6 +62,41 @@ private function handleEmailSubmission(): void } } + public function resetPassword(): void + { + if (isset($_POST['pwd'], $_POST['pwd-repeat'], $_GET['id'])) { + $password = $_POST['pwd']; + $passwordRepeat = $_POST['pwd-repeat']; + $token = $_GET['id']; + + // Check if passwords match + if ($password === $passwordRepeat) { + // Hash the new password + $hashedPassword = password_hash($password, PASSWORD_BCRYPT); + + // Get user ID based on token + $userId = User::getUserIdByToken($token); + + if ($userId !== null) { + // Update user's password + User::updatePassword($userId, $hashedPassword); + + // Redirect to login page or display success message + Utility::redirect('login'); + } else { + // Handle invalid token (redirect to an error page or display an error message) + echo "Invalid token."; + } + } else { + // Handle password mismatch error + echo "Passwords do not match."; + } + } else { + // Handle missing form data error + echo "Form data is missing."; + } + } + public function index(): void { $this->handleEmailSubmission(); diff --git a/src/core/Mailer.php b/src/core/Mailer.php index a7851f1..9349f9d 100644 --- a/src/core/Mailer.php +++ b/src/core/Mailer.php @@ -33,7 +33,7 @@ public function __construct() //SMTP::DEBUG_OFF = off (for production use) //SMTP::DEBUG_CLIENT = client messages //SMTP::DEBUG_SERVER = client and server messages - $this->mail->SMTPDebug = SMTP::DEBUG_OFF; + $this->mail->SMTPDebug = SMTP::DEBUG_SERVER; //Set the hostname of the mail server $this->mail->Host = 'smtp.gmail.com'; diff --git a/src/views/Newpassword.php b/src/views/Newpassword.php index 89c54dd..e319b3c 100644 --- a/src/views/Newpassword.php +++ b/src/views/Newpassword.php @@ -6,7 +6,7 @@

Enter New Password

-
+ From 4544135948ccd90e43fc36a6ea5c42a66a373186 Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sun, 21 Apr 2024 18:24:52 +0400 Subject: [PATCH 10/21] SMTP::DEBUG_SERVER to SMTP::DEBUG_OFF --- src/core/Mailer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Mailer.php b/src/core/Mailer.php index 9349f9d..a7851f1 100644 --- a/src/core/Mailer.php +++ b/src/core/Mailer.php @@ -33,7 +33,7 @@ public function __construct() //SMTP::DEBUG_OFF = off (for production use) //SMTP::DEBUG_CLIENT = client messages //SMTP::DEBUG_SERVER = client and server messages - $this->mail->SMTPDebug = SMTP::DEBUG_SERVER; + $this->mail->SMTPDebug = SMTP::DEBUG_OFF; //Set the hostname of the mail server $this->mail->Host = 'smtp.gmail.com'; From 382cc69171c53814e0fcc1d2c8183201c5c1059c Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Sun, 21 Apr 2024 18:42:41 +0400 Subject: [PATCH 11/21] fix some bugs with password submission --- src/controllers/Password.php | 81 +++++++++++++++++++++--------------- src/views/Newpassword.php | 17 ++++---- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index 69855e2..202098e 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -49,7 +49,7 @@ private function handleEmailSubmission(): void if ($userId) { User::savePasswordChangeRequest($userId, $tokenHash, $expiryDate); - $resetLink = ROOT . "/Newpassword?id=$token"; + $resetLink = ROOT . "/password?id=$token"; try { $this->sendResetEmail($submitted_email, $resetLink); @@ -62,51 +62,66 @@ private function handleEmailSubmission(): void } } - public function resetPassword(): void + public function handlePasswordSubmission(): void { - if (isset($_POST['pwd'], $_POST['pwd-repeat'], $_GET['id'])) { - $password = $_POST['pwd']; - $passwordRepeat = $_POST['pwd-repeat']; - $token = $_GET['id']; - - // Check if passwords match - if ($password === $passwordRepeat) { - // Hash the new password - $hashedPassword = password_hash($password, PASSWORD_BCRYPT); - - // Get user ID based on token - $userId = User::getUserIdByToken($token); - - if ($userId !== null) { - // Update user's password - User::updatePassword($userId, $hashedPassword); - - // Redirect to login page or display success message - Utility::redirect('login'); + if (isset($_POST['pwd'], $_POST['pwd-repeat'], $_GET['token'])) { + $password = $_POST['pwd']; + $passwordRepeat = $_POST['pwd-repeat']; + $token = $_GET['token']; + + // Check if passwords match + if ($password === $passwordRepeat) { + // Hash the new password + $hashedPassword = password_hash($password, PASSWORD_BCRYPT); + + // Get user ID based on token + $userId = User::getUserIdByToken($token); + + if ($userId !== null) { + // Update user's password + User::updatePassword($userId, $hashedPassword); + + // Redirect to login page or display success message + Utility::redirect('login'); + } else { + // Handle invalid token (redirect to an error page or display an error message) + echo "Invalid token."; + } } else { - // Handle invalid token (redirect to an error page or display an error message) - echo "Invalid token."; + // Handle password mismatch error + echo "Passwords do not match."; } } else { - // Handle password mismatch error - echo "Passwords do not match."; + // Handle missing form data error + echo "Form data is missing."; } - } else { - // Handle missing form data error - echo "Form data is missing."; - } } public function index(): void { - $this->handleEmailSubmission(); + if (empty($_GET['token'])) { + // user is accessing /password for the first time - // display form asking for user email - { + if (!empty($_POST['email'])) { + // user has submitted his email + $this->handleEmailSubmission(); + } else { + // display form asking for user email + $this->view( + view_name: 'ResetPassword', + template_title: 'Reset Password' + ); + } + } elseif (!empty($_POST['pwd'])) { + // user has submitted his new password + $this->handlePasswordSubmission(); + } else { + // ask user for his new password $this->view( - view_name: 'ResetPassword', + view_name: 'Newpassword', template_title: 'Reset Password' ); } } } + diff --git a/src/views/Newpassword.php b/src/views/Newpassword.php index e319b3c..2b37448 100644 --- a/src/views/Newpassword.php +++ b/src/views/Newpassword.php @@ -4,13 +4,12 @@ ?> -

Enter New Password

- - - - - - - - +

Enter New Password

+ +
+ + + + +
\ No newline at end of file From 669fec924cde5caf3ddeacabd97e080e06c7ea9a Mon Sep 17 00:00:00 2001 From: divyesh000 Date: Sun, 21 Apr 2024 21:54:15 +0400 Subject: [PATCH 12/21] correct errors in in user model in method getUserIdByToken and $id to $tokenHash in password controllers --- src/controllers/Password.php | 4 ++-- src/models/User.php | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index 202098e..516dcd7 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -49,7 +49,7 @@ private function handleEmailSubmission(): void if ($userId) { User::savePasswordChangeRequest($userId, $tokenHash, $expiryDate); - $resetLink = ROOT . "/password?id=$token"; + $resetLink = ROOT . "/password?token=$tokenHash"; try { $this->sendResetEmail($submitted_email, $resetLink); @@ -119,7 +119,7 @@ public function index(): void // ask user for his new password $this->view( view_name: 'Newpassword', - template_title: 'Reset Password' + template_title: 'New Password' ); } } diff --git a/src/models/User.php b/src/models/User.php index b209961..cff81d9 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -226,12 +226,17 @@ public static function savePasswordChangeRequest(int $userId, string $tokenHash, public static function getUserIdByToken(string $token): ?int { - //Implement logic to fetch user ID by token from the database - $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = false"; + // Implement logic to fetch user ID by token from the database + $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = 0"; $result = self::query($query, ['token' => $token]); - return $result[0]['user_id'] ?? null; + + if ($result && !empty($result[0]->user_id)) { + return $result[0]->user_id; + } + + return null; } - + public static function updatePassword(int $userId, string $hashedPassword): void { // Implement logic to update user's password in the database From 9775e06129132c5106b5ed42a1733400c7781e8f Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:51:40 +0400 Subject: [PATCH 13/21] let PHPMailer throw exceptions --- src/core/Mailer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Mailer.php b/src/core/Mailer.php index a7851f1..3eedebb 100644 --- a/src/core/Mailer.php +++ b/src/core/Mailer.php @@ -24,7 +24,7 @@ class Mailer public function __construct() { //Create a new PHPMailer instance - $this->mail = new PHPMailer(); + $this->mail = new PHPMailer(true); // class will throw exceptions on errors, which we need to catch //Tell PHPMailer to use SMTP $this->mail->isSMTP(); From 1cf075f29f7c6484eeeb71a29ed6a44349135ffc Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:06:06 +0400 Subject: [PATCH 14/21] improve ui and display dialog when email submitted successfully --- src/views/ResetPassword.php | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/views/ResetPassword.php b/src/views/ResetPassword.php index 300e4e6..9814414 100644 --- a/src/views/ResetPassword.php +++ b/src/views/ResetPassword.php @@ -2,11 +2,33 @@ declare(strict_types=1); +/** + * @var bool $email_submit_success Whether email was successfully sent + */ ?> -

Reset Password

+
+

Reset Password

+

Just need to confirm your email to send you instructions to reset your password.

+
+ + +
+
-
- - -
+> +
+

Email submitted! 🎉

+

Thanks - if you have a Steamy Sips account, we've sent you an email.

+ +
+
\ No newline at end of file From 23bd99b1a4e3bfb765c7b9251d9116fc6f2e7085 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:06:50 +0400 Subject: [PATCH 15/21] refactor, handle errors gracefully, move token generation to User class --- src/controllers/Password.php | 66 ++++++++++++++++++++++++------------ src/models/User.php | 28 ++++++++++++--- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index 516dcd7..1d26ab7 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -5,31 +5,48 @@ namespace Steamy\Controller; use PHPMailer\PHPMailer\Exception; +use Random\RandomException; use Steamy\Core\Mailer; use Steamy\Model\User; use Steamy\Core\Controller; use Steamy\Core\Utility; /** - * Displays form asking for email and handles email submission for password reset. + * Controller responsible for managing entire password reset user flow. It + * displays a form asking for user email, handles email submission, sends email, + * handles submission for new password. */ class Password { use Controller; + private array $view_data = []; + private bool $server_error; + + public function __construct() + { + $this->server_error = false; + $this->view_data['email_submit_success'] = false; + } + /** + * Sends an email with a password reset link * @throws Exception */ private function sendResetEmail(string $email, string $resetLink): void { //Implement logic to send reset email using Mailer class $mailer = new Mailer(); - $subject = "Reset Your Password"; + $subject = "Reset Your Password | Steamy Sips"; $htmlMessage = "Click the link below to reset your password:
$resetLink"; $plainMessage = "Click the link below to reset your password:\n$resetLink"; $mailer->sendMail($email, $subject, $htmlMessage, $plainMessage); } + /** + * @throws RandomException Token could not be generated + * @throws Exception Email could not be sent + */ private function handleEmailSubmission(): void { $submitted_email = filter_var($_POST['email'] ?? "", FILTER_VALIDATE_EMAIL); @@ -39,27 +56,22 @@ private function handleEmailSubmission(): void } // email is valid - // Generate random token - $token = bin2hex(random_bytes(16)); // Generating a random token of length 32 bytes (hexadecimal format) - - // Save information about request in the password_change_request table - $expiryDate = date('Y-m-d H:i:s', strtotime('+1 day')); // Expiry date set to 1 day from now - $tokenHash = password_hash($token, PASSWORD_BCRYPT); // Hashing the token for security + // get user ID corresponding to user email $userId = User::getUserIdByEmail($submitted_email); // Get user ID by email - if ($userId) { - User::savePasswordChangeRequest($userId, $tokenHash, $expiryDate); - $resetLink = ROOT . "/password?token=$tokenHash"; - - try { - $this->sendResetEmail($submitted_email, $resetLink); - echo 'Please check your email. We have sent you an email with a link to change your password'; - } catch (Exception $e) { - echo 'Mailer credentials invalid'; - } - } else { - echo $submitted_email . " not in database"; + // if user is not present in database, simply return + // Note: For privacy reasons, we do not inform the client as the person requesting + // the password reset may not be the true owner of the email + if (empty($userId)) { + return; } + + // Get a token corresponding a password change request + $tokenHash = User::savePasswordChangeRequest($userId); + + // Send email to user with password reset link + $passwordResetLink = ROOT . "/password?token=$tokenHash"; + $this->sendResetEmail($submitted_email, $passwordResetLink); } public function handlePasswordSubmission(): void @@ -104,11 +116,23 @@ public function index(): void if (!empty($_POST['email'])) { // user has submitted his email - $this->handleEmailSubmission(); + try { + $this->handleEmailSubmission(); + $this->view_data['email_submit_success'] = true; + } catch (\Exception $e) { + $this->server_error = true; + } + } + + if ($this->server_error) { + // TODO: Call error handler + echo 'Mailing service is down. Please try again later.'; } else { // display form asking for user email + // this form should be displayed before and after email submission $this->view( view_name: 'ResetPassword', + view_data: $this->view_data, template_title: 'Reset Password' ); } diff --git a/src/models/User.php b/src/models/User.php index cff81d9..51007e9 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -4,6 +4,7 @@ namespace Steamy\Model; +use Random\RandomException; use Steamy\Core\Model; abstract class User @@ -215,12 +216,29 @@ public static function getUserIdByEmail(string $email): ?int return $result[0]->user_id; } - public static function savePasswordChangeRequest(int $userId, string $tokenHash, string $expiryDate): void + /** + * @param int $userId + * @return string|null The hashed token for a password reset + * @throws RandomException Unable to generate token + */ + public static function savePasswordChangeRequest(int $userId): ?string { - //Implement logic to save password change request in the database + if ($userId < 0) { + return null; + } + + // Generate random token and its associated information + $token = bin2hex(random_bytes(16)); // length 32 bytes (hexadecimal format) + $expiryDate = date('Y-m-d H:i:s', strtotime('+1 day')); // Expiry date set to 1 day from now + $tokenHash = password_hash($token, PASSWORD_BCRYPT); // Hashing the token for security + + // Save password change info to database $query = "INSERT INTO password_change_request (user_id, token_hash, expiry_date) VALUES (:userId, :tokenHash, :expiryDate)"; + self::query($query, ['userId' => $userId, 'tokenHash' => $tokenHash, 'expiryDate' => $expiryDate]); + + return $tokenHash; } @@ -229,14 +247,14 @@ public static function getUserIdByToken(string $token): ?int // Implement logic to fetch user ID by token from the database $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = 0"; $result = self::query($query, ['token' => $token]); - + if ($result && !empty($result[0]->user_id)) { return $result[0]->user_id; } - + return null; } - + public static function updatePassword(int $userId, string $hashedPassword): void { // Implement logic to update user's password in the database From 550dd398cb12c23e75c3780d6799a95555df65e6 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:17:49 +0400 Subject: [PATCH 16/21] implement new UI with error handling --- src/views/NewPassword.php | 35 +++++++++++++++++++++++++++++++++++ src/views/Newpassword.php | 15 --------------- 2 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 src/views/NewPassword.php delete mode 100644 src/views/Newpassword.php diff --git a/src/views/NewPassword.php b/src/views/NewPassword.php new file mode 100644 index 0000000..50da315 --- /dev/null +++ b/src/views/NewPassword.php @@ -0,0 +1,35 @@ + + +
+

Enter your new password

+ +
+ + + + +
+
+ +> + + \ No newline at end of file diff --git a/src/views/Newpassword.php b/src/views/Newpassword.php deleted file mode 100644 index 2b37448..0000000 --- a/src/views/Newpassword.php +++ /dev/null @@ -1,15 +0,0 @@ - - -

Enter New Password

- -
- - - - -
- \ No newline at end of file From 4bb3af238ae71735e668da23ce92314117f07a35 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:18:09 +0400 Subject: [PATCH 17/21] show any errors --- src/views/ResetPassword.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/ResetPassword.php b/src/views/ResetPassword.php index 9814414..86d6c8e 100644 --- a/src/views/ResetPassword.php +++ b/src/views/ResetPassword.php @@ -4,6 +4,7 @@ /** * @var bool $email_submit_success Whether email was successfully sent + * @var string $error */ ?> @@ -13,6 +14,7 @@
From 9e178531eaf0297ceab160030adbb44a6a9edf3c Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:19:12 +0400 Subject: [PATCH 18/21] add question mark --- src/views/Login.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Login.php b/src/views/Login.php index 993b74c..4a83ee4 100644 --- a/src/views/Login.php +++ b/src/views/Login.php @@ -24,7 +24,7 @@ Register - Forgot Password + Forgot Password? From 68a16152fb71ab8c2188feeb561d3ab053fadeef Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:25:05 +0400 Subject: [PATCH 19/21] rework algorithm for password reset - after generating token, return request id and token - improve logic for verifying token --- src/models/User.php | 92 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 22 deletions(-) diff --git a/src/models/User.php b/src/models/User.php index 51007e9..50a5e49 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -4,7 +4,7 @@ namespace Steamy\Model; -use Random\RandomException; +use Exception; use Steamy\Core\Model; abstract class User @@ -217,49 +217,97 @@ public static function getUserIdByEmail(string $email): ?int } /** - * @param int $userId - * @return string|null The hashed token for a password reset - * @throws RandomException Unable to generate token + * Generates a password reset token for a user. A hash of the token + * is stored in the database. + * + * @param int $userId ID of user to which password reset token belongs + * @return array|null An associative array with attributes `request_id` and `token` + * @throws Exception */ - public static function savePasswordChangeRequest(int $userId): ?string + public static function generatePasswordResetToken(int $userId): ?array { if ($userId < 0) { return null; } + $info = []; // array to store token and request ID + // Generate random token and its associated information - $token = bin2hex(random_bytes(16)); // length 32 bytes (hexadecimal format) + $info['token'] = ""; + try { + $info['token'] = bin2hex(random_bytes(16)); // length 32 bytes (hexadecimal format) + } catch (Exception) { + throw new Exception('Token cannot be generated'); + } + $expiryDate = date('Y-m-d H:i:s', strtotime('+1 day')); // Expiry date set to 1 day from now - $tokenHash = password_hash($token, PASSWORD_BCRYPT); // Hashing the token for security + $tokenHash = password_hash($info['token'], PASSWORD_BCRYPT); // Hashing the token for security - // Save password change info to database + // Save token info to database $query = "INSERT INTO password_change_request (user_id, token_hash, expiry_date) VALUES (:userId, :tokenHash, :expiryDate)"; - self::query($query, ['userId' => $userId, 'tokenHash' => $tokenHash, 'expiryDate' => $expiryDate]); - return $tokenHash; + + // get ID of last inserted record + // Ref: https://stackoverflow.com/a/39141771/17627866 + $query = <<< EOL + SELECT LAST_INSERT_ID(request_id) as request_id + FROM password_change_request + ORDER BY LAST_INSERT_ID(request_id) + DESC LIMIT 1; + EOL; + $info['request_id'] = self::query($query)[0]->request_id; + + return $info; } - public static function getUserIdByToken(string $token): ?int + /** + * Uses a password reset token to change the password of a user. + * @param int $requestID request ID corresponding to password reset token + * @param string $token Password reset token + * @param string $new_password New password of user in plain text (not hashed) + * @return bool + */ + public static function resetPassword(int $requestID, string $token, string $new_password): bool { - // Implement logic to fetch user ID by token from the database - $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = 0"; - $result = self::query($query, ['token' => $token]); + // fetch matching request from database if valid (not expired and unused) + $query = <<< EOL + SELECT * FROM password_change_request + WHERE expiry_date > NOW() AND used = 0 AND request_id = :requestID + EOL; + + $result = self::query($query, ['requestID' => $requestID]); - if ($result && !empty($result[0]->user_id)) { - return $result[0]->user_id; + if (empty($result)) { + return false; } - return null; - } + $request = $result[0]; - public static function updatePassword(int $userId, string $hashedPassword): void - { - // Implement logic to update user's password in the database + // verify token + if (!password_verify($token, $request->token_hash)) { + return false; + } + + // validate password + if (!empty(self::validatePlainPassword($new_password))) { + return false; + } + + $hashedPassword = password_hash($new_password, PASSWORD_BCRYPT); + + // Update user's password in the database $query = "UPDATE user SET password = :password WHERE user_id = :userId"; - self::query($query, ['password' => $hashedPassword, 'userId' => $userId]); + self::query($query, ['password' => $hashedPassword, 'userId' => $request->user_id]); + + + // Invalidate password request token so that token cannot be used again + $query = "UPDATE password_change_request SET used = 1 WHERE request_id = :requestID"; + self::query($query, ['requestID' => $request->request_id]); + + return true; } } From 87eceefacfd3b1698d830fb8d7c3475b0049043b Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:28:19 +0400 Subject: [PATCH 20/21] - add form validation - update password reset link - display appropriate error messages - handle exceptions - handle routing better & show 404 page when needed --- src/controllers/Password.php | 173 +++++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 69 deletions(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index 1d26ab7..f9b05c1 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -4,15 +4,14 @@ namespace Steamy\Controller; -use PHPMailer\PHPMailer\Exception; -use Random\RandomException; +use Exception; use Steamy\Core\Mailer; use Steamy\Model\User; use Steamy\Core\Controller; -use Steamy\Core\Utility; /** - * Controller responsible for managing entire password reset user flow. It + * Controller responsible for managing the entire password reset user flow. It is invoked + * for relative urls of the form /password. It * displays a form asking for user email, handles email submission, sends email, * handles submission for new password. */ @@ -21,12 +20,12 @@ class Password use Controller; private array $view_data = []; - private bool $server_error; public function __construct() { - $this->server_error = false; $this->view_data['email_submit_success'] = false; + $this->view_data['error'] = false; + $this->view_data['password_change_success'] = false; } /** @@ -44,7 +43,7 @@ private function sendResetEmail(string $email, string $resetLink): void } /** - * @throws RandomException Token could not be generated + * Invoked when user submits an email on form. * @throws Exception Email could not be sent */ private function handleEmailSubmission(): void @@ -52,100 +51,136 @@ private function handleEmailSubmission(): void $submitted_email = filter_var($_POST['email'] ?? "", FILTER_VALIDATE_EMAIL); if (empty($submitted_email)) { + $this->view_data['error'] = 'Invalid email'; return; } // email is valid // get user ID corresponding to user email - $userId = User::getUserIdByEmail($submitted_email); // Get user ID by email + $userId = User::getUserIdByEmail($submitted_email); - // if user is not present in database, simply return - // Note: For privacy reasons, we do not inform the client as the person requesting - // the password reset may not be the true owner of the email + // check if account is not present in database if (empty($userId)) { + $this->view_data['error'] = 'Email does not exist'; return; } - // Get a token corresponding a password change request - $tokenHash = User::savePasswordChangeRequest($userId); + // Generate a token for a password change request + try { + $token_info = User::generatePasswordResetToken($userId); + } catch (Exception) { + $this->view_data['error'] = 'Mailing service is not operational. Try again later'; + return; + } - // Send email to user with password reset link - $passwordResetLink = ROOT . "/password?token=$tokenHash"; + // Send email to user with password reset link and user id + $passwordResetLink = ROOT . "/password/reset?token=" . $token_info['token'] . + "&id=" . $token_info['request_id']; $this->sendResetEmail($submitted_email, $passwordResetLink); } - public function handlePasswordSubmission(): void + /** + * Checks if password reset link contains the necessary token and id query parameters. + * @return bool + */ + private function validatePasswordResetLink(): bool { - if (isset($_POST['pwd'], $_POST['pwd-repeat'], $_GET['token'])) { - $password = $_POST['pwd']; - $passwordRepeat = $_POST['pwd-repeat']; - $token = $_GET['token']; - - // Check if passwords match - if ($password === $passwordRepeat) { - // Hash the new password - $hashedPassword = password_hash($password, PASSWORD_BCRYPT); - - // Get user ID based on token - $userId = User::getUserIdByToken($token); - - if ($userId !== null) { - // Update user's password - User::updatePassword($userId, $hashedPassword); - - // Redirect to login page or display success message - Utility::redirect('login'); - } else { - // Handle invalid token (redirect to an error page or display an error message) - echo "Invalid token."; - } - } else { - // Handle password mismatch error - echo "Passwords do not match."; - } + // check if query parameters are present + if (empty($_GET['token']) || empty($_GET['id'])) { + return false; + } + + // validate request id data type + if (!filter_var($_GET['id'], FILTER_VALIDATE_INT)) { + return false; + } + + return true; + } + + /** + * This function is invoked when user opens password reset link from email + * and submits form. + * @return void + */ + private function handlePasswordSubmission(): void + { + if (!$this->validatePasswordResetLink()) { + $this->view_data['error'] = 'Invalid password reset link'; + } + + if (!isset($_POST['pwd'], $_POST['pwd-repeat'])) { + $this->view_data['error'] = 'You must enter new password twice'; + return; + } + + $password = $_POST['pwd']; + $passwordRepeat = $_POST['pwd-repeat']; + $token = $_GET['token']; + $requestID = filter_var($_GET['id'], FILTER_VALIDATE_INT); + + // Check if passwords match + if ($password !== $passwordRepeat) { + $this->view_data['error'] = 'Passwords do not match'; + return; + } + + // check if password valid + $password_errors = User::validatePlainPassword($password); + if (!empty($password_errors)) { + $this->view_data['error'] = $password_errors[0]; + return; + } + + $success = User::resetPassword($requestID, $token, $password); + + if ($success) { + $this->view_data['password_change_success'] = true; } else { - // Handle missing form data error - echo "Form data is missing."; + $this->view_data['error'] = 'Failed to change password. Try generating a new token.'; } } public function index(): void { - if (empty($_GET['token'])) { - // user is accessing /password for the first time - - if (!empty($_POST['email'])) { + // check if url is of form /password + if ($_GET['url'] === 'password') { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { // user has submitted his email try { $this->handleEmailSubmission(); - $this->view_data['email_submit_success'] = true; - } catch (\Exception $e) { - $this->server_error = true; + } catch (Exception) { + $this->view_data['error'] = 'Mailing service is not operational. Please try again later.'; } } + // display form asking for user email + // this form should be displayed before and after email submission + $this->view( + view_name: 'ResetPassword', + view_data: $this->view_data, + template_title: 'Reset Password' + ); + return; + } - if ($this->server_error) { - // TODO: Call error handler - echo 'Mailing service is down. Please try again later.'; - } else { - // display form asking for user email - // this form should be displayed before and after email submission - $this->view( - view_name: 'ResetPassword', - view_data: $this->view_data, - template_title: 'Reset Password' - ); + // check if url is of form /password/reset + if ($_GET['url'] === 'password/reset') { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePasswordSubmission(); } - } elseif (!empty($_POST['pwd'])) { - // user has submitted his new password - $this->handlePasswordSubmission(); - } else { - // ask user for his new password + // display form asking user for his new password $this->view( - view_name: 'Newpassword', + view_name: 'NewPassword', + view_data: $this->view_data, template_title: 'New Password' ); + return; } + + // if url follows some other format display error page + $this->view( + view_name: '404' + ); } } From e5115e58530cc27bf496a07a87b0427318b93306 Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:52:16 +0400 Subject: [PATCH 21/21] return from handlePasswordSubmission when reset link is invalid --- src/controllers/Password.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/Password.php b/src/controllers/Password.php index f9b05c1..a9bd9de 100644 --- a/src/controllers/Password.php +++ b/src/controllers/Password.php @@ -81,7 +81,7 @@ private function handleEmailSubmission(): void /** * Checks if password reset link contains the necessary token and id query parameters. - * @return bool + * @return bool True if valid */ private function validatePasswordResetLink(): bool { @@ -107,6 +107,7 @@ private function handlePasswordSubmission(): void { if (!$this->validatePasswordResetLink()) { $this->view_data['error'] = 'Invalid password reset link'; + return; } if (!isset($_POST['pwd'], $_POST['pwd-repeat'])) {