diff --git a/controllers/auth/models.js b/controllers/auth/models.js
index 3663271..c95f10b 100644
--- a/controllers/auth/models.js
+++ b/controllers/auth/models.js
@@ -1,10 +1,14 @@
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.TextField("email"));
-user.addField(new model.PasswordField("password"));
+user.addField(new model.TextField("username", {unique: true, notNull: true}));
+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("project_id"));
+user.addField(new model.SmallIntegerField("projectId"));
module.exports = model.createClass(user);
+
diff --git a/controllers/auth/templates/login-form.html b/controllers/auth/templates/login-form.html
new file mode 100644
index 0000000..556de28
--- /dev/null
+++ b/controllers/auth/templates/login-form.html
@@ -0,0 +1,24 @@
+{% extends "pyade/base.html" %}
+{% block content %}
+{% if login_failed %}
+
Username or password incorrect. Please try again.
+{% endif %}
+
+{% endblock %}
diff --git a/controllers/auth/templates/login.pug b/controllers/auth/templates/login.pug
index ba77221..64cf229 100644
--- a/controllers/auth/templates/login.pug
+++ b/controllers/auth/templates/login.pug
@@ -1,4 +1,17 @@
extends ../../../templates/base.pug
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
diff --git a/controllers/auth/templates/mail-was-sent.html b/controllers/auth/templates/mail-was-sent.html
new file mode 100644
index 0000000..1fe0934
--- /dev/null
+++ b/controllers/auth/templates/mail-was-sent.html
@@ -0,0 +1,9 @@
+{% extends "pyade/base.html" %}
+{% block content %}
+
+ A mail was sent to you.
+
+
+ In this mail, you'll find a link to activate your account.
+
+{% endblock %}
diff --git a/controllers/auth/templates/mailWasSent.pug b/controllers/auth/templates/mailWasSent.pug
new file mode 100644
index 0000000..c7cb319
--- /dev/null
+++ b/controllers/auth/templates/mailWasSent.pug
@@ -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 :'(
diff --git a/controllers/auth/templates/signup.pug b/controllers/auth/templates/signup.pug
new file mode 100644
index 0000000..64adfc1
--- /dev/null
+++ b/controllers/auth/templates/signup.pug
@@ -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);
+
diff --git a/controllers/auth/templates/validation-mail.pug b/controllers/auth/templates/validation-mail.pug
new file mode 100644
index 0000000..277cb1e
--- /dev/null
+++ b/controllers/auth/templates/validation-mail.pug
@@ -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 #{user.email}.
+ p.
+ To finalize your subscription, please click here to activate your account.
+
+ .footer.
+ If you did not subscribe on PyADE with this email address, please ignore this e-mail.
diff --git a/controllers/auth/templates/validation-mail.txt b/controllers/auth/templates/validation-mail.txt
new file mode 100644
index 0000000..88a6d3f
--- /dev/null
+++ b/controllers/auth/templates/validation-mail.txt
@@ -0,0 +1,10 @@
+Hello #{user.username}, and welcome on ADEjs!
+
+You recently subscribed on ADEjs with the email #{user.email}.
+
+To finalize your subscription, please click here to activate your account.
+
+See you later on ADEjs!
+
+---
+If you did not subscribe on ADEjs with this email address, please ignore this e-mail.
diff --git a/controllers/auth/urls.js b/controllers/auth/urls.js
index d4a207d..b6bcf9b 100644
--- a/controllers/auth/urls.js
+++ b/controllers/auth/urls.js
@@ -2,4 +2,10 @@ const url = require('create-url').url;
module.exports = [
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'),
]
diff --git a/controllers/auth/views.js b/controllers/auth/views.js
index 78d9746..019dfd0 100644
--- a/controllers/auth/views.js
+++ b/controllers/auth/views.js
@@ -1,10 +1,95 @@
+const testPassword = require('model').PasswordField.testSync;
const User = require('./models.js');
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) {
render('login.pug');
}
+module.exports.signup = function(req, res, render) {
+ render('signup.pug');
+}
+
module.exports.logout = function(req, res, render) {
+ req.session.user = undefined;
+ req.session.save();
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'));
+ });
+ });
+}
+
diff --git a/index.js b/index.js
index 1eebfc8..efcc46f 100644
--- a/index.js
+++ b/index.js
@@ -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.CONTROLLERS_DIR);
-const repl = require('repl');
const express = require('express');
const pug = require('pug');
const http = require('http');
@@ -19,7 +18,10 @@ function startServer() {
let app = express();
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 cookieParser = require('cookie-parser');
@@ -52,7 +54,7 @@ function startServer() {
app.use(function(req, res) {
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)
console.log(err);
res.send(result);
@@ -77,7 +79,6 @@ function startServer() {
const commands = {
runserver: startServer,
reinitdb: model.reinitialize,
- shell: repl.start,
}
function showHelp() {
diff --git a/package.json b/package.json
index ce5870d..acdd8ce 100644
--- a/package.json
+++ b/package.json
@@ -14,10 +14,12 @@
"author": "Thomas Forgione",
"license": "MIT",
"dependencies": {
+ "app-module-path": "^2.2.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.2",
"cookie-parser": "^1.4.3",
"cookie-session": "^1.3.1",
+ "emailjs": "^1.0.12",
"express": "^4.15.4",
"pg": "^7.3.0",
"pug": "^2.0.0-rc.4",
diff --git a/scripts/clear-db.js b/scripts/clear-db.js
new file mode 100644
index 0000000..6d4f3e1
--- /dev/null
+++ b/scripts/clear-db.js
@@ -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);
+
+});
diff --git a/settings/private.js b/settings/private.js
index 599aedd..5ea3064 100644
--- a/settings/private.js
+++ b/settings/private.js
@@ -6,5 +6,13 @@ module.exports = {
'USERNAME': 'adejs',
'DBNAME': 'adejs',
'PASSWORD':'97Cnqw023V1G95fQUJR8H7gwvqUke4',
+ },
+ 'MAIL': {
+ 'USERNAME': 'adejs',
+ 'PASSWORD': 'ILm0iSIgYI18w41dJU3ll9Pe5w9qjJ',
+ 'HOSTNAME': 'smtp.tforgione.fr',
+ 'PORT': '587',
+ 'FROM': 'ADEjs ',
+ 'TLS': true,
}
};
diff --git a/templates/404.pug b/templates/404.pug
new file mode 100644
index 0000000..058382c
--- /dev/null
+++ b/templates/404.pug
@@ -0,0 +1,6 @@
+extends ./base.pug
+
+block content
+ h1 Error 404
+ p.
+ This page does not exist.
diff --git a/templates/base.pug b/templates/base.pug
index 3b7781c..0e4650e 100644
--- a/templates/base.pug
+++ b/templates/base.pug
@@ -30,12 +30,12 @@ html
a.btn.btn-light(href=getUrl("login"), role='button')
| Sign in
li.nav-item
- a.btn.btn-success(href='#', role='button')
+ a.btn.btn-success(href=getUrl("signup"), role='button')
| Sign up
else
li.nav-item
a.nav-link(href='#')
- | Username
+ | #{session.user._username.value}
li.nav-item
a.btn.btn-light(href=getUrl("logout"), role='button')
| Log out
diff --git a/tests/test-password.js b/tests/test-password.js
index d768a9f..12c64c8 100644
--- a/tests/test-password.js
+++ b/tests/test-password.js
@@ -12,16 +12,29 @@ model.reinitialize(() => {
log.debug('Database reinitialized');
let user = new User();
- user.email = "toto";
+ user.username = "toto"
+ user.email = "toto@tforgione.fr";
user.password = "tata";
user.resources = 23;
user.project_id = 42;
user.save((err, u) => {
+ if (err) {
+ log.dberror(err);
+ return;
+ }
+
log.debug('New user created');
// Test password
User.getById(1, (err, user) => {
+
+ if (err !== undefined) {
+ log.dberror("User is undefined");
+ console.log(err);
+ return;
+ }
+
if (model.PasswordField.testSync("tata", user.password)) {
log.debug("Password authentication succeed");
} else {
diff --git a/utils/controllers.js b/utils/controllers.js
index 35eb9aa..0720c7e 100644
--- a/utils/controllers.js
+++ b/utils/controllers.js
@@ -22,7 +22,7 @@ module.exports = function(app, controllersDir = __dirname + '/../controllers') {
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) {
diff --git a/utils/create-url.js b/utils/create-url.js
index dcffe49..08dc75f 100644
--- a/utils/create-url.js
+++ b/utils/create-url.js
@@ -1,10 +1,11 @@
let urls = {};
class Url {
- constructor(url, view, name) {
+ constructor(url, view, name, method="GET") {
this.url = url;
this.view = view;
this.name = name;
+ this.method = method;
}
}
@@ -19,8 +20,8 @@ module.exports = function(req, res, next) {
module.exports.getUrl = getUrl;
-module.exports.url = function(url, viewName, urlName) {
- let ret = new Url(url, viewName, urlName);
+module.exports.url = function(url, viewName, urlName, method) {
+ let ret = new Url(url, viewName, urlName, method);
urls[urlName] = ret;
return ret;
}
diff --git a/utils/mail.js b/utils/mail.js
new file mode 100644
index 0000000..5841e44
--- /dev/null
+++ b/utils/mail.js
@@ -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);
+};
diff --git a/utils/model.js b/utils/model.js
index 4a4df11..a0edf64 100644
--- a/utils/model.js
+++ b/utils/model.js
@@ -5,6 +5,40 @@ const bc = require('bcryptjs');
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 {
constructor(name) {
models.push(this);
@@ -19,13 +53,17 @@ module.exports.BaseModel = class {
reinitialize(callback = () => {}) {
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))
.catch(err => log.dberror(err));
}
createTable(callback) {
- let query = 'CREATE TABLE ' + this.name + '(\n';
+ let query = 'CREATE TABLE ' + camelToPostgres(this.name) + '(\n';
query += this.fields
.map(f => '\t' + f.getCreationString())
.join(',\n');
@@ -35,7 +73,7 @@ module.exports.BaseModel = class {
db.query(query)
.then(res => {
- log.debug("Table " + this.name + " created")
+ log.debug("Table " + camelToPostgres(this.name) + " created")
callback();
})
.catch(err => log.dberror(err));
@@ -43,8 +81,30 @@ module.exports.BaseModel = class {
}
module.exports.BaseField = class {
- constructor(name) {
+ constructor(name, properties = {}) {
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() {
@@ -73,43 +133,43 @@ module.exports.BaseField = class {
}
module.exports.SmallIntegerField = class extends module.exports.BaseField {
- constructor(name) {
- super(name);
+ constructor(name, properties = {}) {
+ super(name, properties);
}
- getCreationString() {
- return this.name + " SMALLINT";
+ getPostgresType() {
+ return "SMALLINT";
}
}
module.exports.SerialField = class extends module.exports.BaseField {
- constructor(name) {
- super(name);
+ constructor(name, properties = {}) {
+ super(name, properties);
+ this.unique = true;
}
- getCreationString() {
- return this.name + " SERIAL";
+ getPostgresType() {
+ return "SERIAL";
}
-
}
module.exports.TextField = class extends module.exports.BaseField {
- constructor(name) {
- super(name);
+ constructor(name, properties = {}) {
+ super(name, properties);
}
- getCreationString() {
- return this.name + " TEXT";
+ getPostgresType() {
+ return "TEXT";
}
}
module.exports.PasswordField = class extends module.exports.BaseField {
- constructor(name) {
- super(name);
+ constructor(name, properties = {}) {
+ super(name, properties);
}
- getCreationString() {
- return this.name + ' TEXT';
+ getPostgresType() {
+ return 'TEXT';
}
@@ -121,7 +181,6 @@ module.exports.PasswordField = class extends module.exports.BaseField {
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) {
let ret = function(param) {
this._persisted = false;
for (let field of model.fields) {
this['_' + field.name] = {
- value: undefined,
- changed: false,
+ value: field.default,
+ changed: true,
}
Object.defineProperty(this, field.name, field.createMutators());
}
};
- ret.getById = function(id, callback) {
- db
- .query('SELECT * FROM ' + model.name + ' WHERE id=$1;', [id])
- .then(res => {
- // Create the instance
- let instance = new ret();
- instance._persisted = true;
- instance._setFromDbResult(res);
- callback(undefined, instance);
- })
- .catch(err => callback(err));
+ for (let field of model.fields) {
+ if (!field.unique && field.name !== 'activationKey')
+ continue;
+
+ ret[attributeToGetBy(field.name)] = function(value, callback) {
+ let query =
+ 'SELECT * FROM ' + camelToPostgres(model.name) +
+ ' WHERE ' + camelToPostgres(field.name) +
+ '=$1;';
+
+ 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) {
for (let field of model.fields) {
this['_' + field.name] = {
- value: res.rows[0][field.name],
+ value: res.rows[0][camelToPostgres(field.name)],
changed: false,
};
}
@@ -182,10 +262,10 @@ module.exports.createClass = function(model) {
if (!this._persisted) {
// INSERT INTO
- let queryBegin = 'INSERT INTO ' + model.name + '('
+ let queryBegin = 'INSERT INTO ' + camelToPostgres(model.name) + '('
let queryEnd = ' VALUES (';
queryBegin += fieldsToSave
- .map(f => f.field.name)
+ .map(f => camelToPostgres(f.field.name))
.join(',');
queryEnd += fieldsToSave
@@ -208,16 +288,16 @@ module.exports.createClass = function(model) {
} else {
// UPDATE FROM
- let query = 'UPDATE FROM ' + model.name + ' SET ';
+ let query = 'UPDATE ' + camelToPostgres(model.name) + ' SET ';
- query += fieldsToSave.map(f => {
- return f.field.name + '=' + f.value;
+ query += fieldsToSave.map((f, i) => {
+ return camelToPostgres(f.field.name) + '=$' + (i+1);
}).join(',');
- query += ') RETURNING *;'
+ query += ' WHERE id=$' + (fieldsToSave.length + 1) + ' RETURNING *;'
db
- .query(query, fieldsToSave.map(f => f.value))
+ .query(query, [...fieldsToSave.map(f => f.value), this.id])
.then(res => {
this._setFromDbResult(res);
this._persisted = true;