From 8a477a7e1b781babc74d7935b80ac0b18ec04f86 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Wed, 9 Jan 2019 11:50:07 +0100 Subject: [PATCH] User management logic half-debugged --- TODO | 2 + models/User.js | 45 ++++++----- models/Variant.js | 9 ++- public/javascripts/components/upsertUser.js | 89 ++++++++++++++------- public/javascripts/shared/userCheck.js | 19 +++++ public/javascripts/utils/ajax.js | 1 - public/javascripts/utils/misc.js | 6 ++ routes/users.js | 38 ++++----- views/error.pug | 5 +- views/index.pug | 9 +-- views/layout.pug | 6 +- views/translations/en.pug | 4 +- views/translations/es.pug | 2 +- views/translations/fr.pug | 2 +- 14 files changed, 150 insertions(+), 87 deletions(-) create mode 100644 public/javascripts/shared/userCheck.js diff --git a/TODO b/TODO index cbe32853..667f8cda 100644 --- 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 diff --git a/models/User.js b/models/User.js index 777eeaa2..6e91458e 100644 --- a/models/User.js +++ b/models/User.js @@ -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); }); } diff --git a/models/Variant.js b/models/Variant.js index 9a19f18c..ce5329c7 100644 --- a/models/Variant.js +++ b/models/Variant.js @@ -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); }); } diff --git a/public/javascripts/components/upsertUser.js b/public/javascripts/components/upsertUser.js index 29444b46..f996ea19 100644 --- a/public/javascripts/components/upsertUser.js +++ b/public/javascripts/components/upsertUser.js @@ -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: `
- +
-
`, @@ -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 index 00000000..bd282baf --- /dev/null +++ b/public/javascripts/shared/userCheck.js @@ -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 diff --git a/public/javascripts/utils/ajax.js b/public/javascripts/utils/ajax.js index a3546900..dc217053 100644 --- a/public/javascripts/utils/ajax.js +++ b/public/javascripts/utils/ajax.js @@ -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)) diff --git a/public/javascripts/utils/misc.js b/public/javascripts/utils/misc.js index d72e3ce2..4ad07661 100644 --- a/public/javascripts/utils/misc.js +++ b/public/javascripts/utils/misc.js @@ -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" +} diff --git a/routes/users.js b/routes/users.js index 297072dd..f3f51991 100644 --- a/routes/users.js +++ b/routes/users.js @@ -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({}); diff --git a/views/error.pug b/views/error.pug index 51ec12c6..7eb2affe 100644 --- a/views/error.pug +++ b/views/error.pug @@ -1,6 +1,7 @@ -extends layout +doctype html +html -block content +body h1= message h2= error.status pre #{error.stack} diff --git a/views/index.pug b/views/index.pug index 5dfa903a..45a2ec77 100644 --- a/views/index.pug +++ b/views/index.pug @@ -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 diff --git a/views/layout.pug b/views/layout.pug index 03249bed..6004fc32 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -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)}; diff --git a/views/translations/en.pug b/views/translations/en.pug index 96633111..611236c7 100644 --- a/views/translations/en.pug +++ b/views/translations/en.pug @@ -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", diff --git a/views/translations/es.pug b/views/translations/es.pug index 642059ed..ce219666 100644 --- a/views/translations/es.pug +++ b/views/translations/es.pug @@ -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", diff --git a/views/translations/fr.pug b/views/translations/fr.pug index 78b10744..3f3b1fe0 100644 --- a/views/translations/fr.pug +++ b/views/translations/fr.pug @@ -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", -- 2.44.0