We have email verification

This commit is contained in:
Thomas Forgione 2017-09-23 16:20:56 +00:00
parent fa105f855a
commit dae9643cd0
No known key found for this signature in database
GPG Key ID: 95D964F74A96119E
21 changed files with 446 additions and 59 deletions

View File

@ -1,10 +1,14 @@
const model = require('model'); const model = require('model');
var user = new model.BaseModel("auth_user"); var user = new model.BaseModel("authUser");
user.addField(new model.SerialField("id")); user.addField(new model.SerialField("id"));
user.addField(new model.TextField("email")); user.addField(new model.TextField("username", {unique: true, notNull: true}));
user.addField(new model.PasswordField("password")); user.addField(new model.TextField("email", {unique: true, notNull: true}));
user.addField(new model.BoolField("active", {notNull: true, default: false}));
user.addField(new model.TextField("activationKey"));
user.addField(new model.PasswordField("password", {notNull: true}));
user.addField(new model.SmallIntegerField("resources")); user.addField(new model.SmallIntegerField("resources"));
user.addField(new model.SmallIntegerField("project_id")); user.addField(new model.SmallIntegerField("projectId"));
module.exports = model.createClass(user); module.exports = model.createClass(user);

View File

@ -0,0 +1,24 @@
{% extends "pyade/base.html" %}
{% block content %}
{% if login_failed %}
<div class="alert">Username or password incorrect. Please try again.</div>
{% endif %}
<div class="row">
<div class="col"></div>
<div class="col">
<form method="POST" action="{% url "login_target" %}">
<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="Username" autofocus>
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="Password">
</div>
<div class="form-group">
<input class="btn btn-primary form-control" type="submit" value="Log in">
</div>
{% csrf_token %}
</form>
</div>
<div class="col"></div>
</div>
{% endblock %}

View File

@ -1,4 +1,17 @@
extends ../../../templates/base.pug extends ../../../templates/base.pug
block content block content
p Login if loginFailed
.alert
| Username or password incorrect. Please try again.
.row
.col
.col
form(method="POST", action=getUrl("loginTarget"))
.form-group
input.form-control(type="text", name="username", placeholder="Username", autofocus)
.form-group
input.form-control(type="password", name="password", placeholder="Password")
.form-group
input.btn.btn-primary.form-control(type="submit", value="Log in")
.col

View File

@ -0,0 +1,9 @@
{% extends "pyade/base.html" %}
{% block content %}
<h1>
A mail was sent to you.
</h1>
<p>
In this mail, you'll find a link to activate your account.
</p>
{% endblock %}

View File

@ -0,0 +1,8 @@
extends ../../../templates/base.pug
block content
h1 A mail was sent to you
p.
In this mail, you'll find a link to activate your account.
p.
Please, don't forget that my server is cheap, so depending on your mail provider, my email might get thrown in the spam :'(

View File

@ -0,0 +1,38 @@
extends ../../../templates/base.pug
block content
if registeringFailed
.alert There was an error.
.row
.col
.col
form(method="POST", action=getUrl("signupTarget"))
.form-group
input.form-control(type='text', name='username', placeholder='Username', autofocus='')
.form-group
input#email.form-control(type='email', name='email', placeholder='E-mail address')
.form-group
input#pass1.form-control(type='password', name='password', placeholder='Password')
.form-group
input#pass2.form-control(type='password', placeholder='Retype your password')
.form-group
input.btn.btn-primary.form-control(type='submit', value='Log in')
.col
block extrajs
script.
var mail = document.getElementById('email'),
pass = document.getElementById('pass1'),
pass2 = document.getElementById('pass2');
function validatePasswords() {
if (pass1.value === pass2.value) {
pass2.setCustomValidity('');
} else {
pass2.setCustomValidity("Passwords don't match");
}
}
pass1.addEventListener('change', validatePasswords);
pass2.addEventListener('keyup', validatePasswords);

View File

@ -0,0 +1,50 @@
style.
.body {
width: 570px;
padding: 0px;
margin-right: auto;
margin-left: auto;
border: solid;
border-width: thin;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.header {
padding: 10px;
border-bottom: solid;
border-bottom-width: thin;
}
.footer {
font-size: 75%;
padding: 10px;
border-top: solid;
border-top-width: thin;
}
h1 {
text-align: center;
}
p {
padding: 10px;
margin: 0px;
}
.username {
font-style: italic;
}
.body
.header
h1 Welcome on ADEjs!
p.
Hello #{user.username}!
p.
You recently subscribed on ADEjs with the email <a href="mailto:#{user.email}">#{user.email}</a>.
p.
To finalize your subscription, please <a href="#{activationUrl}">click here to activate your account</a>.
.footer.
If you did not subscribe on PyADE with this email address, please ignore this e-mail.

View File

@ -0,0 +1,10 @@
Hello #{user.username}, and welcome on ADEjs!
You recently subscribed on ADEjs with the email <a href="mailto:#{user.email}">#{user.email}</a>.
To finalize your subscription, please <a href="#{activationUrl}">click here to activate your account</a>.
See you later on ADEjs!
---
If you did not subscribe on ADEjs with this email address, please ignore this e-mail.

View File

@ -2,4 +2,10 @@ const url = require('create-url').url;
module.exports = [ module.exports = [
url('/login', 'login', 'login'), url('/login', 'login', 'login'),
url('/logout', 'logout', 'logout'),
url('/login-target', 'loginTarget', 'loginTarget', 'POST'),
url('/signup', 'signup', 'signup'),
url('/signup-target', 'signupTarget', 'signupTarget', 'POST'),
url('/mail-was-sent', 'mailWasSent', 'mailWasSent'),
url('/activate/:activationKey', 'activate', 'activate'),
] ]

View File

@ -1,10 +1,95 @@
const testPassword = require('model').PasswordField.testSync;
const User = require('./models.js'); const User = require('./models.js');
const getUrl = require('create-url').getUrl; const getUrl = require('create-url').getUrl;
const mail = require('mail');
const pug = require('pug');
const config = require('settings/config');
module.exports.login = function(req, res, render) { module.exports.login = function(req, res, render) {
render('login.pug'); render('login.pug');
} }
module.exports.signup = function(req, res, render) {
render('signup.pug');
}
module.exports.logout = function(req, res, render) { module.exports.logout = function(req, res, render) {
req.session.user = undefined;
req.session.save();
res.redirect(getUrl("index")); res.redirect(getUrl("index"));
} }
module.exports.loginTarget = function(req, res, render) {
User.getByUsername(req.body.username, (err, user) => {
if (user === undefined || !user.active) {
res.redirect(getUrl('login'));
} else {
if (testPassword(req.body.password, user.password)) {
req.session.user = user;
req.session.save();
res.redirect(getUrl('index'));
} else {
res.redirect(getUrl('login'));
}
}
});
}
module.exports.signupTarget = function(req, res, render) {
let user = new User();
user.username = req.body.username;
user.email = req.body.email;
user.password = req.body.password;
require('crypto').randomBytes(48, function(err, buffer) {
user.activationKey = buffer.toString('hex');
user.save((err) => {
res.locals.user = user;
let baseUrl =
req.headers.referer.split('/').slice(0, 3).join('/') + '/'
res.locals.activationUrl =
baseUrl + 'activate/' + user.activationKey;
let html = pug.renderFile(
__dirname + '/templates/validation-mail.pug',
res.locals,
);
mail({
from: config.MAIL.FROM,
to: user.username + ' <' + user.email + '>',
subject: 'Welcome on ADEjs!',
text:'',
attachment: [
{data: html, alternative: true}
]
}, (err, result) => {
res.redirect(getUrl('mailWasSent'));
});
});
});
}
module.exports.mailWasSent = function(req, res, render) {
render('mailWasSent.pug');
}
module.exports.activate = function(req, res, render, next) {
User.getByActivationKey(req.params.activationKey, (err, user) => {
if (user === undefined) {
return next();
}
user.active = true;
user.activationKey = undefined;
user.save((err, user) => {
req.session.user = user;
req.session.save();
res.redirect(getUrl('index'));
});
});
}

View File

@ -3,7 +3,6 @@ require('app-module-path').addPath(config.BASE_DIR);
require('app-module-path').addPath(config.UTILS_DIR); require('app-module-path').addPath(config.UTILS_DIR);
require('app-module-path').addPath(config.CONTROLLERS_DIR); require('app-module-path').addPath(config.CONTROLLERS_DIR);
const repl = require('repl');
const express = require('express'); const express = require('express');
const pug = require('pug'); const pug = require('pug');
const http = require('http'); const http = require('http');
@ -19,7 +18,10 @@ function startServer() {
let app = express(); let app = express();
let http = require('http').Server(app); let http = require('http').Server(app);
let bodyParser = require('body-parser'); var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
let session = require('cookie-session'); let session = require('cookie-session');
let cookieParser = require('cookie-parser'); let cookieParser = require('cookie-parser');
@ -52,7 +54,7 @@ function startServer() {
app.use(function(req, res) { app.use(function(req, res) {
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');
res.render('404.pug', res.locals, function(err, result) { res.render(config.BASE_DIR + '/templates/404.pug', res.locals, function(err, result) {
if (err) if (err)
console.log(err); console.log(err);
res.send(result); res.send(result);
@ -77,7 +79,6 @@ function startServer() {
const commands = { const commands = {
runserver: startServer, runserver: startServer,
reinitdb: model.reinitialize, reinitdb: model.reinitialize,
shell: repl.start,
} }
function showHelp() { function showHelp() {

View File

@ -14,10 +14,12 @@
"author": "Thomas Forgione", "author": "Thomas Forgione",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"app-module-path": "^2.2.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.18.2", "body-parser": "^1.18.2",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"cookie-session": "^1.3.1", "cookie-session": "^1.3.1",
"emailjs": "^1.0.12",
"express": "^4.15.4", "express": "^4.15.4",
"pg": "^7.3.0", "pg": "^7.3.0",
"pug": "^2.0.0-rc.4", "pug": "^2.0.0-rc.4",

15
scripts/clear-db.js Normal file
View File

@ -0,0 +1,15 @@
const config = require('../settings/config.js');
require('app-module-path').addPath(config.BASE_DIR);
require('app-module-path').addPath(config.UTILS_DIR);
require('app-module-path').addPath(config.CONTROLLERS_DIR);
const model = require('model');
const User = require('auth/models');
const log = require('log');
model.reinitialize(() => {
log.debug('Database reinitialized');
process.exit(0);
});

View File

@ -6,5 +6,13 @@ module.exports = {
'USERNAME': 'adejs', 'USERNAME': 'adejs',
'DBNAME': 'adejs', 'DBNAME': 'adejs',
'PASSWORD':'97Cnqw023V1G95fQUJR8H7gwvqUke4', 'PASSWORD':'97Cnqw023V1G95fQUJR8H7gwvqUke4',
},
'MAIL': {
'USERNAME': 'adejs',
'PASSWORD': 'ILm0iSIgYI18w41dJU3ll9Pe5w9qjJ',
'HOSTNAME': 'smtp.tforgione.fr',
'PORT': '587',
'FROM': 'ADEjs <adejs@tforgione.fr>',
'TLS': true,
} }
}; };

6
templates/404.pug Normal file
View File

@ -0,0 +1,6 @@
extends ./base.pug
block content
h1 Error 404
p.
This page does not exist.

View File

@ -30,12 +30,12 @@ html
a.btn.btn-light(href=getUrl("login"), role='button') a.btn.btn-light(href=getUrl("login"), role='button')
| Sign in | Sign in
li.nav-item li.nav-item
a.btn.btn-success(href='#', role='button') a.btn.btn-success(href=getUrl("signup"), role='button')
| Sign up | Sign up
else else
li.nav-item li.nav-item
a.nav-link(href='#') a.nav-link(href='#')
| Username | #{session.user._username.value}
li.nav-item li.nav-item
a.btn.btn-light(href=getUrl("logout"), role='button') a.btn.btn-light(href=getUrl("logout"), role='button')
| Log out | Log out

View File

@ -12,16 +12,29 @@ model.reinitialize(() => {
log.debug('Database reinitialized'); log.debug('Database reinitialized');
let user = new User(); let user = new User();
user.email = "toto"; user.username = "toto"
user.email = "toto@tforgione.fr";
user.password = "tata"; user.password = "tata";
user.resources = 23; user.resources = 23;
user.project_id = 42; user.project_id = 42;
user.save((err, u) => { user.save((err, u) => {
if (err) {
log.dberror(err);
return;
}
log.debug('New user created'); log.debug('New user created');
// Test password // Test password
User.getById(1, (err, user) => { User.getById(1, (err, user) => {
if (err !== undefined) {
log.dberror("User is undefined");
console.log(err);
return;
}
if (model.PasswordField.testSync("tata", user.password)) { if (model.PasswordField.testSync("tata", user.password)) {
log.debug("Password authentication succeed"); log.debug("Password authentication succeed");
} else { } else {

View File

@ -22,7 +22,7 @@ module.exports = function(app, controllersDir = __dirname + '/../controllers') {
for (let url of urls) { for (let url of urls) {
app.get(url.url, ((url) => function(req, res, next) { app[url.method.toLowerCase()](url.url, ((url) => function(req, res, next) {
let path = obj[url.view](req, res, function(template) { let path = obj[url.view](req, res, function(template) {

View File

@ -1,10 +1,11 @@
let urls = {}; let urls = {};
class Url { class Url {
constructor(url, view, name) { constructor(url, view, name, method="GET") {
this.url = url; this.url = url;
this.view = view; this.view = view;
this.name = name; this.name = name;
this.method = method;
} }
} }
@ -19,8 +20,8 @@ module.exports = function(req, res, next) {
module.exports.getUrl = getUrl; module.exports.getUrl = getUrl;
module.exports.url = function(url, viewName, urlName) { module.exports.url = function(url, viewName, urlName, method) {
let ret = new Url(url, viewName, urlName); let ret = new Url(url, viewName, urlName, method);
urls[urlName] = ret; urls[urlName] = ret;
return ret; return ret;
} }

14
utils/mail.js Normal file
View File

@ -0,0 +1,14 @@
const config = require('settings/config');
const mail = require('emailjs');
module.exports = function(params, callback) {
let server = mail.server.connect({
user: config.MAIL.USERNAME,
password: config.MAIL.PASSWORD,
host: config.MAIL.HOSTNAME,
port: config.MAIL.PORT,
tls: config.MAIL.TLS
});
server.send(params, callback);
};

View File

@ -5,6 +5,40 @@ const bc = require('bcryptjs');
let models = []; let models = [];
function postgresToCamel(postgres) {
let ret = '';
for (let i = 0; i < postgres.length; i++) {
let c = postgres.charAt(i);
if (c === '_') {
i++;
ret += postgres.charAt(i).toUpperCase();
} else {
ret += c;
}
}
return ret;
}
function camelToPostgres(camelCase) {
let ret = '';
for (let i = 0; i < camelCase.length; i++) {
let c = camelCase.charAt(i);
if (c === c.toUpperCase()) {
ret += '_' + c.toLowerCase()
} else {
ret += c;
}
}
return ret;
}
function attributeToGetBy(attributeName) {
return 'getBy' +
attributeName.charAt(0).toUpperCase() + attributeName.substr(1);
}
module.exports.BaseModel = class { module.exports.BaseModel = class {
constructor(name) { constructor(name) {
models.push(this); models.push(this);
@ -19,13 +53,17 @@ module.exports.BaseModel = class {
reinitialize(callback = () => {}) { reinitialize(callback = () => {}) {
log.debug('Reinitializing ' + this.name); log.debug('Reinitializing ' + this.name);
db.query('DROP TABLE IF EXISTS ' + this.name + ' CASCADE') let query =
"DROP TABLE IF EXISTS " +
camelToPostgres(this.name) + " CASCADE;";
db.query(query)
.then(res => this.createTable(callback)) .then(res => this.createTable(callback))
.catch(err => log.dberror(err)); .catch(err => log.dberror(err));
} }
createTable(callback) { createTable(callback) {
let query = 'CREATE TABLE ' + this.name + '(\n'; let query = 'CREATE TABLE ' + camelToPostgres(this.name) + '(\n';
query += this.fields query += this.fields
.map(f => '\t' + f.getCreationString()) .map(f => '\t' + f.getCreationString())
.join(',\n'); .join(',\n');
@ -35,7 +73,7 @@ module.exports.BaseModel = class {
db.query(query) db.query(query)
.then(res => { .then(res => {
log.debug("Table " + this.name + " created") log.debug("Table " + camelToPostgres(this.name) + " created")
callback(); callback();
}) })
.catch(err => log.dberror(err)); .catch(err => log.dberror(err));
@ -43,8 +81,30 @@ module.exports.BaseModel = class {
} }
module.exports.BaseField = class { module.exports.BaseField = class {
constructor(name) { constructor(name, properties = {}) {
this.name = name; this.name = name;
this.unique = properties.unique || false;
this.notNull = properties.notNull || false;
this.default = properties.default;
}
getCreationString() {
return [
camelToPostgres(this.name),
this.getPostgresType(),
this.getFormattedPropeties(),
].join(' ');
}
getFormattedPropeties() {
let properties = [];
if (this.unique) {
properties.push('UNIQUE');
}
if (this.notNull) {
properties.push('NOT NULL');
}
return properties.join(' ');
} }
createMutators() { createMutators() {
@ -73,43 +133,43 @@ module.exports.BaseField = class {
} }
module.exports.SmallIntegerField = class extends module.exports.BaseField { module.exports.SmallIntegerField = class extends module.exports.BaseField {
constructor(name) { constructor(name, properties = {}) {
super(name); super(name, properties);
} }
getCreationString() { getPostgresType() {
return this.name + " SMALLINT"; return "SMALLINT";
} }
} }
module.exports.SerialField = class extends module.exports.BaseField { module.exports.SerialField = class extends module.exports.BaseField {
constructor(name) { constructor(name, properties = {}) {
super(name); super(name, properties);
this.unique = true;
} }
getCreationString() { getPostgresType() {
return this.name + " SERIAL"; return "SERIAL";
} }
} }
module.exports.TextField = class extends module.exports.BaseField { module.exports.TextField = class extends module.exports.BaseField {
constructor(name) { constructor(name, properties = {}) {
super(name); super(name, properties);
} }
getCreationString() { getPostgresType() {
return this.name + " TEXT"; return "TEXT";
} }
} }
module.exports.PasswordField = class extends module.exports.BaseField { module.exports.PasswordField = class extends module.exports.BaseField {
constructor(name) { constructor(name, properties = {}) {
super(name); super(name, properties);
} }
getCreationString() { getPostgresType() {
return this.name + ' TEXT'; return 'TEXT';
} }
@ -121,7 +181,6 @@ module.exports.PasswordField = class extends module.exports.BaseField {
changed: true, changed: true,
}; };
} }
} }
} }
@ -130,36 +189,57 @@ module.exports.PasswordField.testSync = function(password, hash) {
} }
module.exports.BoolField = class extends module.exports.BaseField {
constructor(name, properties) {
super(name, properties);
}
getPostgresType() {
return 'BOOLEAN';
}
}
module.exports.createClass = function(model) { module.exports.createClass = function(model) {
let ret = function(param) { let ret = function(param) {
this._persisted = false; this._persisted = false;
for (let field of model.fields) { for (let field of model.fields) {
this['_' + field.name] = { this['_' + field.name] = {
value: undefined, value: field.default,
changed: false, changed: true,
} }
Object.defineProperty(this, field.name, field.createMutators()); Object.defineProperty(this, field.name, field.createMutators());
} }
}; };
ret.getById = function(id, callback) { for (let field of model.fields) {
db if (!field.unique && field.name !== 'activationKey')
.query('SELECT * FROM ' + model.name + ' WHERE id=$1;', [id]) continue;
.then(res => {
// Create the instance ret[attributeToGetBy(field.name)] = function(value, callback) {
let instance = new ret(); let query =
instance._persisted = true; 'SELECT * FROM ' + camelToPostgres(model.name) +
instance._setFromDbResult(res); ' WHERE ' + camelToPostgres(field.name) +
callback(undefined, instance); '=$1;';
})
.catch(err => callback(err)); db
.query(query, [value])
.then(res => {
// Create the instance
let instance = new ret();
instance._persisted = true;
instance._setFromDbResult(res);
callback(undefined, instance);
})
.catch(err => callback(err));
}
} }
ret.prototype._setFromDbResult = function(res) { ret.prototype._setFromDbResult = function(res) {
for (let field of model.fields) { for (let field of model.fields) {
this['_' + field.name] = { this['_' + field.name] = {
value: res.rows[0][field.name], value: res.rows[0][camelToPostgres(field.name)],
changed: false, changed: false,
}; };
} }
@ -182,10 +262,10 @@ module.exports.createClass = function(model) {
if (!this._persisted) { if (!this._persisted) {
// INSERT INTO // INSERT INTO
let queryBegin = 'INSERT INTO ' + model.name + '(' let queryBegin = 'INSERT INTO ' + camelToPostgres(model.name) + '('
let queryEnd = ' VALUES ('; let queryEnd = ' VALUES (';
queryBegin += fieldsToSave queryBegin += fieldsToSave
.map(f => f.field.name) .map(f => camelToPostgres(f.field.name))
.join(','); .join(',');
queryEnd += fieldsToSave queryEnd += fieldsToSave
@ -208,16 +288,16 @@ module.exports.createClass = function(model) {
} else { } else {
// UPDATE FROM // UPDATE FROM
let query = 'UPDATE FROM ' + model.name + ' SET '; let query = 'UPDATE ' + camelToPostgres(model.name) + ' SET ';
query += fieldsToSave.map(f => { query += fieldsToSave.map((f, i) => {
return f.field.name + '=' + f.value; return camelToPostgres(f.field.name) + '=$' + (i+1);
}).join(','); }).join(',');
query += ') RETURNING *;' query += ' WHERE id=$' + (fieldsToSave.length + 1) + ' RETURNING *;'
db db
.query(query, fieldsToSave.map(f => f.value)) .query(query, [...fieldsToSave.map(f => f.value), this.id])
.then(res => { .then(res => {
this._setFromDbResult(res); this._setFromDbResult(res);
this._persisted = true; this._persisted = true;