# Various files
/db/vchess.sqlite
-/utils/sendEmail.js
+/utils/mailer.js
/public/javascripts/socket_url.js
# CSS generated files
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,
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
- 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.
}
// 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'));
}));
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);
-- 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;
-- 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'),
--- /dev/null
+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});
+ }
+ });
+ });
+}
--- /dev/null
+//TODO: at least this model (maybe MoveModel ?!)
--- /dev/null
+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);
+ });
+}
--- /dev/null
+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);
+ });
+}
--- /dev/null
+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
);
// 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',
_.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],
},
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 = () => {
// };
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)
};
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);
// 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;
// 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)
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 => {
},
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,
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 "] +
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 (!!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"]))
`,
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);
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]);
// 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;
+// },
},
});
-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 !
--- /dev/null
+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;
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),
});
});
});
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
--- /dev/null
+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;
+// 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;
--- /dev/null
+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;
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;
--- /dev/null
+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;
--- /dev/null
+const sqlite3 = require('sqlite3');
+const DbPath = __dirname.replace("/utils", "/db/vchess.sqlite");
+const db = new sqlite3.Database(DbPath);
+
+module.exports = db;
--- /dev/null
+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
+};
+++ /dev/null
-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();
- });
-};
include translations/es
when "fr"
include translations/fr
+ if !user
+ include login_register
+ else
+ include logout_update
include contactForm
include settings
main
script(src="https://cdn.jsdelivr.net/npm/vue")
script.
const translations = !{JSON.stringify(translations)};
+ const user = !{JSON.stringify(user)};
block javascripts
--- /dev/null
+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")
--- /dev/null
+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
+ 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 & 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")
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")