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] - 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' + ); } }