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
 
 # Various files
 /db/vchess.sqlite
-/utils/sendEmail.js
+/utils/mailer.js
 /public/javascripts/socket_url.js
 
 # CSS generated files
 /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.
 
 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!
  - 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,
 
 ## Resources
 
 Server side:
  - node,
- - npm packages (see package.json),
+ - Express,
+ - Other npm packages (see package.json),
 
 Client side:
  - Vue.js,
 
 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
  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
 
 
 ## Get involved
 
@@ -42,6 +40,6 @@ All contributions are welcome! For example,
  - Vue front-end,
  - Express back-end.
 
  - 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.
 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:
 }
 
 // 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'));
 
 // 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')));
 
 }));
 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);
 
 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 (
 -- 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
 );
 
        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 (
 create table Problems (
-       num integer primary key,
+       id integer primary key,
        added datetime,
        added datetime,
-       variant varchar,
+       uid integer,
+       vid integer,
        fen varchar,
        instructions text,
        solution text,
        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
 
 -- 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'),
        ('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;
                        );
                        // 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',
                                (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 = [];
                                                _.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(
                                                                || 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,
                                                                                '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],
                                                                                        && !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;
        },
        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 = () => {
 //             };
 
 //             const socketOpenListener = () => {
 //             };
 
@@ -95,7 +95,7 @@ Vue.component('my-game', {
                        switch (data.code)
                        {
                                case "newmove": //..he played!
                        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)
                                        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 = () => {
                };
 
                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);
                        //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)
 
 
                // 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;
                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(() => {
                        // 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)
                                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;
                                }
                        }
                                        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 => {
                                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
                },
                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,
                                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 (mode=="human" && !oppId)
                        {
                                const storageVariant = localStorage.getItem("variant");
-                               if (!!storageVariant && storageVariant !== variant
+                               if (!!storageVariant && storageVariant !== variant.name
                                        && localStorage["score"] == "*")
                                {
                                        return alert(translations["Finish your "] +
                                        && 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)
                                {
                                        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"]))
                                        {
                                                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)
                                {
                                        const score = localStorage.getItem(prefix+"score");
-                                       if (storageVariant !== variant && score == "*")
+                                       if (storageVariant !== variant.name && score == "*")
                                        {
                                                if (!confirm(storageVariant +
                                                        translations[": current analysis will be erased"]))
                                        {
                                                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)
        `,
        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);
                        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];
                        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]);
                        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)
 // 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 !
 
 // 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();
 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) {
 
 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();
 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"});
 
 // 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 subject = req.body["subject"];
-       const content = req.body["content"];
+       const body = req.body["body"];
        // TODO: sanitize ?
        // 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
                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();
 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;
 
 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"});
        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, /-]*$/))
        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)
        if (instructions.length == 0)
-               return res.json({errmsg: "Empty instructions"});
+               return "Empty instructions";
        if (solution.length == 0)
        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;
 
 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');
 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"];
 
 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)
                });
        });
 });
 
 // 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);
        const lang = selectLanguage(req, res);
-       res.render("rules/" + req.params["variant"] + "/" + lang);
+       res.render("rules/" + req.params["vname"] + "/" + lang);
 });
 
 module.exports = router;
 });
 
 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
                                include translations/es
                        when "fr"
                                include translations/fr
+               if !user
+                       include login_register
+               else
+                       include logout_update
                include contactForm
                include settings
                main
                include contactForm
                include settings
                main
@@ -54,4 +58,5 @@ html
                        script(src="https://cdn.jsdelivr.net/npm/vue")
                script.
                        const translations = !{JSON.stringify(translations)};
                        script(src="https://cdn.jsdelivr.net/npm/vue")
                script.
                        const translations = !{JSON.stringify(translations)};
+                       const user = !{JSON.stringify(user)};
                block javascripts
                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/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
        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")
        script(src="/javascripts/components/room.js")
        script(src="/javascripts/components/gameList.js")
        script(src="/javascripts/components/rules.js")