User management logic half-debugged
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 9 Jan 2019 10:50:07 +0000 (11:50 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 9 Jan 2019 10:50:07 +0000 (11:50 +0100)
14 files changed:
TODO
models/User.js
models/Variant.js
public/javascripts/components/upsertUser.js
public/javascripts/shared/userCheck.js [new file with mode: 0644]
public/javascripts/utils/ajax.js
public/javascripts/utils/misc.js
routes/users.js
views/error.pug
views/index.pug
views/layout.pug
views/translations/en.pug
views/translations/es.pug
views/translations/fr.pug

diff --git a/TODO b/TODO
index cbe3285..667f8cd 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,3 +1,5 @@
+// TODO: decodeURIComponent() for GET/DELETE parameters
+
 1) Finish problems tab
 2) Integrate computer play into rules tab
 3) Retrieve users system from old code
index 777eeaa..6e91458 100644 (file)
@@ -16,13 +16,12 @@ var TokenGen = require("../utils/tokenGenerator");
 // User creation
 exports.create = function(name, email, notify, callback)
 {
-       if (!notify)
-               notify = false; //default
        db.serialize(function() {
-               db.run(
+               const query =
                        "INSERT INTO Users " +
                        "(name, email, notify) VALUES " +
-                       "(" + name + "," + email + "," + notify + ")");
+                       "('" + name + "', '" + email + "', " + notify + ")";
+               db.run(query, callback); //TODO: need to get the inserted user (how ?)
        });
 }
 
@@ -31,10 +30,10 @@ exports.getOne = function(by, value, cb)
 {
        const delimiter = (typeof value === "string" ? "'" : "");
        db.serialize(function() {
-               db.get(
+               const query =
                        "SELECT * FROM Users " +
-                       "WHERE " + by + " = " + delimiter + value + delimiter,
-                       callback);
+                       "WHERE " + by + " = " + delimiter + value + delimiter;
+               db.get(query, cb);
        });
 }
 
@@ -44,10 +43,11 @@ exports.getOne = function(by, value, cb)
 exports.setLoginToken = function(token, uid, cb)
 {
        db.serialize(function() {
-               db.run(
+               const query =
                        "UPDATE Users " +
                        "SET loginToken = " + token + " AND loginTime = " + Date.now() + " " +
-                       "WHERE id = " + uid);
+                       "WHERE id = " + uid;
+               db.run(query, cb);
        });
 }
 
@@ -57,18 +57,20 @@ exports.trySetSessionToken = function(uid, cb)
 {
        // Also empty the login token to invalidate future attempts
        db.serialize(function() {
-               db.get(
+               const querySessionTOken =
                        "SELECT sessionToken " +
                        "FROM Users " +
-                       "WHERE id = " + uid, (err,token) => {
-                               if (!!err)
-                                       return cb(err);
-                               const newToken = token || TokenGen.generate(params.token.length);
-                               db.run(
-                                       "UPDATE Users " +
-                                       "SET loginToken = NULL " +
-                                       (!token ? "AND sessionToken = " + newToken + " " : "") +
-                                       "WHERE id = " + uid);
+                       "WHERE id = " + uid;
+               db.get(querySessionToken, (err,token) => {
+                       if (!!err)
+                               return cb(err);
+                       const newToken = token || TokenGen.generate(params.token.length);
+                       const queryUpdate =
+                               "UPDATE Users " +
+                               "SET loginToken = NULL " +
+                               (!token ? "AND sessionToken = " + newToken + " " : "") +
+                               "WHERE id = " + uid;
+                       db.run(queryUpdate);
                                cb(null, newToken);
                });
        });
@@ -77,11 +79,12 @@ exports.trySetSessionToken = function(uid, cb)
 exports.updateSettings = function(user, cb)
 {
        db.serialize(function() {
-               db.run(
+               const query =
                        "UPDATE Users " +
                        "SET name = " + user.name +
                        " AND email = " + user.email +
                        " AND notify = " + user.notify + " " +
-                       "WHERE id = " + user._id);
+                       "WHERE id = " + user._id;
+               db.run(query, cb);
        });
 }
index 9a19f18..ce5329c 100644 (file)
@@ -10,17 +10,18 @@ var db = require("../utils/database");
 exports.getByName = function(name, callback)
 {
        db.serialize(function() {
-               db.get(
+               const query =
                        "SELECT * FROM Variants " +
-                       "WHERE name='" + name + "'",
-                       callback);
+                       "WHERE name='" + name + "'";
+               db.get(query, callback);
        });
 }
 
 exports.getAll = function(callback)
 {
        db.serialize(function() {
-               db.all("SELECT * FROM Variants", callback);
+               const query = "SELECT * FROM Variants";
+               db.all(query, callback);
        });
 }
 
index 29444b4..f996ea1 100644 (file)
@@ -1,41 +1,56 @@
 // Logic to login, or create / update a user (and also logout)
 Vue.component('my-upsert-user', {
-       props: ["initUser"], //to find the game in storage (assumption: it exists)
        data: function() {
                return {
-                       user: initUser, //initialized with prop value
-                       stage: (!initUser.email ? "Login" : "Update"),
+                       user: user, //initialized with global user object
+                       nameOrEmail: "", //for login
+                       stage: (!user.email ? "Login" : "Update"),
                        infoMsg: "",
+                       enterTime: Number.MAX_SAFE_INTEGER, //for a basic anti-bot strategy
                };
        },
        template: `
                <div>
-                       <input id="modalUser" class="modal" type="checkbox"/>
+                       <input id="modalUser" class="modal" type="checkbox"
+                                       @change="trySetEnterTime"/>
                        <div role="dialog">
                                <div class="card">
-                                       <label class="modal-close" for="modalUser">
+                                       <label class="modal-close" for="modalUser"></label>
                                        <h3>{{ stage }}</h3>
                                        <form id="userForm" @submit.prevent="submit">
+                                               <div v-show="stage!='Login'">
+                                                       <fieldset>
+                                                               <label for="username">Name</label>
+                                                               <input id="username" type="text" v-model="user.name"/>
+                                                       </fieldset>
+                                                       <fieldset>
+                                                               <label for="useremail">Email</label>
+                                                               <input id="useremail" type="email" v-model="user.email"/>
+                                                       </fieldset>
+                                                       <fieldset>
+                                                               <label for="notifyNew">Notify new moves &amp; games</label>
+                                                               <input id="notifyNew" type="checkbox" v-model="user.notify"/>
+                                                       </fieldset>
+                                               </div>
+                                               <div v-show="stage=='Login'">
+                                                       <fieldset>
+                                                               <label for="nameOrEmail">Name or Email</label>
+                                                               <input id="nameOrEmail" type="text" v-model="nameOrEmail"/>
+                                                       </fieldset>
+                                               </div>
                                                <fieldset>
-                                                       <label for="useremail">Email</label>
-                                                       <input id="useremail" type="email" v-model="user.email"/>
-                                               <fieldset>
-                                                       <label for="username">Name</label>
-                                                       <input id="username" type="text" v-model="user.name"/>
+                                                       <button id="submit" @click.prevent="submit">
+                                                               <span>{{ submitMessage }}</span>
+                                                               <i class="material-icons">send</i>
+                                                       </button>
                                                </fieldset>
-                                               <fieldset>
-                                                       <label for="notifyNew">Notify new moves &amp; games</label>
-                                                       <input id="notifyNew" type="checkbox" v-model="user.notify"/>
-                                               <button id="submit" @click.prevent="submit">
-                                                       <span>{{ submitMessage }}</span>
-                                                       <i class="material-icons">send</i>
-                               <p v-if="stage!='Update'">
-                                       <button @click.prevent="toggleStage()">
+                                       </form>
+                                       <button v-if="stage!='Update'" @click.prevent="toggleStage()">
                                                <span>{{ stage=="Login" ? "Register" : "Login" }}</span>
                                        </button>
-                                       <button>Logout</button>
-                               </p>
-                               <div id="dialog" :style="{display: displayInfo}">{{ infoMsg }}</div>
+                                       <button v-if="stage=='Update'">Logout</button>
+                                       <div id="dialog" :style="{display: displayInfo}">{{ infoMsg }}</div>
+                               </div>
                        </div>
                </div>
        `,
@@ -56,7 +71,12 @@ Vue.component('my-upsert-user', {
                },
        },
        methods: {
+               trySetEnterTime: function(event) {
+                       if (!!event.target.checked)
+                               this.enterTime = Date.now();
+               },
                toggleStage: function() {
+                       // Loop login <--> register (update is for logged-in users)
                        this.stage = (this.stage == "Login" ? "Register" : "Login");
                },
                ajaxUrl: function() {
@@ -93,19 +113,28 @@ Vue.component('my-upsert-user', {
                        }
                },
                submit: function() {
-                       // TODO: re-activate simple measures like this: (using time of click on modal)
-//                     const exitTime = new Date();
-//                     if (this.stage=="Register" && exitTime.getTime() - enterTime.getTime() < 5000)
-//                             return;
-                       if (!this.user.name.match(/[a-z0-9_]+/i))
-                               return alert("User name: only alphanumerics and underscore");
+                       // Basic anti-bot strategy:
+                       const exitTime = Date.now();
+                       if (this.stage == "Register" && exitTime - this.enterTime < 5000)
+                               return; //silently return, in (curious) case of it was legitimate
+                       let error = undefined;
+                       if (this.stage == 'Login')
+                       {
+                               const type = (this.nameOrEmail.indexOf('@') >= 0 ? "email" : "name");
+                               error = checkNameEmail({[type]: this.nameOrEmail});
+                       }
+                       else
+                               error = checkNameEmail(this.user);
+                       if (!!error)
+                               return alert(error);
                        this.infoMsg = "Processing... Please wait";
                        ajax(this.ajaxUrl(), this.ajaxMethod(),
-                               this.stage == "Login" ? "PUT" : "POST", this.user,
+                               this.stage == "Login" ? { nameOrEmail: this.nameOrEmail } : this.user,
                                res => {
                                        this.infoMsg = this.infoMessage();
                                        if (this.stage != "Update")
                                        {
+                                               this.nameOrEmail = "";
                                                this.user["email"] = "";
                                                this.user["name"] = "";
                                        }
@@ -113,6 +142,10 @@ Vue.component('my-upsert-user', {
                                                this.infoMsg = "";
                                                document.getElementById("modalUser").checked = false;
                                        }, 2000);
+                               },
+                               err => {
+                                       this.infoMsg = "";
+                                       alert(err);
                                }
                        );
                },
diff --git a/public/javascripts/shared/userCheck.js b/public/javascripts/shared/userCheck.js
new file mode 100644 (file)
index 0000000..bd282ba
--- /dev/null
@@ -0,0 +1,19 @@
+function checkNameEmail(o)
+{
+       if (!!o.name)
+       {
+               if (o.name.length == 0)
+                       return "Empty name";
+               if (!o.name.match(/^[\w]+$/))
+                       return "Bad characters in name";
+       }
+       if (!!o.email)
+       {
+               if (o.email.length == 0)
+                       return "Empty email";
+               if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/))
+                       return "Bad characters in email";
+       }
+}
+
+try { module.exports = checkNameEmail; } catch(e) { } //for server
index a354690..dc21705 100644 (file)
@@ -42,7 +42,6 @@ function ajax(url, method, data, success, error)
                // Append query params to URL
                url += "/?" + toQueryString(data);
        }
-
        xhr.open(method, url, true);
        xhr.setRequestHeader('X-Requested-With', "XMLHttpRequest");
        if (["POST","PUT"].includes(method))
index d72e3ce..4ad0766 100644 (file)
@@ -35,3 +35,9 @@ function setLanguage(e)
        setCookie("lang", e.target.value);
        location.reload(); //to include the right .pug file
 }
+
+// Shortcut for an often used click (on a modal)
+function doClick(elemId)
+{
+       document.getElementById(elemId).click(); //or ".checked = true"
+}
index 297072d..f3f5199 100644 (file)
@@ -4,6 +4,7 @@ var sendEmail = require('../utils/mailer');
 var TokenGen = require("../utils/tokenGenerator");
 var access = require("../utils/access");
 var params = require("../config/parameters");
+var checkNameEmail = require("../public/javascripts/shared/userCheck")
 
 // to: object user
 function setAndSendLoginToken(subject, to, res)
@@ -13,7 +14,7 @@ function setAndSendLoginToken(subject, to, res)
        UserModel.setLoginToken(token, to._id, (err,ret) => {
                access.checkRequest(res, err, ret, "Cannot set login token", () => {
                        const body =
-                               "Hello " + to.initials + "!\n" +
+                               "Hello " + to.name + "!\n" +
                                "Access your account here: " +
                                params.siteURL + "/authenticate?token=" + token + "\\n" +
                                "Token will expire in " + params.token.expire/(1000*60) + " minutes."
@@ -27,24 +28,26 @@ function setAndSendLoginToken(subject, to, res)
 // AJAX user life cycle...
 
 router.post('/register', access.unlogged, access.ajax, (req,res) => {
-       let name = decodeURIComponent(req.body.name);
-       let email = decodeURIComponent(req.body.email);
-       let error = checkObject({name:name, email:email}, "User");
-       if (error.length > 0)
+       const name = req.body.name;
+       const email = req.body.email;
+       const notify = !!req.body.notify;
+       const error = checkNameEmail({name: name, email: email});
+       if (!!error)
                return res.json({errmsg: error});
-       UserModel.create(name, email, (err,user) => {
+       UserModel.create(name, email, notify, (err,user) => {
                access.checkRequest(res, err, user, "Registration failed", () => {
                        setAndSendLoginToken("Welcome to " + params.siteURL, user, res);
                });
        });
 });
 
-router.put('/sendtoken', access.unlogged, access.ajax, (req,res) => {
-       let email = decodeURIComponent(req.body.email);
-       let error = checkObject({email:email}, "User");
-       if (error.length > 0)
+router.get('/sendtoken', access.unlogged, access.ajax, (req,res) => {
+       const nameOrEmail = decodeURIComponent(req.query.nameOrEmail);
+       const type = (nameOrEmail.indexOf('@') >= 0 ? "email" : "name");
+       const error = checkNameEmail({[type]: nameOrEmail});
+       if (!!error)
                return res.json({errmsg: error});
-       UserModel.getOne("email", email, (err,user) => {
+       UserModel.getOne(type, nameOrEmail, (err,user) => {
                access.checkRequest(res, err, user, "Unknown user", () => {
                        setAndSendLoginToken("Token for " + params.siteURL, user, res);
                });
@@ -54,7 +57,6 @@ router.put('/sendtoken', access.unlogged, access.ajax, (req,res) => {
 router.get('/authenticate', access.unlogged, (req,res) => {
        UserModel.getByLoginToken(req.query.token, (err,user) => {
                access.checkRequest(res, err, user, "Invalid token", () => {
-                       let tsNow = Date.now();
                        // If token older than params.tokenExpire, do nothing
                        if (Date.now() > user.loginTime + params.token.expire)
                                return res.json({errmsg: "Token expired"});
@@ -75,12 +77,12 @@ router.get('/authenticate', access.unlogged, (req,res) => {
 });
 
 router.put('/settings', access.logged, access.ajax, (req,res) => {
-       const user = JSON.parse(req.body.user);
-       // TODO: either verify email + name, or re-apply the following logic:
-       //let error = checkObject(user, "User");
-       //if (error.length > 0)
-       //      return res.json({errmsg: error});
-       user._id = req.user._id; //TODO:
+       let user = JSON.parse(req.body.user);
+       const error = checkNameEmail({name: user.name, email: user.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({});
index 51ec12c..7eb2aff 100644 (file)
@@ -1,6 +1,7 @@
-extends layout
+doctype html
+html
 
-block content
+body
   h1= message
   h2= error.status
   pre #{error.stack}
index 5dfa903..45a2ec7 100644 (file)
@@ -19,11 +19,9 @@ block content
                                        .info-container
                                                p vchess.club
                                        img(src="/images/index/wildebeest.svg")
-                               #flagMenu.clickable(
-                                               onClick="document.getElementById('modalLang').checked=true")
+                               #flagMenu.clickable(onClick="doClick('modalLang')")
                                        img(src="/images/flags/" + lang + ".svg")
-                               #userMenu.clickable(
-                                               onClick="document.getElementById('modalUser').checked=true")
+                               #userMenu.clickable(onClick="doClick('modalUser')")
                                        .info-container
                                                if !user.email
                                                        p
@@ -33,8 +31,7 @@ block content
                                                        p
                                                                span Update
                                                                i.material-icons person
-                               #introductionMenu.clickable(
-                                               onClick="document.getElementById('modalWelcome').checked=true")
+                               #introductionMenu.clickable(onClick="doClick('modalWelcome')")
                                        .info-container
                                                p Introduction
                .row
index 03249be..6004fc3 100644 (file)
@@ -24,7 +24,6 @@ html
                block css
 
        body
-
                include langNames
                case lang
                        when "en"
@@ -33,10 +32,10 @@ html
                                include translations/es
                        when "fr"
                                include translations/fr
-               include contactForm
                include modalLang
+               include contactForm
                main#VueElement
-                       my-upsert-user(:user="user" :stage="stage")
+                       my-upsert-user()
                        block content
                footer.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2.text-center
                        div
@@ -53,6 +52,7 @@ html
                        script(src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js")
                else
                        script(src="https://cdn.jsdelivr.net/npm/vue")
+               script(src="/javascripts/shared/userCheck.js")
                script(src="/javascripts/components/upsertUser.js")
                script.
                        const translations = !{JSON.stringify(translations)};
index 9663311..611236c 100644 (file)
@@ -2,7 +2,7 @@
        var translations =
        {
                "Language": "Language",
-               "Contact": "Contact",
+               "Contact form": "Contact form",
                "Email": "Email",
                "Subject": "Subject",
                "Content": "Content",
@@ -32,7 +32,7 @@
                "Pawns move diagonally": "Pawns move diagonally",
                "In the shadow": "In the shadow",
                "Move twice": "Move twice",
-               "Head upside down": "Head upside down",
+               "Board upside down": "Board upside down",
 
                // Variant page:
                "New game": "New game",
index 642059e..ce21966 100644 (file)
@@ -25,7 +25,7 @@
                "Pawns move diagonally": "Peones se mueven en diagonal",
                "In the shadow": "En la sombra",
                "Move twice": "Mover dos veces",
-               "Head upside down": "Cabeza al revés",
+               "Board upside down": "Tablero al revés",
 
                // Variant page:
                "New game": "Nueva partida",
index 78b1074..3f3b1fe 100644 (file)
@@ -25,7 +25,7 @@
                "Pawns move diagonally": "Les pions vont en diagonale",
                "In the shadow": "Dans l'ombre",
                "Move twice": "Jouer deux coups",
-               "Head upside down": "La tête à l'envers",
+               "Board upside down": "Échiquier à l'envers",
 
                // Variant page:
                "New game": "Nouvelle partie",