From 8d7e2786f5a67a1b9a77c742d7951e0efbe8747d Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 8 Jan 2019 20:24:10 +0100
Subject: [PATCH] Save current state (unmerged, broken, not working...)

---
 .gitignore                                |   2 +-
 README.md                                 |  18 ++--
 app.js                                    |  23 +++-
 db/create.sql                             |  70 +++++++++++-
 db/populate.sql                           |   2 +-
 models/Challenge.js                       |  79 ++++++++++++++
 models/Game.js                            |   1 +
 models/Problem.js                         |  72 +++++++++++++
 models/User.js                            |  74 +++++++++++++
 models/Variant.js                         |  27 +++++
 public/javascripts/components/board.js    |   6 +-
 public/javascripts/components/game.js     |  10 +-
 public/javascripts/components/problems.js |   4 +-
 public/javascripts/components/room.js     |   6 +-
 public/javascripts/components/rules.js    |   2 +-
 public/javascripts/playCompMove.js        |   2 +-
 public/javascripts/settings.js            |  40 +++----
 public/javascripts/variant.js             |   6 +-
 routes/challenge.js                       |  77 +++++++++++++
 routes/index.js                           |  22 ++--
 routes/messages.js                        |   8 +-
 routes/playing.js                         | 126 ++++++++++++++++++++++
 routes/problems.js                        |  89 ++++++++-------
 routes/users.js                           | 109 +++++++++++++++++++
 routes/variant.js                         |  33 +++---
 utils/access.js                           |  41 +++++++
 utils/database.js                         |   5 +
 utils/mailer.js.dist                      |  50 +++++++++
 utils/sendEmail.js.dist                   |  32 ------
 views/layout.pug                          |   5 +
 views/login_register.pug                  |  31 ++++++
 views/logout_update.pug                   |  36 +++++++
 views/variant.pug                         |   4 +-
 33 files changed, 946 insertions(+), 166 deletions(-)
 create mode 100644 models/Challenge.js
 create mode 100644 models/Game.js
 create mode 100644 models/Problem.js
 create mode 100644 models/User.js
 create mode 100644 models/Variant.js
 create mode 100644 routes/challenge.js
 create mode 100644 routes/playing.js
 create mode 100644 routes/users.js
 create mode 100644 utils/access.js
 create mode 100644 utils/database.js
 create mode 100644 utils/mailer.js.dist
 delete mode 100644 utils/sendEmail.js.dist
 create mode 100644 views/login_register.pug
 create mode 100644 views/logout_update.pug

diff --git a/.gitignore b/.gitignore
index c273cf0c..8a553e65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,7 @@ pids
 
 # Various files
 /db/vchess.sqlite
-/utils/sendEmail.js
+/utils/mailer.js
 /public/javascripts/socket_url.js
 
 # CSS generated files
diff --git a/README.md b/README.md
index 0b2d396b..8c2ec848 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,16 @@ Website to play to many chess variants, including rare ones - some almost
 never seen elsewhere, like "l'Échiqueté" [french], renamed "checkered chess"
 in english.
 
-## Usage
-
-I hope it's intuitive enough :)
-
-But, a few important points:
+Notes:
  - Games start with a random assymetric position!
- - Your identity is revealed only after a game
+ - No ratings, no tournaments: no "competition spirit"
 
 ## Resources
 
 Server side:
  - node,
- - npm packages (see package.json),
+ - Express,
+ - Other npm packages (see package.json),
 
 Client side:
  - Vue.js,
@@ -32,7 +29,8 @@ Sounds and pieces images where found at various locations.
  1. git fat init && git fat pull
  2. Execute db/\*.sql scripts to create and fill db/vchess.sqlite
  3. Rename and edit public/javascripts/socket\_url.js.dist into socket\_url.js
- 4. npm i && npm start
+ 4. Rename and edit utils/mailer.js.dist into mailer.js
+ 5. npm i && npm start
 
 ## Get involved
 
@@ -42,6 +40,6 @@ All contributions are welcome! For example,
  - Vue front-end,
  - Express back-end.
 
-If you wanna help, you can send me an email (address indicated in the "Help"
-menu on the website) so that we can discuss what to do and how :)
+If you wanna help, you can contact me with the form on the website,
+so that we can discuss what to do and how :)
 If you feel comfortable with the code a pull request is a good start too.
diff --git a/app.js b/app.js
index 914e29b1..3199af3e 100644
--- a/app.js
+++ b/app.js
@@ -26,7 +26,7 @@ else
 }
 
 // Allow layout.pug to select the right vue file:
-app.locals.development = app.get('env') === 'development';
+app.locals.development = (app.get('env') === 'development');
 
 // view engine setup
 app.set('views', path.join(__dirname, 'views'));
@@ -43,6 +43,27 @@ app.use(sassMiddleware({
 }));
 app.use(express.static(path.join(__dirname, 'public')));
 
+// Before showing any page, check + save credentials
+app.use(function(req, res, next) {
+	req.loggedIn = false;
+	if (!req.cookies.token)
+		return next();
+	UserModel.getOne("sessionToken", req.cookies.token, function(err, user) {
+		if (!!user)
+		{
+			req.loggedIn = true;
+			res.locals.user = {
+				_id: user._id,
+				name: user.name,
+				email: user.email,
+				notify: user.notify,
+			};
+		}
+		next();
+	});
+});
+
+// Routing
 const routes = require(path.join(__dirname, "routes", "all"));
 app.use('/', routes);
 
diff --git a/db/create.sql b/db/create.sql
index c28bd561..e94b84a3 100644
--- a/db/create.sql
+++ b/db/create.sql
@@ -1,18 +1,78 @@
 -- Database should be in this folder, and named 'vchess.sqlite'
 
 create table Variants (
-	name varchar primary key,
+	id integer primary key,
+	name varchar unique,
 	description text
 );
 
+create table Users (
+	id integer primary key,
+	name varchar unique,
+	email varchar unique,
+	loginToken varchar,
+	loginTime datetime,
+	sessionToken varchar,
+	notify boolean
+);
+
 create table Problems (
-	num integer primary key,
+	id integer primary key,
 	added datetime,
-	variant varchar,
+	uid integer,
+	vid integer,
 	fen varchar,
 	instructions text,
 	solution text,
-	foreign key (variant) references Variants(name)
+	foreign key (uid) references Users(id),
+	foreign key (vid) references Variants(id)
+);
+
+-- All the following tables are for correspondance play only
+-- (Live games are stored only in browsers)
+
+create table Challenges (
+	id integer primary key,
+	added datetime,
+	uid integer,
+	vid integer,
+	foreign key (uid) references Users(id),
+	foreign key (vid) references Variants(id)
+);
+
+-- Store informations about players who accept a challenge
+create table WillPlay (
+	cid integer,
+	uid integer,
+	yes boolean,
+	foreign key (cid) references Challenges(id),
+	foreign key (uid) references Users(id)
+);
+
+create table Games (
+	id integer primary key,
+	vid integer,
+	fen varchar, --initial position
+	score varchar,
+	foreign key (vid) references Variants(id)
+);
+
+-- Store informations about players in a corr game
+create table Players (
+	uid integer,
+	color character,
+	gid integer,
+	foreign key (uid) references Users(id),
+	foreign key (gid) references Games(id)
+);
+
+create table Moves (
+	gid integer,
+	move varchar,
+	played datetime, --when was this move played?
+	idx integer, --index of the move in the game
+	color character, --required for e.g. Marseillais Chess
+	foreign key (gid) references Games(id)
 );
 
-PRAGMA foreign_keys = ON;
+pragma foreign_keys = on;
diff --git a/db/populate.sql b/db/populate.sql
index 8cc67d9f..4659704f 100644
--- a/db/populate.sql
+++ b/db/populate.sql
@@ -1,6 +1,6 @@
 -- Re-run this script after variants are added
 
-insert or ignore into Variants values
+insert or ignore into Variants (name,description) values
 	('Alice', 'Both sides of the mirror'),
 	('Antiking', 'Keep antiking in check'),
 	('Atomic', 'Explosive captures'),
diff --git a/models/Challenge.js b/models/Challenge.js
new file mode 100644
index 00000000..9980921c
--- /dev/null
+++ b/models/Challenge.js
@@ -0,0 +1,79 @@
+var db = require("../utils/database");
+
+/*
+ * Structure:
+ *   _id: BSON id
+ *   vid: variant ID
+ *   from: player ID
+ *   to: player ID, undefined if automatch
+ */
+
+exports.create = function(vid, from, to, callback)
+{
+	let chall = {
+		"vid": vid,
+		"from": from
+	};
+	if (!!to)
+		chall.to = to;
+	db.challenges.insert(chall, callback);
+}
+
+//////////
+// GETTERS
+
+exports.getById = function(cid, callback)
+{
+	db.challenges.findOne({_id: cid}, callback);
+}
+
+// For index page: obtain challenges that the player can accept
+exports.getByPlayer = function(uid, callback)
+{
+	db.challenges.aggregate(
+		{$match: {$or: [
+			{"to": uid},
+			{$and: [{"from": {$ne: uid}}, {"to": {$exists: false}}]}
+		]}},
+		{$project: {_id:0, vid:1}},
+		{$group: {_id:"$vid", count:{$sum:1}}},
+		callback);
+}
+
+// For variant page (challenges related to a player)
+exports.getByVariant = function(uid, vid, callback)
+{
+	db.challenges.find({$and: [
+		{"vid": vid},
+		{$or: [
+			{"to": uid},
+			{"from": uid},
+			{"to": {$exists: false}},
+		]}
+	]}, callback);
+}
+
+//////////
+// REMOVAL
+
+exports.remove = function(cid, callback)
+{
+	db.challenges.remove({_id: cid}, callback);
+}
+
+// Remove challenges older than 1 month, and 1to1 older than 36h
+exports.removeOld = function()
+{
+	var tsNow = new Date().getTime();
+	// 86400000 = 24 hours in milliseconds
+	var day = 86400000;
+	db.challenges.find({}, (err,challengeArray) => {
+		challengeArray.forEach( c => {
+			if (c._id.getTimestamp() + 30*day < tsNow //automatch
+				|| (!!c.to && c._id.getTimestamp() + 1.5*day < tsNow)) //1 to 1
+			{
+				db.challenges.remove({"_id": c._id});
+			}
+		});
+	});
+}
diff --git a/models/Game.js b/models/Game.js
new file mode 100644
index 00000000..2e57309f
--- /dev/null
+++ b/models/Game.js
@@ -0,0 +1 @@
+//TODO: at least this model (maybe MoveModel ?!)
diff --git a/models/Problem.js b/models/Problem.js
new file mode 100644
index 00000000..0c800901
--- /dev/null
+++ b/models/Problem.js
@@ -0,0 +1,72 @@
+var db = require("../utils/database");
+
+/*
+ * Structure:
+ *   _id: problem number (int)
+ *   uid: user id (int)
+ *   vid: variant id (int)
+ *   added: timestamp
+ *   instructions: text
+ *   solution: text
+ */
+
+exports.create = function(vname, fen, instructions, solution)
+{
+	db.serialize(function() {
+		db.get("SELECT id FROM Variants WHERE name = '" + vname + "'", (err,variant) => {
+			db.run(
+				"INSERT INTO Problems (added, vid, fen, instructions, solution) VALUES " +
+				"(" +
+					Date.now() + "," +
+					variant._id + "," +
+					fen + "," +
+					instructions + "," +
+					solution +
+				")");
+		});
+	});
+}
+
+exports.getById = function(id, callback)
+{
+	db.serialize(function() {
+		db.get(
+			"SELECT * FROM Problems " +
+			"WHERE id ='" + id + "'",
+			callback);
+	});
+}
+
+exports.fetchN = function(vname, directionStr, lastDt, MaxNbProblems, callback)
+{
+	db.serialize(function() {
+		db.all(
+			"SELECT * FROM Problems " +
+			"WHERE vid = (SELECT id FROM Variants WHERE name = '" + vname + "') " +
+			"  AND added " + directionStr + " " + lastDt + " " +
+			"ORDER BY added " + (directionStr=="<" ? "DESC " : "") +
+			"LIMIT " + MaxNbProblems,
+			callback);
+	});
+}
+
+exports.update = function(id, uid, fen, instructions, solution)
+{
+	db.serialize(function() {
+		db.run(
+			"UPDATE Problems " +
+				"fen = " + fen + ", " +
+				"instructions = " + instructions + ", " +
+				"solution = " + solution + " " +
+			"WHERE id = " + id + " AND uid = " + uid);
+	});
+}
+
+exports.remove = function(id, uid)
+{
+	db.serialize(function() {
+		db.run(
+			"DELETE FROM Problems " +
+			"WHERE id = " + id + " AND uid = " + uid);
+	});
+}
diff --git a/models/User.js b/models/User.js
new file mode 100644
index 00000000..66b1bf54
--- /dev/null
+++ b/models/User.js
@@ -0,0 +1,74 @@
+var db = require("../utils/database");
+var maild = require("../utils/mailer.js");
+
+/*
+ * Structure:
+ *   _id: integer
+ *   name: varchar
+ *   email: varchar
+ *   loginToken: token on server only
+ *   loginTime: datetime (validity)
+ *   sessionToken: token in cookies for authentication
+ *   notify: boolean (send email notifications for corr games)
+ */
+
+// User creation
+exports.create = function(name, email, notify, callback)
+{
+	if (!notify)
+		notify = false; //default
+	db.serialize(function() {
+		db.run(
+			"INSERT INTO Users " +
+			"(name, email, notify) VALUES " +
+			"(" + name + "," + email + "," + notify + ")");
+	});
+}
+
+// Find one user (by id, name, email, or token)
+exports.getOne = function(by, value, cb)
+{
+	const delimiter = (typeof value === "string" ? "'" : "");
+	db.serialize(function() {
+		db.get(
+			"SELECT * FROM Users " +
+			"WHERE " + by " = " + delimiter + value + delimiter,
+			callback);
+	});
+}
+
+/////////
+// MODIFY
+
+exports.setLoginToken = function(token, uid, cb)
+{
+	db.serialize(function() {
+		db.run(
+			"UPDATE Users " +
+			"SET loginToken = " + token + " AND loginTime = " + Date.now() + " " +
+			"WHERE id = " + uid);
+	});
+}
+
+exports.setSessionToken = function(token, uid, cb)
+{
+	// Also empty the login token to invalidate future attempts
+	db.serialize(function() {
+		db.run(
+			"UPDATE Users " +
+			"SET loginToken = NULL AND sessionToken = " + token + " " +
+			"WHERE id = " + uid);
+	});
+}
+
+exports.updateSettings = function(name, email, notify, cb)
+{
+	db.serialize(function() {
+		db.run(
+			"UPDATE Users " +
+			"SET name = " + name +
+			" AND email = " + email +
+			" AND notify = " + notify + " " +
+			"WHERE id = " + uid);
+	});
+}
diff --git a/models/Variant.js b/models/Variant.js
new file mode 100644
index 00000000..9a19f18c
--- /dev/null
+++ b/models/Variant.js
@@ -0,0 +1,27 @@
+var db = require("../utils/database");
+
+/*
+ * Structure:
+ *   _id: integer
+ *   name: varchar
+ *   description: varchar
+ */
+
+exports.getByName = function(name, callback)
+{
+	db.serialize(function() {
+		db.get(
+			"SELECT * FROM Variants " +
+			"WHERE name='" + name + "'",
+			callback);
+	});
+}
+
+exports.getAll = function(callback)
+{
+	db.serialize(function() {
+		db.all("SELECT * FROM Variants", callback);
+	});
+}
+
+//create, update, delete: directly in DB
diff --git a/public/javascripts/components/board.js b/public/javascripts/components/board.js
index 4cadb0bd..e786e899 100644
--- a/public/javascripts/components/board.js
+++ b/public/javascripts/components/board.js
@@ -57,7 +57,7 @@
 			);
 			// Create board element (+ reserves if needed by variant or mode)
 			const lm = this.vr.lastMove;
-			const showLight = this.hints && variant!="Dark" &&
+			const showLight = this.hints && variant.name!="Dark" &&
 				(this.mode != "idle" ||
 					(this.vr.moves.length > 0 && this.cursor==this.vr.moves.length));
 			const gameDiv = h('div',
@@ -80,7 +80,7 @@
 						_.range(sizeY).map(j => {
 							let cj = (this.mycolor=='w' ? j : sizeY-j-1);
 							let elems = [];
-							if (this.vr.board[ci][cj] != VariantRules.EMPTY && (variant!="Dark"
+							if (this.vr.board[ci][cj] != VariantRules.EMPTY && (variant.name!="Dark"
 								|| this.score!="*" || this.vr.enlightened[this.mycolor][ci][cj]))
 							{
 								elems.push(
@@ -125,7 +125,7 @@
 										'light-square': (i+j)%2==0,
 										'dark-square': (i+j)%2==1,
 										[this.bcolor]: true,
-										'in-shadow': variant=="Dark" && this.score=="*"
+										'in-shadow': variant.name=="Dark" && this.score=="*"
 											&& !this.vr.enlightened[this.mycolor][ci][cj],
 										'highlight': showLight && !!lm && _.isMatch(lm.end, {x:ci,y:cj}),
 										'incheck': showLight && incheckSq[ci][cj],
diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js
index 8009edd7..297ccc9d 100644
--- a/public/javascripts/components/game.js
+++ b/public/javascripts/components/game.js
@@ -82,7 +82,7 @@ Vue.component('my-game', {
 	},
 	created: function() {
 		const url = socketUrl;
-		this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
+		this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant._id);
 //		const socketOpenListener = () => {
 //		};
 
@@ -95,7 +95,7 @@ Vue.component('my-game', {
 			switch (data.code)
 			{
 				case "newmove": //..he played!
-					this.play(data.move, (variant!="Dark" ? "animate" : null));
+					this.play(data.move, (variant.name!="Dark" ? "animate" : null));
 					break;
 				case "pong": //received if we sent a ping (game still alive on our side)
 					if (this.gameId != data.gameId)
@@ -161,7 +161,7 @@ Vue.component('my-game', {
 		};
 
 		const socketCloseListener = () => {
-			this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant);
+			this.conn = new WebSocket(url + "/?sid=" + this.myid + "&page=" + variant._id);
 			//this.conn.addEventListener('open', socketOpenListener);
 			this.conn.addEventListener('message', socketMessageListener);
 			this.conn.addEventListener('close', socketCloseListener);
@@ -187,7 +187,7 @@ Vue.component('my-game', {
 
 
 		// Computer moves web worker logic: (TODO: also for observers in HH games)
-		this.compWorker.postMessage(["scripts",variant]);
+		this.compWorker.postMessage(["scripts",variant.name]);
 		const self = this;
 		this.compWorker.onmessage = function(e) {
 			let compMove = e.data;
@@ -201,7 +201,7 @@ Vue.component('my-game', {
 			// before they appear on page:
 			const delay = Math.max(500-(Date.now()-self.timeStart), 0);
 			setTimeout(() => {
-				const animate = (variant!="Dark" ? "animate" : null);
+				const animate = (variant.name!="Dark" ? "animate" : null);
 				if (self.mode == "computer") //warning: mode could have changed!
 					self.play(compMove[0], animate);
 				if (compMove.length == 2)
diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js
index ad8c54c0..00cb944d 100644
--- a/public/javascripts/components/problems.js
+++ b/public/javascripts/components/problems.js
@@ -175,7 +175,7 @@ Vue.component('my-problems', {
 					last_dt = this.problems[i].added;
 				}
 			}
-			ajax("/problems/" + variant, "GET", {
+			ajax("/problems/" + variant.name, "GET", { //TODO: use variant._id ?
 				direction: direction,
 				last_dt: last_dt,
 			}, response => {
@@ -201,7 +201,7 @@ Vue.component('my-problems', {
 		},
 		sendNewProblem: function() {
 			// Send it to the server and close modal
-			ajax("/problems/" + variant, "POST", {
+			ajax("/problems/" + variant.name, "POST", { //TODO: with variant._id ?
 				fen: this.newProblem.fen,
 				instructions: this.newProblem.instructions,
 				solution: this.newProblem.solution,
diff --git a/public/javascripts/components/room.js b/public/javascripts/components/room.js
index 16fcc903..20f01506 100644
--- a/public/javascripts/components/room.js
+++ b/public/javascripts/components/room.js
@@ -57,7 +57,7 @@ fin de partie corr: garder maxi nbPlayers lastMove sur serveur, pendant 7 jours
 			if (mode=="human" && !oppId)
 			{
 				const storageVariant = localStorage.getItem("variant");
-				if (!!storageVariant && storageVariant !== variant
+				if (!!storageVariant && storageVariant !== variant.name
 					&& localStorage["score"] == "*")
 				{
 					return alert(translations["Finish your "] +
@@ -82,7 +82,7 @@ fin de partie corr: garder maxi nbPlayers lastMove sur serveur, pendant 7 jours
 				if (!!storageVariant)
 				{
 					const score = localStorage.getItem(prefix+"score");
-					if (storageVariant !== variant && score == "*")
+					if (storageVariant !== variant.name && score == "*")
 					{
 						if (!confirm(storageVariant +
 							translations[": unfinished computer game will be erased"]))
@@ -98,7 +98,7 @@ fin de partie corr: garder maxi nbPlayers lastMove sur serveur, pendant 7 jours
 				if (!!storageVariant)
 				{
 					const score = localStorage.getItem(prefix+"score");
-					if (storageVariant !== variant && score == "*")
+					if (storageVariant !== variant.name && score == "*")
 					{
 						if (!confirm(storageVariant +
 							translations[": current analysis will be erased"]))
diff --git a/public/javascripts/components/rules.js b/public/javascripts/components/rules.js
index 1a597878..02d3a0ca 100644
--- a/public/javascripts/components/rules.js
+++ b/public/javascripts/components/rules.js
@@ -10,7 +10,7 @@ Vue.component('my-rules', {
 	`,
 	mounted: function() {
 		// AJAX request to get rules content (plain text, HTML)
-		ajax("/rules/" + variant, "GET", response => {
+		ajax("/rules/" + variant.name, "GET", response => {
 			let replaceByDiag = (match, p1, p2) => {
 				const args = this.parseFen(p2);
 				return getDiagram(args);
diff --git a/public/javascripts/playCompMove.js b/public/javascripts/playCompMove.js
index fc69ce24..86769337 100644
--- a/public/javascripts/playCompMove.js
+++ b/public/javascripts/playCompMove.js
@@ -13,7 +13,7 @@ onmessage = function(e)
 			break;
 		case "init":
 			const fen = e.data[1];
-			self.vr = new VariantRules(fen, []);
+			self.vr = new VariantRules(fen);
 			break;
 		case "newmove":
 			self.vr.play(e.data[1]);
diff --git a/public/javascripts/settings.js b/public/javascripts/settings.js
index 85f89c1f..f9d7649c 100644
--- a/public/javascripts/settings.js
+++ b/public/javascripts/settings.js
@@ -1,23 +1,23 @@
 // TODO:
 //à chaque onChange, envoyer matching event settings update
 //(par exemple si mise à jour du nom, juste envoyer cet update aux autres connectés ...etc)
-		setMyname: function(e) {
-			this.myname = e.target.value;
-			localStorage["username"] = this.myname;
-		},
-		showSettings: function(e) {
-			this.getRidOfTooltip(e.currentTarget);
-			document.getElementById("modal-settings").checked = true;
-		},
-		toggleHints: function() {
-			this.hints = !this.hints;
-			localStorage["hints"] = (this.hints ? "1" : "0");
-		},
-		setBoardColor: function(e) {
-			this.bcolor = e.target.options[e.target.selectedIndex].value;
-			localStorage["bcolor"] = this.bcolor;
-		},
-		setSound: function(e) {
-			this.sound = parseInt(e.target.options[e.target.selectedIndex].value);
-			localStorage["sound"] = this.sound;
-		},
+//		setMyname: function(e) {
+//			this.myname = e.target.value;
+//			localStorage["username"] = this.myname;
+//		},
+//		showSettings: function(e) {
+//			this.getRidOfTooltip(e.currentTarget);
+//			document.getElementById("modal-settings").checked = true;
+//		},
+//		toggleHints: function() {
+//			this.hints = !this.hints;
+//			localStorage["hints"] = (this.hints ? "1" : "0");
+//		},
+//		setBoardColor: function(e) {
+//			this.bcolor = e.target.options[e.target.selectedIndex].value;
+//			localStorage["bcolor"] = this.bcolor;
+//		},
+//		setSound: function(e) {
+//			this.sound = parseInt(e.target.options[e.target.selectedIndex].value);
+//			localStorage["sound"] = this.sound;
+//		},
diff --git a/public/javascripts/variant.js b/public/javascripts/variant.js
index 510ea290..eac1ec09 100644
--- a/public/javascripts/variant.js
+++ b/public/javascripts/variant.js
@@ -22,9 +22,9 @@ new Vue({
 	},
 });
 		
-const continuation = (localStorage.getItem("variant") === variant);
-			if (continuation) //game VS human has priority
-				this.continueGame("human");
+//const continuation = (localStorage.getItem("variant") === variant.name);
+//			if (continuation) //game VS human has priority
+//				this.continueGame("human");
 
 // TODO:
 // si quand on arrive il y a une continuation "humaine" : display="game" et retour à la partie !
diff --git a/routes/challenge.js b/routes/challenge.js
new file mode 100644
index 00000000..47aaad63
--- /dev/null
+++ b/routes/challenge.js
@@ -0,0 +1,77 @@
+var router = require("express").Router();
+var ObjectID = require("bson-objectid");
+var ChallengeModel = require('../models/Challenge');
+var UserModel = require('../models/User');
+var ObjectID = require("bson-objectid");
+var access = require("../utils/access");
+
+// Only AJAX requests here (from variant page and index)
+
+// variant page
+router.get("/challengesbyvariant", access.logged, access.ajax, (req,res) => {
+	if (req.query["uid"] != req.user._id)
+		return res.json({errmsg: "Not your challenges"});
+	let uid = ObjectID(req.query["uid"]);
+	let vid = ObjectID(req.query["vid"]);
+	ChallengeModel.getByVariant(uid, vid, (err, challengeArray) => {
+		res.json(err || {challenges: challengeArray});
+	});
+});
+
+// index
+router.get("/challengesbyplayer", access.logged, access.ajax, (req,res) => {
+	if (req.query["uid"] != req.user._id)
+		return res.json({errmsg: "Not your challenges"});
+	let uid = ObjectID(req.query["uid"]);
+	ChallengeModel.getByPlayer(uid, (err, challengeArray) => {
+		res.json(err || {challenges: challengeArray});
+	});
+});
+
+function createChallenge(vid, from, to, res)
+{
+	ChallengeModel.create(vid, from, to, (err, chall) => {
+		res.json(err || {
+			// A challenge can be sent using only name, thus 'to' is returned
+			to: chall.to,
+			cid: chall._id
+		});
+	});
+}
+
+// from[, to][,nameTo]
+router.post("/challenges", access.logged, access.ajax, (req,res) => {
+	if (req.body.from != req.user._id)
+		return res.json({errmsg: "Identity usurpation"});
+	let from = ObjectID(req.body.from);
+	let to = !!req.body.to ? ObjectID(req.body.to) : undefined;
+	let nameTo = !!req.body.nameTo ? req.body.nameTo : undefined;
+	let vid = ObjectID(req.body.vid);
+	if (!to && !!nameTo)
+	{
+		UserModel.getByName(nameTo, (err,user) => {
+			access.checkRequest(res, err, user, "Opponent not found", () => {
+				createChallenge(vid, from, user._id, res);
+			});
+		});
+	}
+	else if (!!to)
+		createChallenge(vid, from, to, res);
+	else
+		createChallenge(vid, from, undefined, res); //automatch
+});
+
+router.delete("/challenges", access.logged, access.ajax, (req,res) => {
+	let cid = ObjectID(req.query.cid);
+	ChallengeModel.getById(cid, (err,chall) => {
+		access.checkRequest(res, err, chall, "Challenge not found", () => {
+			if (!chall.from.equals(req.user._id) && !!chall.to && !chall.to.equals(req.user._id))
+				return res.json({errmsg: "Not your challenge"});
+			ChallengeModel.remove(cid, err => {
+				res.json(err || {});
+			});
+		});
+	});
+});
+
+module.exports = router;
diff --git a/routes/index.js b/routes/index.js
index ab44cf5e..0ed5b07a 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,19 +1,15 @@
 let router = require("express").Router();
-const sqlite3 = require('sqlite3');//.verbose();
-const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite");
-const db = new sqlite3.Database(DbPath);
-const selectLanguage = require(__dirname.replace("/routes", "/utils/language.js"));
+const VariantModel = require("../models/Variant");
+const selectLanguage = require("../utils/language.js");
 
 router.get('/', function(req, res, next) {
-	db.serialize(function() {
-		db.all("SELECT * FROM Variants", (err,variants) => {
-			if (!!err)
-				return next(err);
-			res.render('index', {
-				title: 'club',
-				variantArray: variants,
-				lang: selectLanguage(req, res),
-			});
+	VariantModel.getAll((err,variants) => {
+		if (!!err)
+			return next(err);
+		res.render('index', {
+			title: 'club',
+			variantArray: variants,
+			lang: selectLanguage(req, res),
 		});
 	});
 });
diff --git a/routes/messages.js b/routes/messages.js
index 7e60b7cd..9a72f5e4 100644
--- a/routes/messages.js
+++ b/routes/messages.js
@@ -1,15 +1,15 @@
 let router = require("express").Router();
-const sendEmail = require(__dirname.replace("/routes", "/utils/sendEmail"));
+const mailer = require(__dirname.replace("/routes", "/utils/mailer"));
 
 // Send a message through contact form
 router.post("/messages", (req,res,next) => {
 	if (!req.xhr)
 		return res.json({errmsg: "Unauthorized access"});
-	const email = req.body["email"];
+	const from = req.body["email"];
 	const subject = req.body["subject"];
-	const content = req.body["content"];
+	const body = req.body["body"];
 	// TODO: sanitize ?
-	sendEmail(email, subject, content, err => {
+	mailer.send(from, mailer.contact, subject, body, err => {
 		if (!!err)
 			return res.json({errmsg:err});
 		// OK, everything fine
diff --git a/routes/playing.js b/routes/playing.js
new file mode 100644
index 00000000..37592c9d
--- /dev/null
+++ b/routes/playing.js
@@ -0,0 +1,126 @@
+var router = require("express").Router();
+var UserModel = require("../models/User");
+var GameModel = require('../models/Game');
+var VariantModel = require('../models/Variant');
+var ObjectId = require("bson-objectid");
+var access = require("../utils/access");
+
+// Notify about a game (start, new move)
+function tryNotify(uid, gid, vname, subject)
+{
+	UserModel.getOne("id", uid, (err,user) => {
+		if (!!err && user.notify)
+		{
+			maild.send({
+				from: params.mailFrom,
+				to: user.email,
+				subject: subject,
+				body: params.siteURL + "?v=" + vname + "&g=" + gid
+			}, err => {
+				// TODO: log error somewhere.
+			});
+		}
+	)};
+}
+
+// From variant page, start game between player 0 and 1
+router.post("/games", access.logged, access.ajax, (req,res) => {
+	let variant = JSON.parse(req.body.variant);
+	let players = JSON.parse(req.body.players);
+	if (!players.includes(req.user._id.toString())) //TODO: should also check challenge...
+		return res.json({errmsg: "Cannot start someone else's game"});
+	let fen = req.body.fen;
+	// Randomly shuffle colors white/black
+	if (Math.random() < 0.5)
+		players = [players[1],players[0]];
+	GameModel.create(
+		ObjectId(variant._id), [ObjectId(players[0]),ObjectId(players[1])], fen,
+		(err,game) => {
+			access.checkRequest(res, err, game, "Cannot create game", () => {
+				if (!!req.body.offlineOpp)
+					UserModel.tryNotify(ObjectId(req.body.offlineOpp), game._id, variant.name, "New game");
+				game.movesLength = game.moves.length; //no need for all moves here
+				delete game["moves"];
+				res.json({game: game});
+			});
+		}
+	);
+});
+
+// game page
+router.get("/games", access.ajax, (req,res) => {
+	let gameID = req.query["gid"];
+	GameModel.getById(ObjectId(gameID), (err,game) => {
+		access.checkRequest(res, err, game, "Game not found", () => {
+			res.json({game: game});
+		});
+	});
+});
+
+router.put("/games", access.logged, access.ajax, (req,res) => {
+	let gid = ObjectId(req.body.gid);
+	let result = req.body.result;
+	// NOTE: only game-level life update is "gameover"
+	GameModel.gameOver(gid, result, ObjectId(req.user._id), (err,game) => {
+		access.checkRequest(res, err, game, "Cannot find game", () => {
+			res.json({});
+		});
+	});
+});
+
+// variant page
+router.get("/gamesbyvariant", access.logged, access.ajax, (req,res) => {
+	if (req.query["uid"] != req.user._id)
+		return res.json({errmsg: "Not your games"});
+	let uid = ObjectId(req.query["uid"]);
+	let vid = ObjectId(req.query["vid"]);
+	GameModel.getByVariant(uid, vid, (err,gameArray) => {
+		// NOTE: res.json already stringify, no need to do it manually
+		res.json(err || {games: gameArray});
+	});
+});
+
+// For index: only moves count + myColor
+router.get("/gamesbyplayer", access.logged, access.ajax, (req,res) => {
+	if (req.query["uid"] != req.user._id)
+		return res.json({errmsg: "Not your games"});
+	let uid = ObjectId(req.query["uid"]);
+	GameModel.getByPlayer(uid, (err,games) => {
+		res.json(err || {games: games});
+	});
+});
+
+// Load a rules page
+router.get("/rules/:variant([a-zA-Z0-9]+)", access.ajax, (req,res) => {
+	res.render("rules/" + req.params["variant"]);
+});
+
+// TODO: if newmove fail, takeback in GUI // TODO: check move structure
+router.post("/moves", access.logged, access.ajax, (req,res) => {
+	let gid = ObjectId(req.body.gid);
+	let fen = req.body.fen;
+	let vname = req.body.vname; //defined only if !!offlineOpp
+	// NOTE: storing the moves encoded lead to double stringify --> error at parsing
+	let move = JSON.parse(req.body.move);
+	GameModel.addMove(gid, move, fen, req._user._id, (err,game) => {
+		access.checkRequest(res, err, game, "Cannot find game", () => {
+			if (!!req.body.offlineOpp)
+				UserModel.tryNotify(ObjectId(req.body.offlineOpp), gid, vname, "New move");
+			res.json({});
+		});
+	});
+});
+
+//TODO: if new chat fails, do not show chat message locally
+router.post("/chats", access.logged, access.ajax, (req,res) => {
+	let gid = ObjectId(req.body.gid);
+	let uid = ObjectId(req.body.uid);
+	let msg = req.body.msg; //TODO: sanitize HTML (strip all tags...)
+	GameModel.addChat(gid, uid, msg, (err,game) => {
+		access.checkRequest(res, err, game, "Cannot find game", () => {
+			res.json({});
+		});
+	});
+});
+
+module.exports = router;
diff --git a/routes/problems.js b/routes/problems.js
index 62a2da15..b94aa601 100644
--- a/routes/problems.js
+++ b/routes/problems.js
@@ -1,58 +1,67 @@
+// AJAX methods to get, create, update or delete a problem
+
 let router = require("express").Router();
-const sqlite3 = require('sqlite3');
-const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite");
-const db = new sqlite3.Database(DbPath);
+const access = require("../utils/access");
+const ProblemModel = require("../models/Problem");
 const sanitizeHtml = require('sanitize-html');
 const MaxNbProblems = 20;
 
-// Fetch N previous or next problems (AJAX)
-router.get("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
-	if (!req.xhr)
-		return res.json({errmsg: "Unauthorized access"});
-	const vname = req.params["variant"];
+// Fetch N previous or next problems
+router.get("/problems/:vname([a-zA-Z0-9]+)", access.ajax, (req,res) => {
+	const vname = req.params["vname"];
 	const directionStr = (req.query.direction == "forward" ? ">" : "<");
 	const lastDt = req.query.last_dt;
 	if (!lastDt.match(/[0-9]+/))
 		return res.json({errmsg: "Bad timestamp"});
-	db.serialize(function() {
-		const query = "SELECT * FROM Problems " +
-			"WHERE variant='" + vname + "' " +
-			"  AND added " + directionStr + " " + lastDt + " " +
-			"ORDER BY added " + (directionStr=="<" ? "DESC " : "") +
-			"LIMIT " + MaxNbProblems;
-		db.all(query, (err,problems) => {
-			if (!!err)
-				return res.json(err);
-			return res.json({problems: problems});
-		});
+	ProblemModel.fetchN(vname, directionStr, lastDt, MaxNbProblems, (err,problems) => {
+		if (!!err)
+			return res.json(err);
+		return res.json({problems: problems});
 	});
 });
 
-// Upload a problem (AJAX)
-router.post("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
-	if (!req.xhr)
-		return res.json({errmsg: "Unauthorized access"});
-	const vname = req.params["variant"];
-	const timestamp = Date.now();
-	// Sanitize them
-	const fen = req.body["fen"];
+function sanitizeUserInput(fen, instructions, solution)
+{
 	if (!fen.match(/^[a-zA-Z0-9, /-]*$/))
-		return res.json({errmsg: "Bad characters in FEN string"});
-	const instructions = sanitizeHtml(req.body["instructions"]).trim();
-	const solution = sanitizeHtml(req.body["solution"]).trim();
+		return "Bad characters in FEN string";
+	instructions = sanitizeHtml(instructions);
+	solution = sanitizeHtml(solution);
 	if (instructions.length == 0)
-		return res.json({errmsg: "Empty instructions"});
+		return "Empty instructions";
 	if (solution.length == 0)
-		return res.json({errmsg: "Empty solution"});
-	db.serialize(function() {
-		let stmt = db.prepare("INSERT INTO Problems " +
-			"(added,variant,fen,instructions,solution) VALUES (?,?,?,?,?)");
-		stmt.run(timestamp, vname, fen, instructions, solution);
-		stmt.finalize();
-	});
-  res.json({});
+		return "Empty solution";
+	return {
+		fen: fen,
+		instructions: instructions,
+		solution: solution
+	};
+}
+
+// Upload a problem (sanitize inputs)
+router.post("/problems/:vname([a-zA-Z0-9]+)", access.logged, access.ajax, (req,res) => {
+	const vname = req.params["vname"];
+	const s = sanitizeUserInput(req.body["fen"], req.body["instructions"], req.body["solution"]);
+	if (typeof s === "string")
+		return res.json({errmsg: s});
+  ProblemModel.create(vname, s.fen, s.instructions, s.solution);
+	res.json({});
 });
 
-// TODO: edit, delete a problem
+// Update a problem (also sanitize inputs)
+router.put("/problems/:id([0-9]+)", access.logged, access.ajax, (req,res) => {
+	const pid = req.params["id"]; //problem ID
+	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);
+	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);
+	res.json({});
+});
 
 module.exports = router;
diff --git a/routes/users.js b/routes/users.js
new file mode 100644
index 00000000..dd9914ec
--- /dev/null
+++ b/routes/users.js
@@ -0,0 +1,109 @@
+var router = require("express").Router();
+var UserModel = require('../models/User');
+var maild = require('../utils/mailer');
+var TokenGen = require("../utils/tokenGenerator");
+var access = require("../utils/access");
+
+// to: object user
+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, to.ip, (err,ret) => {
+		access.checkRequest(res, err, ret, "Cannot set login token", () => {
+			maild.send({
+				from: params.mail.from,
+				to: to.email,
+				subject: subject,
+				body: "Hello " + to.initials + "!\n" +
+					"Access your account here: " +
+					params.siteURL + "/authenticate?token=" + token + "\\n" +
+					"Token will expire in " + params.token.expire/(1000*60) + " minutes."
+			}, err => {
+				res.json(err || {});
+			});
+		});
+	});
+}
+
+// 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)
+		return res.json({errmsg: error});
+	UserModel.create(name, email, (err,user) => {
+		access.checkRequest(res, err, user, "Registration failed", () => {
+			user.ip = req.ip;
+			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");
+	console.log(email)
+	if (error.length > 0)
+		return res.json({errmsg: error});
+	UserModel.getByEmail(email, (err,user) => {
+		access.checkRequest(res, err, user, "Unknown user", () => {
+			setAndSendLoginToken("Token for " + params.siteURL, user, res);
+		});
+	});
+});
+
+router.get('/authenticate', access.unlogged, (req,res) => {
+	UserModel.getByLoginToken(req.query.token, (err,user) => {
+		access.checkRequest(res, err, user, "Invalid token", () => {
+			if (user.loginToken.ip != req.ip)
+				return res.json({errmsg: "IP address mismatch"});
+			let now = new Date();
+			let tsNow = now.getTime();
+			// If token older than params.tokenExpire, do nothing
+			if (user.loginToken.timestamp + params.token.expire < tsNow)
+				return res.json({errmsg: "Token expired"});
+			// Generate and update session token + destroy login token
+			let token = TokenGen.generate(params.token.length);
+			UserModel.setSessionToken(token, user._id, (err,ret) => {
+				if (!!err)
+					return res.json(err);
+				// Set cookie
+				res.cookie("token", token, {
+					httpOnly: true,
+					maxAge: params.cookieExpire
+				});
+				res.redirect("/");
+			});
+		});
+	});
+});
+
+router.put('/settings', access.logged, access.ajax, (req,res) => {
+	let user = JSON.parse(req.body.user);
+	let error = checkObject(user, "User");
+	if (error.length > 0)
+		return res.json({errmsg: error});
+	user._id = ObjectID(req.user._id);
+	UserModel.updateSettings(user, (err,ret) => {
+		access.checkRequest(res, err, ret, "Settings update failed", () => {
+			res.json({});
+		});
+	});
+});
+
+router.get('/logout', access.logged, (req,res) => {
+	// TODO: cookie + redirect is enough (https, secure cookie
+	// https://www.information-security.fr/securite-sites-web-lutilite-flags-secure-httponly/ )
+	UserModel.logout(req.cookies.token, (err,ret) => {
+		access.checkRequest(res, err, ret, "Logout failed", () => {
+			res.clearCookie("token");
+			req.user = null;
+			res.redirect('/');
+		});
+	});
+});
+
+module.exports = router;
diff --git a/routes/variant.js b/routes/variant.js
index 44b7d804..f45c9594 100644
--- a/routes/variant.js
+++ b/routes/variant.js
@@ -1,33 +1,28 @@
 let router = require("express").Router();
 const createError = require('http-errors');
-const sqlite3 = require('sqlite3');
-const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite");
-const db = new sqlite3.Database(DbPath);
-const selectLanguage = require(__dirname.replace("/routes", "/utils/language.js"));
+const VariantModel = require("../models/Variant");
+const selectLanguage = require("../utils/language.js");
+const access = require("../utils/access");
 
 router.get("/:variant([a-zA-Z0-9]+)", (req,res,next) => {
 	const vname = req.params["variant"];
-	db.serialize(function() {
-		db.all("SELECT * FROM Variants WHERE name='" + vname + "'", (err,variant) => {
-			if (!!err)
-				return next(err);
-			if (!variant || variant.length==0)
-				return next(createError(404));
-			res.render('variant', {
-				title: vname + ' Variant',
-				variant: vname,
-				lang: selectLanguage(req, res),
-			});
+	VariantModel.getByName(vname, (err,variant) => {
+		if (!!err)
+			return next(err);
+		if (!variant)
+			return next(createError(404));
+		res.render('variant', {
+			title: vname + ' Variant',
+			variant: variant, //the variant ID might also be useful
+			lang: selectLanguage(req, res),
 		});
 	});
 });
 
 // Load a rules page (AJAX)
-router.get("/rules/:variant([a-zA-Z0-9]+)", (req,res) => {
-	if (!req.xhr)
-		return res.json({errmsg: "Unauthorized access"});
+router.get("/rules/:vname([a-zA-Z0-9]+)", access.ajax, (req,res) => {
 	const lang = selectLanguage(req, res);
-	res.render("rules/" + req.params["variant"] + "/" + lang);
+	res.render("rules/" + req.params["vname"] + "/" + lang);
 });
 
 module.exports = router;
diff --git a/utils/access.js b/utils/access.js
new file mode 100644
index 00000000..ca50b1c8
--- /dev/null
+++ b/utils/access.js
@@ -0,0 +1,41 @@
+var Access = {};
+
+// Prevent access to "users pages"
+Access.logged = function(req, res, next)
+{
+	if (!req.loggedIn)
+		return res.redirect("/");
+	next();
+};
+
+// Prevent access to "anonymous pages"
+Access.unlogged = function(req, res, next)
+{
+	if (!!req.loggedIn)
+		return res.redirect("/");
+	next();
+};
+
+// Prevent direct access to AJAX results
+Access.ajax = function(req, res, next)
+{
+	if (!req.xhr)
+		return res.json({errmsg: "Unauthorized access"});
+	next();
+}
+
+// Check for errors before callback (continue page loading). TODO: better name.
+Access.checkRequest = function(res, err, out, msg, cb)
+{
+	if (!!err)
+		return res.json(err);
+	if (!out
+		|| (Array.isArray(out) && out.length == 0)
+		|| (typeof out === "object" && Object.keys(out).length == 0))
+	{
+		return res.json({errmsg: msg});
+	}
+	cb();
+}
+
+module.exports = Access;
diff --git a/utils/database.js b/utils/database.js
new file mode 100644
index 00000000..39c7e5e0
--- /dev/null
+++ b/utils/database.js
@@ -0,0 +1,5 @@
+const sqlite3 = require('sqlite3');
+const DbPath = __dirname.replace("/utils", "/db/vchess.sqlite");
+const db = new sqlite3.Database(DbPath);
+
+module.exports = db;
diff --git a/utils/mailer.js.dist b/utils/mailer.js.dist
new file mode 100644
index 00000000..06cdc591
--- /dev/null
+++ b/utils/mailer.js.dist
@@ -0,0 +1,50 @@
+const nodemailer = require('nodemailer');
+
+const contact = "your_contact_email";
+
+const send = function(from, to, subject, body, cb)
+{
+  // Create reusable transporter object using the default SMTP transport
+	const transporter = nodemailer.createTransport({
+		host: "smtp_host_address",
+		port: 465, //if secure; otherwise use 587
+		secure: true,
+		auth: {
+			user: "user_name",
+			pass: "user_password"
+		}
+	});
+
+	// Setup email data with unicode symbols
+	const mailOptions = {
+		from: from, //note: some SMTP serves might forbid this
+		to: to,
+		subject: subject,
+		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)
+			return cb(error);
+    // Ignore info. Option:
+		//console.log('Message sent: %s', info.messageId);
+		return cb();
+  });
+};
+
+module.exports = {
+	contact: contact,
+	send: send
+};
diff --git a/utils/sendEmail.js.dist b/utils/sendEmail.js.dist
deleted file mode 100644
index cad123e8..00000000
--- a/utils/sendEmail.js.dist
+++ /dev/null
@@ -1,32 +0,0 @@
-const nodemailer = require('nodemailer');
-
-module.exports = function(email, subject, content, cb)
-{
-  // create reusable transporter object using the default SMTP transport
-	const transporter = nodemailer.createTransport({
-		host: "smtp_host_address",
-		port: 465, //if secure; otherwise use 587
-		secure: true,
-		auth: {
-			user: "user_name",
-			pass: "user_password"
-		}
-	});
-
-	// setup email data with unicode symbols
-	const mailOptions = {
-		from: email, //note: some SMTP serves might forbid this
-		to: "contact_email",
-		subject: subject,
-		text: content,
-  };
-
-	// send mail with defined transport object
-	transporter.sendMail(mailOptions, (error, info) => {
-		if (!!error)
-			return cb(error);
-    // Ignore info. Option:
-		//console.log('Message sent: %s', info.messageId);
-		return cb();
-  });
-};
diff --git a/views/layout.pug b/views/layout.pug
index da99fc31..60b7adcf 100644
--- a/views/layout.pug
+++ b/views/layout.pug
@@ -33,6 +33,10 @@ html
 				include translations/es
 			when "fr"
 				include translations/fr
+		if !user
+			include login_register
+		else
+			include logout_update
 		include contactForm
 		include settings
 		main
@@ -54,4 +58,5 @@ html
 			script(src="https://cdn.jsdelivr.net/npm/vue")
 		script.
 			const translations = !{JSON.stringify(translations)};
+			const user = !{JSON.stringify(user)};
 		block javascripts
diff --git a/views/login_register.pug b/views/login_register.pug
new file mode 100644
index 00000000..c831c8a1
--- /dev/null
+++ b/views/login_register.pug
@@ -0,0 +1,31 @@
+extends layout
+
+block css
+	link(rel="stylesheet", href="/stylesheets/login.css")
+
+block content
+	.mui-container
+		.row
+			.mui-col.xs-12.mui-col-sm-8.mui-col-sm-offset-2.mui-col-md-6.mui-col-md-offset-3.mui-col-lg-4.mui-col-lg-offset-4.mui--z1.white.pad-updown.pad-sides
+				form#loginForm(@submit.prevent="submit")
+					.mui-textfield.mui-textfield--float-label
+						input#email(type="email" ref="userEmail" v-model="user.email")
+						label#labEmail(for="email") Email
+					.mui-textfield.mui-textfield--float-label(v-show="stage == 'Register'")
+						input#name(type="text" v-model="user.name")
+						label#labName(for="name") Name
+					.mui--pull-left.space-bottom.space-top
+						button#submit.mui-btn.mui-btn--primary(@click.prevent="submit")
+							span {{ stage=="Login" ? "Go" : "Send" }}
+							i.material-icons.right send
+					.mui--pull-right.space-bottom.space-top
+						p
+							button.mui-btn.mui-btn--accent(@click.prevent="toggleStage()")
+								span {{ stage=="Login" ? "Register" : "Login" }}
+					#dialog.mui--hide.space-top
+
+block javascripts
+	script(src="//cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js")
+	script(src="/javascripts/utils/dialog.js")
+	script(src="/javascripts/utils/validation.js")
+	script(src="/javascripts/login.js")
diff --git a/views/logout_update.pug b/views/logout_update.pug
new file mode 100644
index 00000000..1b84483b
--- /dev/null
+++ b/views/logout_update.pug
@@ -0,0 +1,36 @@
+extends layout
+
+block css
+	link(rel="stylesheet", href="/stylesheets/settings.css")
+
+block content
+	.mui-container-fluid
+		.mui-row
+			.mui-col-xs-12.mui-col-sm-10.mui-col-sm-offset-1.mui-col-md-8.mui-col-md-offset-2.mui-col-lg-6.mui-col-lg-offset-3.mui--z1.white.pad-updown.pad-sides
+				form#settingsForm(@submit.prevent="submit")
+					.mui-textfield.mui-textfield--float-label
+						input#email(type="email" ref="userEmail" v-model="user.email")
+						label#labEmail.active(for="email") Email
+					.mui-textfield.mui-textfield--float-label
+						input#name(type="text" v-model="user.name")
+						label#labName.active(for="name") Name
+					p
+						span Theme &nbsp;&nbsp;&nbsp;&nbsp;
+						button(v-for="theme in themes" class="theme-btn mui-btn grey"
+								:class="themeClass(theme)" @click.prevent="toggleTheme(theme)")
+							| {{ theme }}
+					.mui-radio
+						input#notify(type="checkbox" v-model="user.notify")
+						label(for="notify") Notify new moves &amp; games
+					button#submit.mui-btn.mui-btn--primary(@click.prevent="submit")
+						span Apply
+						i.material-icons.right send
+					#dialog.mui--hide.space-top
+
+block javascripts
+	script(src="//cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js")
+	script(src="/javascripts/utils/dialog.js")
+	script(src="/javascripts/utils/validation.js")
+	script.
+		var user = !{JSON.stringify(user)};
+	script(src="/javascripts/settings.js")
diff --git a/views/variant.pug b/views/variant.pug
index 2fbf21e9..9419998a 100644
--- a/views/variant.pug
+++ b/views/variant.pug
@@ -38,10 +38,10 @@ block javascripts
 	script(src="/javascripts/utils/datetime.js")
 	script(src="/javascripts/socket_url.js")
 	script(src="/javascripts/base_rules.js")
-	script(src="/javascripts/variants/" + variant + ".js")
+	script(src="/javascripts/variants/" + variant.name + ".js")
 	script.
 		const V = VariantRules; //because this variable is often used
-		const variant = "#{variant}";
+		const variant = !{JSON.stringify(variant)};
 	script(src="/javascripts/components/room.js")
 	script(src="/javascripts/components/gameList.js")
 	script(src="/javascripts/components/rules.js")
-- 
2.44.0