Merge pull request #31 from AlexisNP/evol/verification

User verification system
This commit is contained in:
Joururi
2020-12-27 19:13:19 +01:00
committed by GitHub
10 changed files with 341 additions and 49 deletions

View File

@@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `user` (
`password` VARCHAR(255) NOT NULL, `password` VARCHAR(255) NOT NULL,
`role_id` INT UNSIGNED NOT NULL DEFAULT 1, `role_id` INT UNSIGNED NOT NULL DEFAULT 1,
`verified` BOOLEAN DEFAULT false, `verified` BOOLEAN DEFAULT false,
`verification_token` VARCHAR(255),
`banned` BOOLEAN DEFAULT false, `banned` BOOLEAN DEFAULT false,
PRIMARY KEY(`id`), PRIMARY KEY(`id`),
FOREIGN KEY(`role_id`) REFERENCES role(`id`) FOREIGN KEY(`role_id`) REFERENCES role(`id`)

View File

@@ -33,13 +33,13 @@ app.use(helmet());
// Server // Server
app.listen(port, () => console.log(`App listening on port ${port}`)); app.listen(port, () => console.log(`App listening on port ${port}`));
// Auth guard // TEMP Auth guard
const authguard = (req, res, next) => { const authguard = (req, res, next) => {
if (req.headers.auracle_key !== process.env.API_KEY_PUBLIC) { // if (req.headers.auracle_key !== process.env.API_KEY_PUBLIC) {
return res.status(401).send('The API key is either missing or incorrect.'); // return res.status(401).send('The API key is either missing or incorrect.');
} else { // } else {
next(); next();
} // }
} }
// Routing // Routing

40
api/package-lock.json generated
View File

@@ -856,6 +856,25 @@
"which": "^1.2.14" "which": "^1.2.14"
} }
}, },
"handlebars": {
"version": "4.7.6",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz",
"integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==",
"requires": {
"minimist": "^1.2.5",
"neo-async": "^2.6.0",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4",
"wordwrap": "^1.0.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"has-unicode": { "has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -1564,6 +1583,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
}, },
"neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"nocache": { "nocache": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz",
@@ -1591,6 +1615,11 @@
"tar": "^4.4.2" "tar": "^4.4.2"
} }
}, },
"nodemailer": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.17.tgz",
"integrity": "sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ=="
},
"nopt": { "nopt": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
@@ -2303,6 +2332,12 @@
"mime-types": "~2.1.24" "mime-types": "~2.1.24"
} }
}, },
"uglify-js": {
"version": "3.12.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.3.tgz",
"integrity": "sha512-feZzR+kIcSVuLi3s/0x0b2Tx4Iokwqt+8PJM7yRHKuldg4MLdam4TCFeICv+lgDtuYiCtdmrtIP+uN9LWvDasw==",
"optional": true
},
"unc-path-regex": { "unc-path-regex": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
@@ -2414,6 +2449,11 @@
"string-width": "^1.0.2 || 2" "string-width": "^1.0.2 || 2"
} }
}, },
"wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
},
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -32,12 +32,14 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"handlebars": "^4.7.6",
"helmet": "^3.22.0", "helmet": "^3.22.0",
"jsonschema": "^1.2.6", "jsonschema": "^1.2.6",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"knex": "^0.21.1", "knex": "^0.21.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"nodemailer": "^6.4.17",
"uuid": "^8.2.0" "uuid": "^8.2.0"
} }
} }

View File

@@ -7,6 +7,9 @@ const model = require('../models/user-model')
const bcrypt = require('bcrypt') const bcrypt = require('bcrypt')
const { v4: uuidv4 } = require('uuid') const { v4: uuidv4 } = require('uuid')
// Mailing methods
const mails = require('../smtp/mails')
// Model validation // Model validation
const Validator = require('jsonschema').Validator const Validator = require('jsonschema').Validator
const v = new Validator() const v = new Validator()
@@ -92,7 +95,9 @@ class UserRepository {
}) })
} else { } else {
let hash = await bcrypt.hash(u.password, 10) let hash = await bcrypt.hash(u.password, 10)
let uuid = uuidv4() let uuid = uuidv4()
let verification_token = uuidv4();
this.checkIfEmailAvailable(u.mail) this.checkIfEmailAvailable(u.mail)
.then(() => { .then(() => {
@@ -102,6 +107,7 @@ class UserRepository {
'name': u.name, 'name': u.name,
'mail': u.mail, 'mail': u.mail,
'password': hash, 'password': hash,
'verification_token': verification_token,
}) })
.save(null, { .save(null, {
transacting: t transacting: t
@@ -114,6 +120,17 @@ class UserRepository {
return this.getOneByUUID(uuid, false) return this.getOneByUUID(uuid, false)
}) })
.then(newUser => { .then(newUser => {
// Send a mail to the new user's email with a link to verification url
mails.sendRegistrationMail({
user: {
name: newUser.name,
mail: newUser.mail,
token: verification_token,
}
})
// Then resolves the api call
resolve({ resolve({
"message": `Compte utilisateur #${newUser.id} créé avec succès.`, "message": `Compte utilisateur #${newUser.id} créé avec succès.`,
"code": 201, "code": 201,
@@ -134,6 +151,37 @@ class UserRepository {
}) })
} }
verifyUser(token) {
return new Promise((resolve, reject) => {
model.forge()
.where({ 'verification_token' : token })
.fetch()
.then(v => {
bookshelf.transaction(t => {
return v.save({
'verification_token': null,
'verified': 1,
}, {
method: 'update',
transacting: t
})
})
.then(v => {
resolve({
"message": "Insérez ici une future redirection vers le client.",
"code": 202,
})
})
})
.catch(() => {
reject({
"message": "Le lien de vérification ne semble pas correct.",
"code": 404
})
})
});
}
// Log user with an email address and a password // Log user with an email address and a password
logUser(mail, password) { logUser(mail, password) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -85,29 +85,6 @@ router.get('/available/:mail/', async (req, res) => {
}) })
// LOG A USER ------------------
const logUser = (mail, password) => {
return Users.logUser(mail, password)
.catch(err => {
throw err
})
}
router.post('/login', async (req, res) => {
logUser(req.body.mail, req.body.password)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(JSON.stringify(v))
})
.catch(err => {
res.status(err.code).send(JSON.stringify(
{
"error": err.message,
"code": err.code
})
)
})
})
// CREATE ONE ------------------ // CREATE ONE ------------------
const addUser = (u) => { const addUser = (u) => {
return Users.addOne(u) return Users.addOne(u)
@@ -131,4 +108,51 @@ router.post('/', async (req, res) => {
}) })
}) })
// VERIFY A USER
const verifyUser = (token) => {
return Users.verifyUser(token)
.catch(err => {
throw err;
})
}
router.get('/verification/:token', async (req, res) => {
verifyUser(req.params.token)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.send(JSON.stringify(v))
})
.catch(err => {
res.status(err.code).send(JSON.stringify(
{
"error": err.message,
"code": err.code
})
)
})
});
// LOG A USER ------------------
const logUser = (mail, password) => {
return Users.logUser(mail, password)
.catch(err => {
throw err
})
}
router.post('/login', async (req, res) => {
logUser(req.body.mail, req.body.password)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(JSON.stringify(v))
})
.catch(err => {
res.status(err.code).send(JSON.stringify(
{
"error": err.message,
"code": err.code
})
)
})
})
module.exports = router module.exports = router

14
api/smtp/config.js Normal file
View File

@@ -0,0 +1,14 @@
const nodemailer = require('nodemailer');
const transport = nodemailer.createTransport({
host: "smtp.mailtrap.io",
port: 2525,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
}
});
module.exports = {
transport,
};

61
api/smtp/mails.js Normal file
View File

@@ -0,0 +1,61 @@
const smtp = require('./config');
const fs = require('fs');
const handlebars = require('handlebars');
// Sender address for mail service
const sender = 'tymos@auracle.io';
// Fetches a HTML template file for parsing
const getTemplateFile = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, {encoding: 'utf-8'}, (err, html) => {
if (err) {
reject(err);
} else {
resolve(html);
}
})
})
};
/**
* SEND REGISTRATION MAIL FUNCTION
* @param {Object} data
* - user
* - name
* - mail
* - token
*/
const sendRegistrationMail = (data) => {
getTemplateFile(__dirname + '/templates/template-sign-up.html')
.then(html => {
let template = handlebars.compile(html);
let template_parsed = template(data);
let message = {
from: sender,
to: data.user.mail,
subject: 'Inscription Auracle.io',
html: template_parsed,
};
smtp.transport.sendMail(message, (err, info) => {
if (err) {
throw err;
} else {
console.log(info);
}
});
})
.catch(err => {
console.log(err);
});
}
const sendBanEmail = (date) => {
return null;
}
module.exports = {
sendRegistrationMail,
}

View File

@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html>
<head>
<title>Template Email Auracle Inscription</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Playfair+Display:wght@700;800;900&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--black: 52, 58, 64;
--white: #FFF;
--primary: 89, 158, 244;
}
body {
color: rgb(var(--black));
font-family: 'Lato', sans-serif;
max-width: 100%;
}
.display {
font-family: 'Playfair Display', serif;
}
h1 {
font-size: 32px;
}
p:not(:last-child) {
margin-bottom: 15px;
}
.italics {
font-style: italic;
}
.wrapper {
position: relative;
padding: 10vh 5vw;
background: url('https://i.imgur.com/IxEKKEY.png') center center fixed repeat;
}
.wrapper:before {
display: block;
content: '';
position: absolute;
top: 0;
height: 120px;
left: 0;
right: 0;
background-color: rgb(var(--primary));
z-index: 0;
}
.container {
position: relative;
background-color: var(--white);
z-index: 5;
}
header, footer {
text-align: center;
color: var(--white);
background-color: rgb(var(--black));
}
header {
padding: 15px;
}
main {
padding: 30px;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="container">
<header>
<h1 class="display">Bienvenue sur Auracle !</h1>
</header>
<main>
<p>Votre compte Auracle a bien été enregistré et est rattaché à cette adresse mail.</p>
<p>Cependant, afin de garantir la sécurité de vos données, votre compte doit être vérifié avant votre première connexion. Vous pouvez <a href="http://localhost:2814/api/users/verification/{{ user.token }}">suivre ce lien afin de confirmer votre inscription.</a></p>
<p>Si vous rencontrez des problèmes, n'hésitez pas à envoyer un mail à <a href="mailto:support@auracle.io">support@auracle.io</a></p>
<p><strong>Merci de votre soutien !</strong></p>
<p class="italics">Bien sincèrement, Izàc Tymos.</p>
</main>
<footer>
<div class="icon">
<!-- <img src="https://i.imgur.com/dJTklvU.png" alt=""> -->
</div>
</footer>
</div>
</div>
</body>
</html>

View File

@@ -11,7 +11,7 @@
v-model="name" v-model="name"
:class="{ :class="{
'is-invalid': errors.name, 'is-invalid': errors.name,
'is-valid': !errors.name }" 'is-valid': submitted && !errors.name }"
type="text" type="text"
id="username" id="username"
class="form-control" class="form-control"
@@ -38,7 +38,7 @@
v-model="email" v-model="email"
:class="{ :class="{
'is-invalid': errors.email, 'is-invalid': errors.email,
'is-valid': !errors.email }" 'is-valid': submitted && !errors.email }"
type="email" type="email"
id="email" id="email"
class="form-control" class="form-control"
@@ -64,7 +64,7 @@
<input <input
:class="{ :class="{
'is-invalid': errors.password, 'is-invalid': errors.password,
'is-valid': !errors.password }" 'is-valid': submitted && !errors.password }"
v-model="password" v-model="password"
type="password" type="password"
id="password" id="password"
@@ -78,7 +78,7 @@
v-model="password_check" v-model="password_check"
:class="{ :class="{
'is-invalid': errors.password_check || errors.password, 'is-invalid': errors.password_check || errors.password,
'is-valid': !errors.password_check}" 'is-valid': submitted && !errors.password_check}"
type="password" type="password"
id="password_check" id="password_check"
class="form-control" class="form-control"