From: Benjamin Auder <benjamin.auder@somewhere>
Date: Wed, 9 Jan 2019 15:43:15 +0000 (+0100)
Subject: Finished User management implementation
X-Git-Url: https://git.auder.net/doc/html/css/scripts/%7B%7B%20asset%28%27mixstore/DESCRIPTION?a=commitdiff_plain;h=c018b304ba439ca92348dcb65715707f5cfcee05;p=vchess.git

Finished User management implementation
---

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', {
 				<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);
 				},
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)