Browse Source

Added spam protection to the contact form.

Signed-off-by: Dušan Mitrović <dusan@dusanmitrovic.xyz>
master
Dušan Mitrović 3 months ago
parent
commit
4735f7c9d9
Signed by: dusan GPG Key ID: 8E81D1BFCE8427E5
5 changed files with 146 additions and 5 deletions
  1. +29
    -3
      routes/contact.js
  2. +109
    -0
      utilities/form-antispam.js
  3. +4
    -1
      validations/contact-validations.js
  4. +1
    -1
      views/contact.hbs
  5. +3
    -0
      views/partials/contact-form.hbs

+ 29
- 3
routes/contact.js View File

@@ -9,8 +9,10 @@ const validations = require('../validations/contact-validations');
const { validationResult } = require('express-validator');
const capitalize = require('../utilities/capitalize');
const MailerService = require('../services/mailer-service');
const FormAntispam = require('../utilities/form-antispam');

const router = express.Router();
const formAntispam = new FormAntispam();

router.get('/', (req, res) => {
return res.render('contact', {
@@ -24,13 +26,14 @@ router.get('/', (req, res) => {
name: '',
email: '',
subject: '',
message: ''
message: '',
antispam: formAntispam.generate(),
}
});
});

router.post('/', validations.contact, async (req, res) => {
const { name, email, subject, message } = req.body;
const { name, email, subject, message, antispamQuestion, antispamAnswer } = req.body;
const errors = validationResult(req);

if (!errors.isEmpty()) {
@@ -53,7 +56,30 @@ router.post('/', validations.contact, async (req, res) => {
name,
email,
subject,
message
message,
antispam: formAntispam.generate(),
}
});
}

if (parseInt(antispamAnswer) !== formAntispam.solve(antispamQuestion)) {
return res.status(422).render('contact', {
title: 'Contact',
css: [
'/static/css/contact.css',
],
form: {
id: 'contact-form',
action: '/contact',
error: {
message: 'Wrong answer.',
},
success: {},
name,
email,
subject,
message,
antispam: formAntispam.generate(),
}
});
}


+ 109
- 0
utilities/form-antispam.js View File

@@ -0,0 +1,109 @@
/**
* @author Dusan Mitrovic <dusan@dusanmitrovic.xyz>
* @license AGPL-3.0-or-later https://opensource.org/licenses/AGPL-3.0
*
* @summary Generates and solves math questions like three + four = ?
*/
class FormAntispam {
constructor() {
this.numberMap = {
zero: 0,
one: 1,
two: 2,
three: 3,
four: 4,
five: 5,
six: 6,
seven: 7,
eight: 8,
nine: 9,
};

this.operatorMap = {
plus: '+',
minus: '-',
times: '*',
};
}

/**
* Generate a math question
*/
generate() {
const numberMapKeys = Object.keys(this.numberMap);
const operatorMapKeys = Object.keys(this.operatorMap);

const firstOperand = this.#getRandomMapValue(this.numberMap, numberMapKeys);
const secondOperand = this.#getRandomMapValue(this.numberMap, numberMapKeys);
const operator = this.#getRandomMapValue(this.operatorMap, operatorMapKeys);

switch(operator.value) {
case '+':
return `${firstOperand.key} ${operator.key} ${secondOperand.key}`;
case '*':
return `${firstOperand.key} ${operator.key} ${secondOperand.key}`;
case '-':
return firstOperand.value >= secondOperand.value ?
`${firstOperand.key} ${operator.key} ${secondOperand.key}` :
`${secondOperand.key} ${operator.key} ${firstOperand.key}`;
}
}

/**
* Solve a math question generated by generate
*
* @param {string} question
* @return {number|null}
*/
solve(question) {
const questionParts = question.split(' ');

// Something went wrong, the question must have exactly 3 parts
if (3 !== questionParts.length) {
return null;
}

const [firstOperandKey, operatorKey, secondOperandKey] = questionParts;

// Something went wrong, at least one key doesn't exist on its respective object
if (
!this.numberMap.hasOwnProperty(firstOperandKey) ||
!this.numberMap.hasOwnProperty(secondOperandKey) ||
!this.operatorMap.hasOwnProperty(operatorKey)
) {
return null;
}

const firstOperand = this.numberMap[firstOperandKey];
const secondOperand = this.numberMap[secondOperandKey];
const operator = this.operatorMap[operatorKey];

switch (operator) {
case '+':
return firstOperand + secondOperand;
case '*':
return firstOperand * secondOperand;
case '-':
return firstOperand - secondOperand;
}
}

/**
* Gets a random value from the passed maps
*
* @param {Object} map
* @param {Array<string>} mapKeys
* @return {Object}
*/
#getRandomMapValue(map, mapKeys) {
const randomMapKey = mapKeys[Math.floor(Math.random() * mapKeys.length)];

return {
key: randomMapKey,
value: map[randomMapKey],
};
}
}

module.exports = FormAntispam;


+ 4
- 1
validations/contact-validations.js View File

@@ -25,7 +25,10 @@ const validations = {
body('message')
.notEmpty()
.withMessage('is required.')
.escape()
.escape(),
body('antispamAnswer')
.isNumeric()
.withMessage('must be numeric.'),
]
};



+ 1
- 1
views/contact.hbs View File

@@ -1,3 +1,3 @@
<section id="contact-page" class="container">
{{> contact-form id=form.id action=form.action error=form.error success=form.success name=form.name email=form.email subject=form.subject message=form.message}}
{{> contact-form id=form.id action=form.action error=form.error success=form.success name=form.name email=form.email subject=form.subject message=form.message antispam=form.antispam}}
</section>

+ 3
- 0
views/partials/contact-form.hbs View File

@@ -9,5 +9,8 @@
<input name="email" class="form-input" type="email" placeholder="Your email address" value="{{ email }}" required>
<input name="subject" class="form-input" type="text" placeholder="Subject" value="{{ subject }}" required>
<textarea rows="20" cols="10" name="message" class="form-textarea" placeholder="Your message" required>{{message}}</textarea>
<label for="antispamAnswer">What is {{ antispam }}?</label>
<input name="antispamAnswer" class="form-input" type="number" required>
<input name="antispamQuestion" type="hidden" value="{{ antispam }}">
<button class="form-button" type="submit">Send</button>
</form>

Loading…
Cancel
Save