From c018b304ba439ca92348dcb65715707f5cfcee05 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 9 Jan 2019 16:43:15 +0100 Subject: [PATCH] Finished User management implementation --- app.js | 13 ++-- config/parameters.js.dist | 41 ++++++------ models/Problem.js | 4 +- models/User.js | 34 +++++----- models/Variant.js | 9 ++- public/javascripts/components/upsertUser.js | 30 +++++---- public/javascripts/shared/userCheck.js | 4 +- routes/problems.js | 4 +- routes/users.js | 70 ++++++++++++--------- utils/access.js | 6 +- utils/mailer.js | 21 +++---- 11 files changed, 133 insertions(+), 103 deletions(-) diff --git a/app.js b/app.js index 22c220b9..28fd3757 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ var cookieParser = require('cookie-parser'); var logger = require('morgan'); var sassMiddleware = require('node-sass-middleware'); var favicon = require('serve-favicon'); +var UserModel = require(path.join(__dirname, "models", "User")); var app = express(); @@ -45,21 +46,25 @@ app.use(express.static(path.join(__dirname, 'public'))); // Before showing any page, check + save credentials app.use(function(req, res, next) { - req.loggedIn = false; - res.locals.user = { name: "" }; + req.userId = 0; //means "anonymous" + res.locals.user = { name: "" }; //"anonymous" if (!req.cookies.token) return next(); UserModel.getOne("sessionToken", req.cookies.token, function(err, user) { if (!!user) { - req.loggedIn = true; + req.userId = user.id; res.locals.user = { - _id: user._id, name: user.name, email: user.email, notify: user.notify, }; } + else + { + // Token in cookies presumably wrong: erase it + res.clearCookie("token"); + } next(); }); }); diff --git a/config/parameters.js.dist b/config/parameters.js.dist index 944a6ee5..bb3cbe29 100644 --- a/config/parameters.js.dist +++ b/config/parameters.js.dist @@ -1,25 +1,30 @@ -var Parameters = { }; +const Parameters = +{ + // For mail sending. NOTE: *no trailing slash* + siteURL: "http://localhost:3000", -// For mail sending. NOTE: *no trailing slash* -Parameters.siteURL = "http://localhost:3000"; + // To know in which environment the code run + env: process.env.NODE_ENV || 'development', -// Lifespan of a (login) cookie -Parameters.cookieExpire = 183*24*3600*1000; //6 months in milliseconds + // Lifespan of a (login) cookie + cookieExpire: 183*24*3600*1000, //6 months in milliseconds -// Characters in a login token, and period of validity (in milliseconds) -Parameters.token = { - length: 16, - expire: 1000*60*30, //30 minutes in milliseconds -}; + // Characters in a login token, and period of validity (in milliseconds) + token: { + length: 16, + expire: 1000*60*30, //30 minutes in milliseconds + }, -// Email settings -Parameters.mail = { - host: "mail_host_address", - port: 465, //if secure; otherwise use 587 - secure: true, //...or false - user: "mail_user_name", - pass: "mail_password", - contact: "some_contact_email", + // Email settings + mail: { + host: "mail_host_address", + port: 465, //if secure; otherwise use 587 + secure: true, //...or false + user: "mail_user_name", + pass: "mail_password", + noreply: "some_noreply_email", + contact: "some_contact_email", + }, }; module.exports = Parameters; diff --git a/models/Problem.js b/models/Problem.js index 0c800901..cdd146e7 100644 --- a/models/Problem.js +++ b/models/Problem.js @@ -2,7 +2,7 @@ var db = require("../utils/database"); /* * Structure: - * _id: problem number (int) + * id: problem number (int) * uid: user id (int) * vid: variant id (int) * added: timestamp @@ -18,7 +18,7 @@ exports.create = function(vname, fen, instructions, solution) "INSERT INTO Problems (added, vid, fen, instructions, solution) VALUES " + "(" + Date.now() + "," + - variant._id + "," + + variant.id + "," + fen + "," + instructions + "," + solution + diff --git a/models/User.js b/models/User.js index 6e91458e..6eff2735 100644 --- a/models/User.js +++ b/models/User.js @@ -1,6 +1,7 @@ var db = require("../utils/database"); var maild = require("../utils/mailer.js"); var TokenGen = require("../utils/tokenGenerator"); +var params = require("../config/parameters"); /* * Structure: @@ -17,11 +18,15 @@ var TokenGen = require("../utils/tokenGenerator"); exports.create = function(name, email, notify, callback) { db.serialize(function() { - const query = + const insertQuery = "INSERT INTO Users " + "(name, email, notify) VALUES " + "('" + name + "', '" + email + "', " + notify + ")"; - db.run(query, callback); //TODO: need to get the inserted user (how ?) + db.run(insertQuery, err => { + if (!!err) + return callback(err); + db.get("SELECT last_insert_rowid() AS rowid", callback); + }); }); } @@ -31,7 +36,8 @@ exports.getOne = function(by, value, cb) const delimiter = (typeof value === "string" ? "'" : ""); db.serialize(function() { const query = - "SELECT * FROM Users " + + "SELECT * " + + "FROM Users " + "WHERE " + by + " = " + delimiter + value + delimiter; db.get(query, cb); }); @@ -45,7 +51,7 @@ exports.setLoginToken = function(token, uid, cb) db.serialize(function() { const query = "UPDATE Users " + - "SET loginToken = " + token + " AND loginTime = " + Date.now() + " " + + "SET loginToken = '" + token + "', loginTime = " + Date.now() + " " + "WHERE id = " + uid; db.run(query, cb); }); @@ -57,21 +63,21 @@ exports.trySetSessionToken = function(uid, cb) { // Also empty the login token to invalidate future attempts db.serialize(function() { - const querySessionTOken = + const querySessionToken = "SELECT sessionToken " + "FROM Users " + "WHERE id = " + uid; - db.get(querySessionToken, (err,token) => { + db.get(querySessionToken, (err,ret) => { if (!!err) return cb(err); - const newToken = token || TokenGen.generate(params.token.length); + const token = ret.sessionToken || TokenGen.generate(params.token.length); const queryUpdate = "UPDATE Users " + - "SET loginToken = NULL " + - (!token ? "AND sessionToken = " + newToken + " " : "") + + "SET loginToken = NULL" + + (!ret.sessionToken ? (", sessionToken = '" + token + "'") : "") + " " + "WHERE id = " + uid; db.run(queryUpdate); - cb(null, newToken); + cb(null, token); }); }); } @@ -81,10 +87,10 @@ exports.updateSettings = function(user, cb) db.serialize(function() { const query = "UPDATE Users " + - "SET name = " + user.name + - " AND email = " + user.email + - " AND notify = " + user.notify + " " + - "WHERE id = " + user._id; + "SET name = '" + user.name + "'" + + ", email = '" + user.email + "'" + + ", notify = " + user.notify + " " + + "WHERE id = " + user.id; db.run(query, cb); }); } diff --git a/models/Variant.js b/models/Variant.js index ce5329c7..8d7eba25 100644 --- a/models/Variant.js +++ b/models/Variant.js @@ -2,7 +2,7 @@ var db = require("../utils/database"); /* * Structure: - * _id: integer + * id: integer * name: varchar * description: varchar */ @@ -11,7 +11,8 @@ exports.getByName = function(name, callback) { db.serialize(function() { const query = - "SELECT * FROM Variants " + + "SELECT * " + + "FROM Variants " + "WHERE name='" + name + "'"; db.get(query, callback); }); @@ -20,7 +21,9 @@ exports.getByName = function(name, callback) exports.getAll = function(callback) { db.serialize(function() { - const query = "SELECT * FROM Variants"; + const query = + "SELECT * " + + "FROM Variants"; db.all(query, callback); }); } diff --git a/public/javascripts/components/upsertUser.js b/public/javascripts/components/upsertUser.js index f996ea19..dde6708e 100644 --- a/public/javascripts/components/upsertUser.js +++ b/public/javascripts/components/upsertUser.js @@ -1,5 +1,5 @@ // Logic to login, or create / update a user (and also logout) -Vue.component('my-upsert-user', { +vv = Vue.component('my-upsert-user', { data: function() { return { user: user, //initialized with global user object @@ -17,7 +17,7 @@ Vue.component('my-upsert-user', {

{{ stage }}

-
+
@@ -38,17 +38,19 @@ Vue.component('my-upsert-user', {
-
- -
- - +
+ + + +
{{ infoMsg }}
@@ -112,7 +114,7 @@ Vue.component('my-upsert-user', { return "Modifications applied!"; } }, - submit: function() { + onSubmit: function() { // Basic anti-bot strategy: const exitTime = Date.now(); if (this.stage == "Register" && exitTime - this.enterTime < 5000) @@ -140,6 +142,8 @@ Vue.component('my-upsert-user', { } setTimeout(() => { this.infoMsg = ""; + if (this.stage == "Register") + this.stage = "Login"; document.getElementById("modalUser").checked = false; }, 2000); }, diff --git a/public/javascripts/shared/userCheck.js b/public/javascripts/shared/userCheck.js index bd282baf..65ed1db8 100644 --- a/public/javascripts/shared/userCheck.js +++ b/public/javascripts/shared/userCheck.js @@ -1,13 +1,13 @@ function checkNameEmail(o) { - if (!!o.name) + if (typeof o.name === "string") { if (o.name.length == 0) return "Empty name"; if (!o.name.match(/^[\w]+$/)) return "Bad characters in name"; } - if (!!o.email) + if (typeof o.email === "string") { if (o.email.length == 0) return "Empty email"; diff --git a/routes/problems.js b/routes/problems.js index b94aa601..43258a0c 100644 --- a/routes/problems.js +++ b/routes/problems.js @@ -53,14 +53,14 @@ router.put("/problems/:id([0-9]+)", access.logged, access.ajax, (req,res) => { const s = sanitizeUserInput(req.body["fen"], req.body["instructions"], req.body["solution"]); if (typeof s === "string") return res.json({errmsg: s}); - ProblemModel.update(pid, req.user._id, fen, instructions, solution); + ProblemModel.update(pid, req.userId, fen, instructions, solution); res.json({}); }); // Delete a problem router.delete("/problems/:id([0-9]+)", access.logged, access.ajax, (req,res) => { const pid = req.params["id"]; //problem ID - ProblemModel.delete(pid, req.user._id); + ProblemModel.delete(pid, req.userId); res.json({}); }); diff --git a/routes/users.js b/routes/users.js index f3f51991..9639ad58 100644 --- a/routes/users.js +++ b/routes/users.js @@ -6,21 +6,21 @@ var access = require("../utils/access"); var params = require("../config/parameters"); var checkNameEmail = require("../public/javascripts/shared/userCheck") -// to: object user +// to: object user (to who we send an email) function setAndSendLoginToken(subject, to, res) { // Set login token and send welcome(back) email with auth link - let token = TokenGen.generate(params.token.length); - UserModel.setLoginToken(token, to._id, (err,ret) => { - access.checkRequest(res, err, ret, "Cannot set login token", () => { - const body = - "Hello " + to.name + "!\n" + - "Access your account here: " + - params.siteURL + "/authenticate?token=" + token + "\\n" + - "Token will expire in " + params.token.expire/(1000*60) + " minutes." - sendEmail(params.mail.from, to.email, subject, body, err => { - res.json(err || {}); - }); + const token = TokenGen.generate(params.token.length); + UserModel.setLoginToken(token, to.id, err => { + if (!!err) + return res.json({errmsg: err.toString()}); + const body = + "Hello " + to.name + "!\n" + + "Access your account here: " + + params.siteURL + "/authenticate?token=" + token + "\\n" + + "Token will expire in " + params.token.expire/(1000*60) + " minutes." + sendEmail(params.mail.noreply, to.email, subject, body, err => { + res.json(err || {}); }); }); } @@ -34,10 +34,15 @@ router.post('/register', access.unlogged, access.ajax, (req,res) => { const error = checkNameEmail({name: name, email: email}); if (!!error) return res.json({errmsg: error}); - UserModel.create(name, email, notify, (err,user) => { - access.checkRequest(res, err, user, "Registration failed", () => { - setAndSendLoginToken("Welcome to " + params.siteURL, user, res); - }); + UserModel.create(name, email, notify, (err,uid) => { + if (!!err) + return res.json({errmsg: err.toString()}); + const user = { + id: uid["rowid"], + name: name, + email: email, + }; + setAndSendLoginToken("Welcome to " + params.siteURL, user, res); }); }); @@ -55,20 +60,20 @@ router.get('/sendtoken', access.unlogged, access.ajax, (req,res) => { }); router.get('/authenticate', access.unlogged, (req,res) => { - UserModel.getByLoginToken(req.query.token, (err,user) => { + UserModel.getOne("loginToken", req.query.token, (err,user) => { access.checkRequest(res, err, user, "Invalid token", () => { // If token older than params.tokenExpire, do nothing if (Date.now() > user.loginTime + params.token.expire) return res.json({errmsg: "Token expired"}); // Generate session token (if not exists) + destroy login token - UserModel.trySetSessionToken(user._id, (err,token) => { + UserModel.trySetSessionToken(user.id, (err,token) => { if (!!err) - return res.json(err); + return res.json({errmsg: err.toString()}); // Set cookie res.cookie("token", token, { httpOnly: true, - secure: true, - maxAge: params.cookieExpire + secure: !!params.siteURL.match(/^https/), + maxAge: params.cookieExpire, }); res.redirect("/"); }); @@ -76,21 +81,24 @@ router.get('/authenticate', access.unlogged, (req,res) => { }); }); -router.put('/settings', access.logged, access.ajax, (req,res) => { - let user = JSON.parse(req.body.user); - const error = checkNameEmail({name: user.name, email: user.email}); +router.put('/update', access.logged, access.ajax, (req,res) => { + const name = req.body.name; + const email = req.body.email; + const error = checkNameEmail({name: name, email: email}); if (!!error) return res.json({errmsg: error}); - user.notify = !!user.notify; //in case of... - user._id = res.locals.user._id; //in case of... - UserModel.updateSettings(user, (err,ret) => { - access.checkRequest(res, err, ret, "Settings update failed", () => { - res.json({}); - }); + const user = { + id: req.userId, + name: name, + email: email, + notify: !!req.body.notify, + }; + UserModel.updateSettings(user, err => { + res.json(err ? {errmsg: err.toString()} : {}); }); }); -// Logout on server because the token cookie is secured + http-only +// Logout on server because the token cookie is httpOnly router.get('/logout', access.logged, (req,res) => { res.clearCookie("token"); res.redirect('/'); diff --git a/utils/access.js b/utils/access.js index ca50b1c8..1c82cb67 100644 --- a/utils/access.js +++ b/utils/access.js @@ -3,7 +3,7 @@ var Access = {}; // Prevent access to "users pages" Access.logged = function(req, res, next) { - if (!req.loggedIn) + if (req.userId == 0) return res.redirect("/"); next(); }; @@ -11,7 +11,7 @@ Access.logged = function(req, res, next) // Prevent access to "anonymous pages" Access.unlogged = function(req, res, next) { - if (!!req.loggedIn) + if (req.userId > 0) return res.redirect("/"); next(); }; @@ -28,7 +28,7 @@ Access.ajax = function(req, res, next) Access.checkRequest = function(res, err, out, msg, cb) { if (!!err) - return res.json(err); + return res.json({errmsg: err.errmsg || err.toString()}); if (!out || (Array.isArray(out) && out.length == 0) || (typeof out === "object" && Object.keys(out).length == 0)) diff --git a/utils/mailer.js b/utils/mailer.js index c8080b95..8a059da2 100644 --- a/utils/mailer.js +++ b/utils/mailer.js @@ -3,6 +3,16 @@ const params = require("../config/parameters"); module.exports = function(from, to, subject, body, cb) { + // Avoid the actual sending in development mode + if (params.env === 'development') + { + console.log("New mail: from " + from + " / to " + to); + console.log("Subject: " + subject); + let msgText = body.split('\\n'); + msgText.forEach(msg => { console.log(msg); }); + return cb(); + } + // Create reusable transporter object using the default SMTP transport const transporter = nodemailer.createTransport({ host: params.mail.host, @@ -22,17 +32,6 @@ module.exports = function(from, to, subject, body, cb) text: body, }; - // Avoid the actual sending in development mode - const env = process.env.NODE_ENV || 'development'; - if ('development' === env) - { - console.log("New mail: from " + from + " / to " + to); - console.log("Subject: " + subject); - let msgText = body.split('\\n'); - msgText.forEach(msg => { console.log(msg); }); - return cb(); - } - // Send mail with the defined transport object transporter.sendMail(mailOptions, (error, info) => { if (!!error) -- 2.44.0