Skip to content
This repository has been archived by the owner on Feb 26, 2023. It is now read-only.

Add Front-End #7

Merged
merged 21 commits into from
Nov 26, 2018
Merged
Show file tree
Hide file tree
Changes from 17 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
8 changes: 6 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"extends": "airbnb-base"
}
"extends": "airbnb-base",
"env": {
"browser": true,
"node": true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to consider having different ESLint configurations for the back-end and front-end sources so it detects incorrect Node usage in front-end code and vice versa.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the best option, I was just unsure on how to achieve that. I'll have a look

}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"body-parser": "^1.18.3",
"express": "^4.16.4",
"express-async-errors": "^3.1.1",
"liquidjs": "^6.1.1",
"nconf": "^0.10.0",
"nodemailer": "^4.7.0",
"pg-promise": "^8.5.2",
Expand Down
26 changes: 26 additions & 0 deletions public/email-templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>{{ subject }}</title>
<style media="screen">
a.big {
text-align: center;
}
a.big:link, a.big:visited {
background-color: #B8242B;
color: white;
padding: 14px 25px;
text-align: center;
text-decoration: none;
display: inline-block;
}
a.big:hover, a.big:active {
background-color: #8D1C21;
}
</style>
</head>
<body>
{% block -%}{%- endblock %}
</body>
</html>
5 changes: 5 additions & 0 deletions public/email-templates/footer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<br>
<footer>
<p>This email was sent automatically, if it wasn't you who requested this email, please ignore it.</p>
<p>You can always unsubscribe <a href="{{ unsub }}">here</a>.</p>
</footer>
10 changes: 10 additions & 0 deletions public/email-templates/verify-dice.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% layout 'base' %}
<h1>The dice have been cast!</h1>
<p>Roll-Time: {{ date }}</p>
<p>Results: {{ dice }}</p>
<br>
<p>You can click below to verify your results:</p>
<a class="big" href="{{ url }}">Verify!</a>
<br>
<p>Note we might not be able to correctly verify integrity for very old (1+ years) dice rolls</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing period at end of sentence?

{% include 'footer' %}
7 changes: 7 additions & 0 deletions public/email-templates/verify-email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% layout 'base' %}
<h1>Verify your Email on {{ host }}</h1>
<p>You (or someone else prententing to be you) recently registered this email to be used for the dice rolling service.</p>
<p>In order to allow you to use this service you need to click 'Confirm!' below:</p>
<a class="big" href="{{ url }}">Confirm!</a>
<p>Note: The link expires after 24 hours or when a new confirmation email is sent.</p>
{% include 'footer' %}
13 changes: 13 additions & 0 deletions public/partials/form-ajaxify.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% include 'submit-button' %}
<span id="error-display" style="display: none; text-align: center;"></span>
<script type="text/javascript" src="./js/ajax-form.js"></script>
<script type="text/javascript">
registerForm(
'{{ formId }}',
'submit-button',
'error-display',
'{{ method }}',
'{{ url }}',
'{{ buttonText }}',
'Success!');
</script>
14 changes: 14 additions & 0 deletions public/partials/layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>{{title}} | TripleA Dice Server</title>
<link rel="stylesheet" type="text/css" href="./css/base.css">
</head>
<body>
<div class="center">
<h1>{{title}}</h1>
{% block -%}{%- endblock %}
</div>
</body>
</html>
5 changes: 5 additions & 0 deletions public/partials/submit-button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="extended">
<div class="button-center">
<button id="submit-button" type="submit" name="button">{{ buttonText }}</button>
</div>
</div>
64 changes: 64 additions & 0 deletions public/static/css/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
button {
background-color: #B8242B;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
button:disabled {
background-color: #B8242B7F;
}
button:hover:not(:disabled) {
background-color: #8D1C21;
color: #EEE;
}
input[type=text], input[type=email]{
width: 100%;
padding: 12px 20px;
margin: 8px 0;
box-sizing: border-box;
}
.center {
margin: auto;
width: 50%;
padding: 10%;
}
.button-center {
margin: auto;
width: 50%;
text-align: center;
}
h1 {
width: 100%;
text-align: center;
}
.extended {
width: 100%;
}
.lds-dual-ring {
display: inline-block;
width: 32px;
height: 32px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 23px;
height: 23px;
margin: 1px;
border-radius: 50%;
border: 5px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
Binary file added public/static/favicon.ico
Binary file not shown.
30 changes: 30 additions & 0 deletions public/static/js/ajax-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
window.registerForm = (formId, buttonId, errorDisplayId, method, url, text, successText) => {
const form = document.getElementById(formId);
const button = document.getElementById(buttonId);
const errorDisplay = document.getElementById(errorDisplayId);
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
button.disabled = true;
button.value = '';
button.innerHTML = '<div class="lds-dual-ring"></div>';
const request = new XMLHttpRequest();
request.addEventListener('load', (serverResponse) => {
const response = JSON.parse(serverResponse.target.responseText);
if (response.status === 'OK') {
errorDisplay.style.display = 'none';
button.innerHTML = successText;
button.disabled = false;
} else {
errorDisplay.style.display = 'block';
errorDisplay.innerHTML = response.errors.join('<br>');
button.disabled = false;
button.innerHTML = text;
}
});
request.open(method, url);
// urlencode the FormData
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
request.send([...formData.entries()].map(e => `${encodeURIComponent(e[0])}=${encodeURIComponent(e[1])}`).join('&'));
});
};
2 changes: 2 additions & 0 deletions public/static/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
12 changes: 12 additions & 0 deletions public/views/confirm-register.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{%- layout 'layout', title: 'Confirm Registration' -%}
{%- if email and token -%}
{%- capture url %}./api/register/{{ token }}{% endcapture -%}
{%- assign method = 'POST' -%}
{%- assign formId = 'form' -%}
<form id="{{ formId }}" action="{{ url }}" method="{{ method }}">
<input type="hidden" name="email" value="{{ email }}" required>
{% include 'form-ajaxify', buttonText: 'Confirm Registration!' %}
</form>
{%- else -%}
<h2>Invalid Arguments!</h2>
{%- endif -%}
8 changes: 8 additions & 0 deletions public/views/register.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{%- layout 'layout', title: 'Register for the Dice-Server' -%}
{%- assign url ='./api/register' -%}
{%- assign method = 'POST' -%}
{%- assign formId = 'form' -%}
<form id="{{ formId }}" action="{{ url }}" method="{{ method }}">
<input type="email" name="email" placeholder="Insert your email adress" required>
{% include 'form-ajaxify', buttonText: 'Register!', formId: 'form' %}
</form>
9 changes: 9 additions & 0 deletions public/views/unregister.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{%- layout 'layout', title: 'Unregister from the dice Server' -%}
{%- assign url ='./api/unregister' -%}
{%- assign method = 'POST' -%}
{%- assign formId = 'form' -%}

<form id="{{ formId }}" action="{{ url }}" method="{{ method }}">
<input type="email" name="email" placeholder="Insert your email adress" value="{{ email }}" required>
{% include 'form-ajaxify', buttonText: 'Unregister!' %}
</form>
15 changes: 15 additions & 0 deletions public/views/verify.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{%- layout 'layout', title: 'Verify your dice' %}
{%- if invalid -%}
<h2>Invalid link!</h2>
{%- elsif token and dice and date -%}
{%- capture url %}./api/verify/{{ token }}{% endcapture -%}
{%- assign method = 'GET' -%}
{%- assign formId = 'form' -%}
<form id="{{ formId }}" action="{{ url }}" method="{{ method }}">
<p>Dice Rolled: {{ dice | join: ", " }}</p>
<p>At {{ date | date: "%a, %b %d, %y" }}</p>
{% include 'form-ajaxify', buttonText: 'Verify!' %}
</form>
{%- else -%}
<h2>Malformed JSON</h2>
{%- endif -%}
20 changes: 15 additions & 5 deletions src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class Api {
this.validator = new Validator();
}

static isSingleEmail(email) {
return !/[,\s<>]/.test(email);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There appear to be a few modules out there that validate email address syntax according to the RFC. Would it be more appropriate to sanity check that the address syntax is truly valid before we attempt to send an email?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah there's some controversy around this topic.
There are some super long regexes that are supposed to validate an email correctly, which are almost never used because there are shorter regexes that are supposed to be around 99.9% accurate.
Then again, there are some people that have the opinion that only sending out an email will truly validate that email + will keep up with potentially changing standards.
That's why I chose the simplest route.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found this site: https://emailregex.com
Will use the proposed JavaScript regex

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have clarified my concern better. I was thinking of the non-malicious case where a user simply entered a malformed email address and had to wait for us to actually try to send the email before receiving an error message indicating the address is invalid. I forgot to check the client side, and now see that you're using an email input element on the form. That should handle client validation sufficiently to address the scenario I had in my mind. 👍

But a better regex is still an improvement for our (future) REST API users. 😄

}

async registrationMiddleware(req, res, next) {
const errors = [];
await Promise.all([req.body.email1, req.body.email2].map(email => (
Expand Down Expand Up @@ -124,13 +128,13 @@ class Api {
}

async handleEmailRegisterConfirm(req, res) {
const verified = await this.emailManager.verifyEmail(req.params.email, req.params.token);
const verified = await this.emailManager.verifyEmail(req.body.email, req.params.token);
if (verified) {
res.status(200).json({ status: 'OK' });
} else {
res.status(403).json({
status: 'Error',
errors: 'invalid Token or mail.',
errors: ['Invalid Token or E-Mail.'],
});
}
}
Expand All @@ -149,7 +153,14 @@ class Api {

static verifyEmailParam(req, res, next) {
if (typeof req.body.email === 'string') {
next();
if (Api.isSingleEmail(req.body.email)) {
next();
} else {
res.status(422).json({
status: 'Error',
errors: ['Email has invalid format'],
});
}
} else {
res.status(422).json({
status: 'Error',
Expand All @@ -164,8 +175,7 @@ module.exports = (router, database) => {
router.get('/verify/:token', Api.validateVerifyArgs, api.handleVerify.bind(api));
router.post('/roll', api.registrationMiddleware.bind(api), Api.validateRollArgs, api.handleRoll.bind(api));
router.post('/register', Api.verifyEmailParam, api.handleEmailRegister.bind(api));
// TODO replace with frontend and merge with middleware above
router.get('/register/:email/:token', api.handleEmailRegisterConfirm.bind(api));
router.post('/register/:token', api.handleEmailRegisterConfirm.bind(api));
router.post('/unregister', Api.verifyEmailParam, api.handleEmailUnregister.bind(api));

// express.js behaves differently if no next parameter is used here
Expand Down
52 changes: 33 additions & 19 deletions src/api/email-manager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const nodemailer = require('nodemailer');
const crypto = require('crypto');
const Liquid = require('liquidjs');
const path = require('path');
const TokenCache = require('../util/token-cache.js');

const getServerBaseUrl = ({
Expand All @@ -16,6 +18,10 @@ class EmailManager {
this.transport = nodemailer.createTransport(transport);
this.server = server;
this.emailsender = emailsender;
this.engine = Liquid({
root: path.resolve(__dirname, '../../public/email-templates/'),
extname: '.html',
});
}

async verifyEmail(email, token) {
Expand All @@ -30,46 +36,54 @@ class EmailManager {
if (await this.dbhandler.checkMail(email)) {
return false;
}
// TODO replace with frontend
const token = crypto.randomBytes(512).toString('base64');
this.emailMap.put(email, token);
const url = `${getServerBaseUrl(this.server)}/api/register/${email}/${encodeURIComponent(token)}`;

const subject = 'Verify your E-Mail';
const baseUrl = getServerBaseUrl(this.server);
const encodedEmail = encodeURIComponent(email);
const content = await this.engine.renderFile('verify-email.html', {
subject,
url: `${baseUrl}/register?email=${encodedEmail}&token=${encodeURIComponent(token)}`,
host: this.server.host,
unsub: `${baseUrl}/unregister?email=${encodedEmail}`,
});

return this.transport.sendMail({
from: this.emailsender,
// FIXME email should be escaped
to: email,
subject: 'Confirm your email', // TODO use proper templating engine
html: `Please click this link to confirm your email adress: <a href="${url}">Confirm!</a>
<br>
It will expire after 24 hours or when a new confirmation email is sent.`,
subject,
html: content,
});
}

unregisterEmail(email) {
return this.dbhandler.removeUser(email);
}

sendDiceVerificationEmail(email1, email2, dice, signature, date) {
// TODO replace with frontend
async sendDiceVerificationEmail(email1, email2, dice, signature, date) {
const properties = {
dice,
signature,
date,
};
const subject = 'The dice have been cast!';
const encodedProperties = encodeURIComponent(Buffer.from(JSON.stringify(properties)).toString('base64'));
const url = `${getServerBaseUrl(this.server)}/api/verify/${encodedProperties}`;
const baseUrl = getServerBaseUrl(this.server);

const content = await this.engine.renderFile('verify-dice.html', {
subject,
date: new Date(date).toLocaleString('en-US'),
dice: JSON.stringify(dice),
url: `${baseUrl}/verify?token=${encodedProperties}`,
unsub: `${baseUrl}/unregister`,
});

return this.transport.sendMail({
from: this.emailsender,
// FIXME emails should be escaped
to: `${email1}, ${email2}`,
subject: 'Dice were rolled', // TODO use proper templating engine
html: `The dice have been cast!
<br>
Date: ${new Date(date).toLocaleString('en-US')}
<br>
Results: ${JSON.stringify(dice)}
<br>
<a href="${url}">Verify the validity of this message!</a>`,
subject,
html: content,
});
}
}
Expand Down
Loading