Skip to content

Commit

Permalink
Merge branch 'main' into new-db-schema
Browse files Browse the repository at this point in the history
  • Loading branch information
creme332 authored Apr 24, 2024
2 parents e7d3583 + 4138d9d commit bcfb567
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ jobs:
BUSINESS_GMAIL=""
BUSINESS_GMAIL_PASSWORD=""
run: |
echo "$ENV" > src/core/.env
cat src/core/.env
echo "$ENV" > .env
cat .env
- name: Validate composer.json and composer.lock
run: composer validate --strict
Expand Down
3 changes: 2 additions & 1 deletion docs/INSTALLATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Install composer dependencies:
composer update
```

In the [`src/core/`](../src/core/config.php) folder, create a `.env` file with the following contents:
In the root directory, create a `.env` file with the following contents:

```php
PUBLIC_ROOT="http://localhost/steamy-sips/public"
Expand Down Expand Up @@ -116,6 +116,7 @@ In the root directory of the project, run:
```bash
npm install
```

## Autoload setup

Whenever changes are made to the autoload settings in `composer.json`, you must run `composer dump-autoload`.
2 changes: 1 addition & 1 deletion docs/USAGE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Optionally, you can display a live error log:
sudo tail -f /var/log/apache2/error.log
```

Enter the `PUBLIC_ROOT` value (e.g., http://localhost/steamy-sips/public/) from [`src/core/.env`](../src/core/.env) in
Enter the `PUBLIC_ROOT` value (e.g., http://localhost/steamy-sips/public/) from [`.env`](../.env) in
your browser
to access the client website.

Expand Down
30 changes: 29 additions & 1 deletion resources/database/dump/cafe.sql
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,34 @@ LOCK TABLES `order_product` WRITE;
/*!40000 ALTER TABLE `order_product` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `password_change_request`
--

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_request` (
`request_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) unsigned NOT NULL,
`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_request`
--

LOCK TABLES `password_change_request` WRITE;
/*!40000 ALTER TABLE `password_change_request` DISABLE KEYS */;
/*!40000 ALTER TABLE `password_change_request` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `product`
--
Expand Down Expand Up @@ -364,4 +392,4 @@ UNLOCK TABLES;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2024-04-24 8:19:02
-- Dump completed on 2024-04-24 8:19:02
188 changes: 188 additions & 0 deletions src/controllers/Password.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

declare(strict_types=1);

namespace Steamy\Controller;

use Exception;
use Steamy\Core\Mailer;
use Steamy\Model\User;
use Steamy\Core\Controller;

/**
* 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.
*/
class Password
{
use Controller;

private array $view_data = [];

public function __construct()
{
$this->view_data['email_submit_success'] = false;
$this->view_data['error'] = false;
$this->view_data['password_change_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 | Steamy Sips";
$htmlMessage = "Click the link below to reset your password:<br><a href='$resetLink'>$resetLink</a>";
$plainMessage = "Click the link below to reset your password:\n$resetLink";
$mailer->sendMail($email, $subject, $htmlMessage, $plainMessage);
}

/**
* Invoked when user submits an email on form.
* @throws Exception Email could not be sent
*/
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);

// check if account is not present in database
if (empty($userId)) {
$this->view_data['error'] = 'Email does not exist';
return;
}

// 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 and user id
$passwordResetLink = ROOT . "/password/reset?token=" . $token_info['token'] .
"&id=" . $token_info['request_id'];
$this->sendResetEmail($submitted_email, $passwordResetLink);
}

/**
* Checks if password reset link contains the necessary token and id query parameters.
* @return bool True if valid
*/
private function validatePasswordResetLink(): bool
{
// 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';
return;
}

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 {
$this->view_data['error'] = 'Failed to change password. Try generating a new token.';
}
}

public function index(): void
{
// 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) {
$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;
}

// check if url is of form /password/reset
if ($_GET['url'] === 'password/reset') {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$this->handlePasswordSubmission();
}
// display form asking user for his new password
$this->view(
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'
);
}
}

25 changes: 15 additions & 10 deletions src/core/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use PHPMailer\PHPMailer\SMTP;

/**
* Class for sending mails to clients
* Class for sending mails
*
* Reference: https://github.com/PHPMailer/PHPMailer/blob/master/examples/gmail.phps
*/
Expand All @@ -23,7 +23,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();
Expand All @@ -32,7 +32,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';
Expand Down Expand Up @@ -65,25 +65,30 @@ public function __construct()
}

/**
* @throws Exception
* @param string $email Gmail address of recipient
* @param string $subject Email subject line
* @param string $html_message Message body as an HTML string
* @param string $plain_message Message as plain text
* @return bool false on error - See the ErrorInfo property for details of the error
* @throws Exception Error when calling addAddress or msgHTML
*/
public function sendMail(string $email, string $subject, $html_message, $plain_message): void
public function sendMail(string $email, string $subject, string $html_message, string $plain_message): bool
{
//Set who the message is to be sent to
$this->mail->addAddress($email);

//Set the subject line
$this->mail->Subject = $subject;

//Read an HTML message body from an external file, convert referenced images to embedded,
//convert HTML into a basic plain-text alternative body
// Read an HTML message body from an external file, convert referenced images to embedded,
// convert HTML into a basic plain-text alternative body
$this->mail->msgHTML($html_message);

//Replace the plain text body with one created manually
// Replace the plain text body with one created manually
$this->mail->AltBody = $plain_message;

//send the message
$this->mail->send();
// Send the message
return $this->mail->send();
}
}

2 changes: 1 addition & 1 deletion src/core/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
declare(strict_types=1);

// load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__.'/../..');
$dotenv->load();

// define absolute URL to public folder
Expand Down
Loading

0 comments on commit bcfb567

Please sign in to comment.