Finished User management implementation
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 9 Jan 2019 15:43:15 +0000 (16:43 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 9 Jan 2019 15:43:15 +0000 (16:43 +0100)
app.js
config/parameters.js.dist
models/Problem.js
models/User.js
models/Variant.js
public/javascripts/components/upsertUser.js
public/javascripts/shared/userCheck.js
routes/problems.js
routes/users.js
utils/access.js
utils/mailer.js

diff --git a/app.js b/app.js
index 22c220b..28fd375 100644 (file)
--- 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();
        });
 });
index 944a6ee..bb3cbe2 100644 (file)
@@ -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;
index 0c80090..cdd146e 100644 (file)
@@ -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 +
index 6e91458..6eff273 100644 (file)
@@ -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);
        });
 }
index ce5329c..8d7eba2 100644 (file)
@@ -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);
        });
 }
index f996ea1..dde6708 100644 (file)
@@ -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', {
                                <div class="card">
                                        <label class="modal-close" for="modalUser"></label>
                                        <h3>{{ stage }}</h3>
-                                       <form id="userForm" @submit.prevent="submit">
+                                       <form id="userForm" @submit.prevent="onSubmit()">
                                                <div v-show="stage!='Login'">
                                                        <fieldset>
                                                                <label for="username">Name</label>
@@ -38,17 +38,19 @@ Vue.component('my-upsert-user', {
                                                                <input id="nameOrEmail" type="text" v-model="nameOrEmail"/>
                                                        </fieldset>
                                                </div>
-                                               <fieldset>
-                                                       <button id="submit" @click.prevent="submit">
-                                                               <span>{{ submitMessage }}</span>
-                                                               <i class="material-icons">send</i>
-                                                       </button>
-                                               </fieldset>
                                        </form>
-                                       <button v-if="stage!='Update'" @click.prevent="toggleStage()">
-                                               <span>{{ stage=="Login" ? "Register" : "Login" }}</span>
-                                       </button>
-                                       <button v-if="stage=='Update'">Logout</button>
+                                       <div class="button-group">
+                                               <button id="submit" @click="onSubmit()">
+                                                       <span>{{ submitMessage }}</span>
+                                                       <i class="material-icons">send</i>
+                                               </button>
+                                               <button v-if="stage!='Update'" @click="toggleStage()">
+                                                       <span>{{ stage=="Login" ? "Register" : "Login" }}</span>
+                                               </button>
+                                               <button v-if="stage=='Update'" onClick="location.replace('/logout')">
+                                                       <span>Logout</span>
+                                               </button>
+                                       </div>
                                        <div id="dialog" :style="{display: displayInfo}">{{ infoMsg }}</div>
                                </div>
                        </div>
@@ -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);
                                },
index bd282ba..65ed1db 100644 (file)
@@ -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";
index b94aa60..43258a0 100644 (file)
@@ -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({});
 });
 
index f3f5199..9639ad5 100644 (file)
@@ -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('/');
index ca50b1c..1c82cb6 100644 (file)
@@ -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))
index c8080b9..8a059da 100644 (file)
@@ -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)