From 8a477a7e1b781babc74d7935b80ac0b18ec04f86 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
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: `
 		<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
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