Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verification & Updates via SendGrid #4

Merged
merged 6 commits into from
Nov 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ DB_USER=mkdocs_user
DB_PASSWORD=mkdocs_password

# Adminer port
ADMINER_PORT=9321
ADMINER_PORT=9321

# Sendgrid API key
SENDGRID_API_KEY=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
[email protected]
4 changes: 4 additions & 0 deletions app/app/config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@
define('DB_NAME', getenv('DB_NAME'));
define('DB_USER', getenv('DB_USER'));
define('DB_PASS', getenv('DB_PASSWD'));

// SendGrid
define('SENDGRID_API_KEY', getenv('SENDGRID_API_KEY'));
define('EMAIL_FROM', getenv('EMAIL_FROM'));
30 changes: 29 additions & 1 deletion app/app/controllers/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,37 @@ private function nextPage(Project|null $project, int &$currentStep, int $maxPage
if ($currentStep >= $maxPage && !$wantPrevious) {
// Save the project
$newProject = $this->loadProject($data);
if (isset($project)) {

// Check if the project is new
if (isset($project) && $project->id > 0) {
// Set the id of the project
$newProject->id = $project->id;

// Check if the status has changed
if ($project->status->value != $newProject->status->value) {
// Set the confirmed by
$newProject->confirmedBy = SessionManager::getCurrentUserId();

// Load the user repository
$userRepository = $this->loadRepository('UserRepository');
$owner = $userRepository->getUserById($project->userId);

// Check if the owner exists
if (isset($owner) && $owner->wantsUpdates) {
// Project URL
$projectUrl = URLROOT . '/ProjectController/edit/' . $newProject->id . '/3';

// Inform the user about the status change
$this->logger->log('The status of project ' . $newProject->id . ' has changed to ' . $newProject->status->value . ' by ' . SessionManager::getCurrentUserId(), Logger::INFO);
$sg = new SendgridService();
$sg->sendStatusChanged($owner->name, $owner->email, $newProject->title, $projectUrl, $newProject->status->name);
} else {
$this->logger->log('Owner of project ' . $newProject->id . ' does not exist', Logger::WARNING);
}
}
}

// // Save the project
$this->projectRepository->save($newProject);
redirect('', true);
}
Expand Down
251 changes: 234 additions & 17 deletions app/app/controllers/UserController.php

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/app/core/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ private function initTwig()
'autoescape' => 'html',
]);

$this->logger->log('The Twig has been initialized', Logger::NOTICE);
$this->logger->log('The Twig has been initialized', Logger::INFO);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/app/core/LogManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function __construct(string $loggerName)
$this->logger->pushHandler($handler);
}

$this->log('The logger has been initialized', Logger::NOTICE);
$this->log('The logger has been initialized', Logger::DEBUG);
}

/**
Expand Down
153 changes: 153 additions & 0 deletions app/app/core/SendgridService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

use Monolog\Logger;
use \SendGrid\Mail\TemplateId;
use \SendGrid\Mail\Mail;

class SendGridServiceException extends Exception
{
}

/**
* Service for sending emails via SendGrid
*
* @link [SendGrid](https://sendgrid.com/)
* @link [SendGrid PHP Library](https://github.com/sendgrid/sendgrid-php)
*/
class SendgridService
{

private SendGrid $sg;
private LogManager $logger;

private string $verificationTemplateId = 'd-9a0bbff0f3a54c6ea9e1c449e690eb88';
private string $statusTemplateId = 'd-4ddbea2de78d40ea88260162df4846e1';

public function __construct()
{
$this->logger = new LogManager('php-sendgrid');
$this->sg = new SendGrid(SENDGRID_API_KEY);
$this->logger->log('The SendGrid service has been initialized', Logger::DEBUG);
}

/**
* Throws an exception and logs it
*
* @param string $error The error message
* @throws Exception The exception that was thrown
*/
protected function throwError(string $error)
{
$this->logger->log($error, Logger::ERROR);
throw new SendGridServiceException($error);
}

/**
* Sends a verification email to the given email address
*
* @param string $name
* @param string $emailTo
* @param string $token
* @throws SendGridServiceException If the email could not be sent or the email address is invalid
*/
public function sendVerification(string $name, string $emailTo, string $token)
{
// Validate the parameters
if (empty($name) || empty($emailTo) || empty($token)) {
$this->throwError('The name, email, and token are required to send a verification email');
}
if (!filter_var($emailTo, FILTER_VALIDATE_EMAIL)) {
$this->throwError('The email address is not valid');
}

$this->logger->log("Sending verification email to $emailTo", Logger::DEBUG);

// Create the email
$email = new Mail();
$email->setFrom(EMAIL_FROM, 'MkSimple');
$email->addTo($emailTo, $name);
$email->setTemplateId(new TemplateId($this->verificationTemplateId));

// Fill in the template variables
$email->addDynamicTemplateDatas([
'name' => $name,
'verification_url' => URLROOT . "/UserController/verify/$token",
]);

// Send the email
$response = $this->sg->send($email);

// Check the response
if ($this->resolveStatusCode($response->statusCode())) {
$this->logger->log('Verification email sent successfully to ' . $emailTo, Logger::INFO);
}
}

/**
* Sends a status email to the given email address
*
* @param string $name The name of the user
* @param string $emailTo The email address of the user
* @param string $projectName The name of the project
* @param string $projectUrl The URL of the project
* @param string $status The status of the project
*/
public function sendStatusChanged(string $name, string $emailTo, string $projectName, string $projectUrl, string $status)
{
// Validate the parameters
if (empty($name) || empty($emailTo) || empty($projectName) || empty($projectUrl) || empty($status)) {
$this->throwError('The name, email, project name, project URL, and status are required to send a status email');
}
if (!filter_var($emailTo, FILTER_VALIDATE_EMAIL)) {
$this->throwError('The email address is not valid');
}

$this->logger->log('Sending status change email to ' . $emailTo, Logger::DEBUG);

// Create the email
$email = new Mail();
$email->setFrom(EMAIL_FROM, 'MkSimple');
$email->addTo($emailTo, $name);
$email->setTemplateId(new TemplateId($this->statusTemplateId));

// Fill in the template variables
$email->addDynamicTemplateDatas([
'name' => $name,
'project_name' => $projectName,
'status' => $status,
'project_url' => $projectUrl,
]);

// Send the email
$response = $this->sg->send($email);

// Check the response
if ($this->resolveStatusCode($response->statusCode())) {
$this->logger->log('Verification email sent successfully to ' . $emailTo, Logger::INFO);
}
}

/**
* Resolves the status code of the response
*
* @param integer $statusCode The status code
* @return boolean True if the status code is 2xx
* @throws SendGridServiceException If the status code is not 2xx
*/
private function resolveStatusCode(int $statusCode): bool
{
if ($statusCode >= 200 && $statusCode < 300) {
// Success
return true;
}

if ($statusCode == 429) {
// Too many requests (rate limit)
$this->throwError('Too many requests to SendGrid (max: 100 per day)');
return false;
}

// Other error
$this->throwError("An unknown error ($statusCode) occurred while sending the email");
}
}
3 changes: 2 additions & 1 deletion app/app/init.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
require_once 'helpers/url_helper.php';

// Die Main-Klassen unserer App
require_once 'core/App.php';
require_once 'core/LogManager.php';
require_once 'core/SessionManager.php';
require_once 'core/App.php';
require_once 'core/Controller.php';
require_once 'core/SendgridService.php';
21 changes: 0 additions & 21 deletions app/app/repositories/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,27 +135,6 @@ public function getUserByEmail(string $email): User|null
return $this->loadUser($result);
}

/**
* Gets the user by the verification code
*
* @param string $verificationCode The verification code of the user
* @return User The user if found, `null` otherwise
*/
public function getUserByVerificationCode(string $verificationCode): User|null
{
$this->logger->log("Searching for a user by the verification code '$verificationCode'", Logger::DEBUG);

// Get the user
$this->db->query('SELECT * FROM user WHERE verificationCode = :verificationCode LIMIT 1');
$this->db->bind(':verificationCode', $verificationCode);

// Get the result
$result = $this->db->single();

// Check if the user was found
return $this->loadUser($result);
}

/**
* Gets all the users from the database ordered by the creation date
*
Expand Down
15 changes: 15 additions & 0 deletions app/app/views/input/message.twig.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@ <h3 class="text-lg font-bold">{{ message_title }}</h3>
{% endif %}

{% endmacro %}

{% macro message_raw(modal_id, message_title, message) %}

<input type="checkbox" id="{{ modal_id }}" class="modal-toggle" />
<div class="modal">
<div class="modal-box relative">
<label for="{{ modal_id }}" class="btn btn-sm btn-circle absolute right-2 top-2">✕</label>
<h3 class="text-lg font-bold">{{ message_title }}</h3>
<div class="py-4">
{{ message|raw }}
</div>
</div>
</div>

{% endmacro %}
6 changes: 3 additions & 3 deletions app/app/views/landingpage.twig.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
{% block content %}
<div class="grid grid-cols-2 place-items-center px-5">
<!-- title -->
<h2 class="tracking-wider font-bold text-3xl pl-4 lg:text-5xl leading-relaxed col-span-2 sm:col-span-1">
<h2 class="tracking-wider font-bold text-3xl pl-4 py-6 sm:py-1 lg:text-5xl leading-relaxed col-span-2 sm:col-span-1">
Configure
<span class="box-decoration-clone bg-gradient-to-r from-primary to-secondary text-white px-3 leading-loose text-4xl lg:text-6xl">MkDocs</span>
<span class="box-decoration-clone bg-gradient-to-r from-primary to-secondary text-white px-3 leading-loose text-4xl xl:text-6xl">MkDocs</span>
<span class="whitespace-nowrap">with ease</span>
</h2>
<img class="h-70 col-span-2 xl:row-span-2 sm:col-span-1" alt="girl writing a blog illustration" src="/img/article.svg" />
Expand Down Expand Up @@ -52,7 +52,7 @@ <h2 class="tracking-wider font-bold text-3xl pl-4 lg:text-5xl leading-relaxed co

<!-- information -->
<img class="w-100 col-span-2 sm:col-span-1 md:hover:scale-75 delay-200 transition-all duration-700" alt="girl writing a blog illustration" src="/img/algorithm.svg" />
<div class="col-span-2 sm:col-span-1">
<div class="col-span-2 sm:col-span-1 py-5">
<h3 class="tracking-wider font-bold text-3xl leading-relaxed">What is MkDocs?</h3>
<hr />
<p class="text-lg">
Expand Down
16 changes: 12 additions & 4 deletions app/app/views/user/signin.twig.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends "base/main.twig.html" %}
{% set wantFooter = true %}
{% set reset_modal = random(1000, 2000) %}

{% import "input/inputs.twig.html" as inputs %}
{% import "input/message.twig.html" as message %}
Expand All @@ -20,9 +21,9 @@ <h2 class="card-title">Sign In</h2>
{{ inputs.input_field('email', data.email, data.email_err, 'email', 255) }}
{{ inputs.input_field('password', data.password, data.password_err, 'password', 500) }}

<div class="tooltip tooltip-bottom" data-tip="Stay calm! Think in silence and try again or open your password manager 😉">
<p class="underline underline-offset-4 pt-2">Forgot your password?</p>
</div>
{% if allow_reset %}
<label for="{{ reset_modal }}" class="underline underline-offset-4 pt-2">Forgot your password?</label>
{% endif %}

<div class="form-control mt-5">
<button type="submit" class="btn btn-primary">Sign In</button>
Expand All @@ -35,6 +36,13 @@ <h2 class="card-title">Sign In</h2>

{% import "input/message.twig.html" as message %}

{{ message.message('Verification error', message) }}
{{ message.message(message_title, message) }}

{% set password_reset %}
<p>If you have forgotten your password or lost the verification email, you can reset it by clicking the button below. You will then receive a verification email where you can reset your password.</p>
<a href="/UserController/passwordReset/{{ data.email|url_encode }}" class="btn btn-primary btn-sm mt-4">Reset credentials</a>
{% endset %}

{{ message.message_raw(reset_modal, 'Forgot your password?', password_reset) }}

{% endblock %}
46 changes: 46 additions & 0 deletions app/app/views/user/verify.twig.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% extends "base/main.twig.html" %}

{% import "input/inputs.twig.html" as inputs %}

{% block navlinks %}
<a href="/login/signIn" class="flex-0 btn btn-ghost px-2 text-base-content">Sign In</a>
<a href="/login/signUp" class="flex-0 btn btn-outline btn-ghost px-2 text-base-content">Sign Up</a>
{% endblock %}

{% block content %}
{% if is_valid %}

<div class="grid grid-cols-3 gap-4 py-4 mx-5 place-content-center">
<div class="col-span-3 md:col-span-2 shadow-2xl bg-base-100 p-5 rounded-md md:w-4/6 place-self-center">
<form action="{{ form_url }}" method="post" enctype="multipart/form-data">
<h2 class="card-title">Password Reset</h2>
<hr class="mt-2" />

{{ inputs.input_field('password', data.password, data.password_err, 'password', 500) }}

<input type="hidden" name="token" value="{{ data.token }}">

<div class="form-control mt-5">
<button type="submit" class="btn btn-active btn-primary">Reset</button>
</div>
</form>
</div>

<img src="/img/verification.svg" alt="verification illustration" class="h-full hidden md:block hover:scale-125 transition-all duration-700" />
</div>

{% else %}

<div class="grid grid-cols-1 gap-4 place-content-center">
<div class="h-80 w-64 place-self-center">
<img src="/img/verification.svg" alt="verification illustration">
<h4 class="text-center underline decoration-pink-500 font-semibold tracking-wide">Invalid Verification</h4>
</div>
<div class="text-center">
<p class="pb-5">Your verification code is invalid! You may have already used it.</p>
<a href="/login" class="btn btn-outline btn-primary">Login</a>
</div>
</div>

{% endif %}
{% endblock %}
1 change: 1 addition & 0 deletions app/public/img/verification.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading