Save current state (unmerged, broken, not working...)
authorBenjamin Auder <benjamin.auder@somewhere>
Tue, 8 Jan 2019 19:24:10 +0000 (20:24 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Tue, 8 Jan 2019 19:24:10 +0000 (20:24 +0100)
33 files changed:
.gitignore
README.md
app.js
db/create.sql
db/populate.sql
models/Challenge.js [new file with mode: 0644]
models/Game.js [new file with mode: 0644]
models/Problem.js [new file with mode: 0644]
models/User.js [new file with mode: 0644]
models/Variant.js [new file with mode: 0644]
public/javascripts/components/board.js
public/javascripts/components/game.js
public/javascripts/components/problems.js
public/javascripts/components/room.js
public/javascripts/components/rules.js
public/javascripts/playCompMove.js
public/javascripts/settings.js
public/javascripts/variant.js
routes/challenge.js [new file with mode: 0644]
routes/index.js
routes/messages.js
routes/playing.js [new file with mode: 0644]
routes/problems.js
routes/users.js [new file with mode: 0644]
routes/variant.js
utils/access.js [new file with mode: 0644]
utils/database.js [new file with mode: 0644]
utils/mailer.js.dist [new file with mode: 0644]
utils/sendEmail.js.dist [deleted file]
views/layout.pug
views/login_register.pug [new file with mode: 0644]
views/logout_update.pug [new file with mode: 0644]
views/variant.pug

index c273cf0..8a553e6 100644 (file)
@@ -17,7 +17,7 @@ pids
 
 # Various files
 /db/vchess.sqlite
-/utils/sendEmail.js
+/utils/mailer.js
 /public/javascripts/socket_url.js
 
 # CSS generated files
index 0b2d396..8c2ec84 100644 (file)
--- 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 914e29b..3199af3 100644 (file)
--- 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);
 
index c28bd56..e94b84a 100644 (file)
@@ -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;
index 8cc67d9..4659704 100644 (file)
@@ -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 (file)
index 0000000..9980921
--- /dev/null
@@ -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 (file)
index 0000000..2e57309
--- /dev/null
@@ -0,0 +1 @@
+//TODO: at least this model (maybe MoveModel ?!)
diff --git a/models/Problem.js b/models/Problem.js
new file mode 100644 (file)
index 0000000..0c80090
--- /dev/null
@@ -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 (file)
index 0000000..66b1bf5
--- /dev/null
@@ -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 (file)
index 0000000..9a19f18
--- /dev/null
@@ -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
index 4cadb0b..e786e89 100644 (file)
@@ -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(
                                                                                '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],
index 8009edd..297ccc9 100644 (file)
@@ -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)
index ad8c54c..00cb944 100644 (file)
@@ -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,
index 16fcc90..20f0150 100644 (file)
@@ -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"]))
index 1a59787..02d3a0c 100644 (file)
@@ -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);
index fc69ce2..8676933 100644 (file)
@@ -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]);
index 85f89c1..f9d7649 100644 (file)
@@ -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;
+//             },
index 510ea29..eac1ec0 100644 (file)
@@ -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 (file)
index 0000000..47aaad6
--- /dev/null
@@ -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;
index ab44cf5..0ed5b07 100644 (file)
@@ -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),
                });
        });
 });
index 7e60b7c..9a72f5e 100644 (file)
@@ -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 (file)
index 0000000..37592c9
--- /dev/null
@@ -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;
index 62a2da1..b94aa60 100644 (file)
@@ -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 (file)
index 0000000..dd9914e
--- /dev/null
@@ -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;
index 44b7d80..f45c959 100644 (file)
@@ -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 (file)
index 0000000..ca50b1c
--- /dev/null
@@ -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 (file)
index 0000000..39c7e5e
--- /dev/null
@@ -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 (file)
index 0000000..06cdc59
--- /dev/null
@@ -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 (file)
index cad123e..0000000
+++ /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();
-  });
-};
index da99fc3..60b7adc 100644 (file)
@@ -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 (file)
index 0000000..c831c8a
--- /dev/null
@@ -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 (file)
index 0000000..1b84483
--- /dev/null
@@ -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")
index 2fbf21e..9419998 100644 (file)
@@ -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")