From 0234201fb338fc239d6f613c677fa932c7c3697c Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Mon, 16 Mar 2020 03:40:33 +0100 Subject: [PATCH] Refactor models (merge Players in Games), add cursor to correspondance games. Finished but not working yet --- client/src/components/GameList.vue | 7 +- client/src/views/Analyse.vue | 2 +- client/src/views/Game.vue | 19 +- client/src/views/Hall.vue | 89 +++++-- client/src/views/MyGames.vue | 88 +++++-- client/src/views/News.vue | 13 +- server/app.js | 11 +- server/config/parameters.js.dist | 3 +- server/db/create.sql | 17 +- server/models/Challenge.js | 24 +- server/models/Game.js | 370 +++++++++++++++++------------ server/models/News.js | 5 +- server/models/Problem.js | 23 +- server/models/User.js | 57 ++--- server/models/Variant.js | 6 +- server/routes/challenges.js | 28 +-- server/routes/games.js | 95 ++++---- server/routes/news.js | 11 +- server/routes/problems.js | 22 +- server/routes/users.js | 36 +-- server/sockets.js | 2 +- server/utils/access.js | 37 ++- server/utils/database.js | 3 +- server/utils/mailer.js | 13 +- server/utils/tokenGenerator.js | 11 +- 25 files changed, 550 insertions(+), 442 deletions(-) diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue index 1a50e956..549e2a1d 100644 --- a/client/src/components/GameList.vue +++ b/client/src/components/GameList.vue @@ -62,7 +62,7 @@ export default { ); if ( this.st.user.sid == g.players[0].sid || - this.st.user.id == g.players[0].uid + this.st.user.id == g.players[0].id ) return g.players[1].name || "@nonymous"; return g.players[0].name || "@nonymous"; @@ -113,8 +113,7 @@ export default { if ( // My game ? game.players.some(p => - p.sid == this.st.user.sid || - p.uid == this.st.user.id + p.sid == this.st.user.sid || p.id == this.st.user.id ) ) { const message = @@ -131,7 +130,7 @@ export default { GameStorage.remove(game.id, afterDelete); else { const mySide = - game.players[0].uid == this.st.user.id + game.players[0].id == this.st.user.id ? "White" : "Black"; game["deletedBy" + mySide] = true; diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue index f662d614..becc4de0 100644 --- a/client/src/views/Analyse.vue +++ b/client/src/views/Analyse.vue @@ -80,10 +80,10 @@ export default { .catch((err) => { this.alertAndQuit("Mispelled variant name", true); }); }, loadGame: function(orientation) { - // NOTE: no need to set score (~unused) this.game.vname = this.gameRef.vname; this.game.fenStart = this.gameRef.fen; this.game.fen = this.gameRef.fen; + this.game.score = "*"; //never change this.curFen = this.game.fen; this.adjustFenSize(); this.game.mycolor = orientation || V.ParseFen(this.gameRef.fen).turn; diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index e7db9729..a401e464 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -352,7 +352,7 @@ export default { isConnected: function(index) { const player = this.game.players[index]; // Is it me ? In this case no need to bother with focus - if (this.st.user.sid == player.sid || this.st.user.id == player.uid) + if (this.st.user.sid == player.sid || this.st.user.id == player.id) // Still have to check for name (because of potential multi-accounts // on same browser, although this should be rare...) return (!this.st.user.name || this.st.user.name == player.name); @@ -365,9 +365,9 @@ export default { ) || ( - player.uid && + player.id && Object.values(this.people).some(p => - p.id == player.uid && p.focus) + p.id == player.id && p.focus) ) ); }, @@ -415,7 +415,7 @@ export default { { data: data, targets: this.game.players.map(p => { - return { sid: p.sid, uid: p.uid }; + return { sid: p.sid, id: p.id }; }) } ); @@ -693,7 +693,7 @@ export default { this.addAndGotoLiveGame(gameInfo); } else if ( gameType == "corr" && - gameInfo.players.some(p => p.uid == this.st.user.id) + gameInfo.players.some(p => p.id == this.st.user.id) ) { this.$router.push("/game/" + gameInfo.id); } else { @@ -918,7 +918,7 @@ export default { const gtype = this.getGameType(game); const tc = extractTime(game.cadence); const myIdx = game.players.findIndex(p => { - return p.sid == this.st.user.sid || p.uid == this.st.user.id; + return p.sid == this.st.user.sid || p.id == this.st.user.id; }); const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers if (!game.chats) game.chats = []; //live games don't have chat history @@ -1031,7 +1031,7 @@ export default { // opponent sid not strictly required (or available), but easier // at least oppsid or oppid is available anyway: oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid, - oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].uid + oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].id }, game, ); @@ -1085,6 +1085,9 @@ export default { g.moves.forEach(m => { m.squares = JSON.parse(m.squares); }); + g.players = [{ id: g.white }, { id: g.black }]; + delete g["white"]; + delete g["black"]; afterRetrieval(g); } } @@ -1366,7 +1369,7 @@ export default { this.game.scoreMsg = scoreMsg; this.$set(this.game, "scoreMsg", scoreMsg); const myIdx = this.game.players.findIndex(p => { - return p.sid == this.st.user.sid || p.uid == this.st.user.id; + return p.sid == this.st.user.sid || p.id == this.st.user.id; }); if (myIdx >= 0) { // OK, I play in this game diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index f9519fe2..b9262d12 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -185,12 +185,17 @@ main :showBoth="true" @show-game="showGame" ) - GameList( - v-show="gdisplay=='corr'" - :games="filterGames('corr')" - :showBoth="true" - @show-game="showGame" - ) + div(v-show="gdisplay=='corr'") + GameList( + :games="filterGames('corr')" + :showBoth="true" + @show-game="showGame" + ) + button#loadMoreBtn( + v-if="hasMore" + @click="loadMore()" + ) + | {{ st.tr["Load more"] }} </template> <script> @@ -218,6 +223,10 @@ export default { st: store.state, cdisplay: "live", //or corr gdisplay: "live", + // timestamp of last showed (oldest) corr game: + cursor: Number.MAX_SAFE_INTEGER, + // hasMore == TRUE: a priori there could be more games to load + hasMore: true, games: [], challenges: [], people: {}, @@ -321,10 +330,13 @@ export default { this.setDisplay('g', showGtype); // Ask server for current corr games (all but mines) ajax( - "/games", + "/observedgames", "GET", { - data: { uid: this.st.user.id, excluded: true }, + data: { + uid: this.st.user.id, + cursor: this.cursor + }, success: (response) => { if ( response.games.length > 0 && @@ -337,13 +349,12 @@ export default { } this.games = this.games.concat( response.games.map(g => { - const type = this.classifyObject(g); const vname = this.getVname(g.vid); return Object.assign( {}, g, { - type: type, + type: "corr", vname: vname } ); @@ -766,7 +777,7 @@ export default { const game = data.data; // Ignore games where I play (will go in MyGames page) if (game.players.every(p => - p.sid != this.st.user.sid && p.uid != this.st.user.id)) + p.sid != this.st.user.sid && p.id != this.st.user.id)) { let locGame = this.games.find(g => g.id == game.id); if (!locGame) { @@ -830,6 +841,36 @@ export default { this.conn.addEventListener("message", this.socketMessageListener); this.conn.addEventListener("close", this.socketCloseListener); }, + loadMore: function() { + ajax( + "/observedgames", + "GET", + { + data: { + uid: this.st.user.id, + cursor: this.cursor + }, + success: (res) => { + if (res.games.length > 0) { + const L = res.games.length; + this.cursor = res.games[L - 1].created; + let moreGames = res.games.map(g => { + const vname = this.getVname(g.vid); + return Object.assign( + {}, + g, + { + type: "corr", + vname: vname + } + ); + }); + this.games = this.games.concat(moreGames); + } else this.hasMore = false; + } + } + ); + }, // Challenge lifecycle: addChallenge: function(chall) { // NOTE about next condition: see "askchallenges" case. @@ -1098,16 +1139,11 @@ export default { !!c.mycolor ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat]) : shuffle([c.from, c.seat]); - // Convention for players IDs in stored games is 'uid' - players.forEach(p => { - let pWithUid = p; - pWithUid["uid"] = p.id; - delete pWithUid["id"]; - }); // These game informations will be shared let gameInfo = { id: getRandString(), fen: c.fen || V.GenRandInitFen(c.randomness), + randomness: c.randomness, //for rematch // White player index 0, black player index 1: players: c.mycolor ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat]) @@ -1126,7 +1162,7 @@ export default { { data: gameInfo, targets: gameInfo.players.map(p => { - return { sid: p.sid, uid: p.uid }; + return { sid: p.sid, id: p.id }; }) } ); @@ -1174,13 +1210,12 @@ export default { () => { GameStorage.add(game, (err) => { // If an error occurred, game is not added: a tab already - // added the game and (if focused) is redirected toward it. - // If no error and the tab is hidden: do not show anything. - if (!err && !document.hidden) { - if (this.st.settings.sound) - new Audio("/sounds/newgame.flac").play().catch(() => {}); - this.$router.push("/game/" + gameInfo.id); - } + // added the game. Maybe a focused one, maybe not. + // We know for sure that it emitted the gong start sound. + // ==> Do not play it again. + if (!err && this.st.settings.sound) + new Audio("/sounds/newgame.flac").play().catch(() => {}); + this.$router.push("/game/" + gameInfo.id); }); }, document.hidden ? 500 + 1000 * Math.random() : 0 @@ -1292,6 +1327,10 @@ tr > td h4 margin: 5px 0 +button#loadMoreBtn + margin-top: 0 + margin-bottom: 0 + td.remove-preset background-color: lightgrey text-align: center diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue index 1362cb24..628d72eb 100644 --- a/client/src/views/MyGames.vue +++ b/client/src/views/MyGames.vue @@ -14,13 +14,18 @@ main @show-game="showGame" @abortgame="abortGame" ) - GameList( - ref="corrgames" - v-show="display=='corr'" - :games="corrGames" - @show-game="showGame" - @abortgame="abortGame" - ) + div(v-show="display=='corr'") + GameList( + ref="corrgames" + :games="corrGames" + @show-game="showGame" + @abortgame="abortGame" + ) + button#loadMoreBtn( + v-if="hasMore" + @click="loadMore()" + ) + | {{ st.tr["Load more"] }} </template> <script> @@ -42,6 +47,10 @@ export default { display: "live", liveGames: [], corrGames: [], + // timestamp of last showed (oldest) corr game: + cursor: Number.MAX_SAFE_INTEGER, + // hasMore == TRUE: a priori there could be more games to load + hasMore: true, conn: null, connexionString: "" }; @@ -89,23 +98,35 @@ export default { this.decorate(localGames); this.liveGames = localGames; if (this.st.user.id > 0) { + // Ask running corr games first ajax( - "/games", + "/runninggames", "GET", { - data: { uid: this.st.user.id }, success: (res) => { - let serverGames = res.games.filter(g => { - const mySide = - g.players[0].uid == this.st.user.id - ? "White" - : "Black"; - return !g["deletedBy" + mySide]; - }); - serverGames.forEach(g => g.type = "corr"); - this.decorate(serverGames); - this.corrGames = serverGames; - adjustAndSetDisplay(); + // These games are garanteed to not be deleted + this.corrGames = res.games; + this.corrGames.forEach(g => g.type = "corr"); + this.decorate(this.corrGames); + // Now ask completed games (partial list) + ajax( + "/completedgames", + "GET", + { + data: { cursor: this.cursor }, + success: (res2) => { + if (res2.games.length > 0) { + const L = res2.games.length; + this.cursor = res2.games[L - 1].created; + let completedGames = res2.games; + completedGames.forEach(g => g.type = "corr"); + this.decorate(completedGames); + this.corrGames = this.corrGames.concat(completedGames); + adjustAndSetDisplay(); + } + } + } + ); } } ); @@ -140,7 +161,7 @@ export default { decorate: function(games) { games.forEach(g => { g.myColor = - (g.type == "corr" && g.players[0].uid == this.st.user.id) || + (g.type == "corr" && g.players[0].id == this.st.user.id) || (g.type == "live" && g.players[0].sid == this.st.user.sid) ? 'w' : 'b'; @@ -193,7 +214,7 @@ export default { gameInfo ); game.myTurn = - (type == "corr" && game.players[0].uid == this.st.user.id) || + (type == "corr" && game.players[0].id == this.st.user.id) || (type == "live" && game.players[0].sid == this.st.user.sid); gamesArrays[type].push(game); if (game.myTurn) this.tryShowNewsIndicator(type); @@ -260,6 +281,25 @@ export default { } ); } + }, + loadMore: function() { + ajax( + "/completedgames", + "GET", + { + data: { cursor: this.cursor }, + success: (res) => { + if (res.games.length > 0) { + const L = res.games.length; + this.cursor = res.games[L - 1].created; + let moreGames = res.games; + moreGames.forEach(g => g.type = "corr"); + this.decorate(moreGames); + this.corrGames = this.corrGames.concat(moreGames); + } else this.hasMore = false; + } + } + ); } } }; @@ -275,6 +315,10 @@ export default { table.game-list max-height: 100% +button#loadMoreBtn + margin-top: 0 + margin-bottom: 0 + .somethingnew background-color: #c5fefe !important </style> diff --git a/client/src/views/News.vue b/client/src/views/News.vue index ec1517df..0e3c3c7a 100644 --- a/client/src/views/News.vue +++ b/client/src/views/News.vue @@ -48,8 +48,10 @@ export default { return { devs: [1], //for now the only dev is me st: store.state, - cursor: 0, //ID of last showed news - hasMore: true, //a priori there could be more news to load + // timestamp of oldest showed news: + cursor: Number.MAX_SAFE_INTEGER, + // hasMore == TRUE: a priori there could be more news to load + hasMore: true, curnews: { id: 0, content: "" }, newsList: [], infoMsg: "" @@ -62,9 +64,10 @@ export default { { data: { cursor: this.cursor }, success: (res) => { - this.newsList = res.newsList.sort((n1, n2) => n2.added - n1.added); + // The returned list is sorted from most recent to oldest + this.newsList = res.newsList; const L = res.newsList.length; - if (L > 0) this.cursor = this.newsList[0].id; + if (L > 0) this.cursor = res.newsList[L - 1].added; } } ); @@ -169,7 +172,7 @@ export default { if (res.newsList.length > 0) { this.newsList = this.newsList.concat(res.newsList); const L = res.newsList.length; - if (L > 0) this.cursor = res.newsList[L - 1].id; + if (L > 0) this.cursor = res.newsList[L - 1].added; } else this.hasMore = false; } } diff --git a/server/app.js b/server/app.js index 785088b3..df15fa20 100644 --- a/server/app.js +++ b/server/app.js @@ -11,12 +11,9 @@ let app = express(); app.use(favicon(path.join(__dirname, "static", "favicon.ico"))); if (app.get('env') === 'development') -{ // Full logging in development mode app.use(logger('dev')); -} -else -{ +else { // http://dev.rdybarra.com/2016/06/23/Production-Logging-With-Morgan-In-Express/ app.set('trust proxy', true); // In prod, only log error responses (https://github.com/expressjs/morgan) @@ -34,8 +31,7 @@ app.use(express.static(path.join(__dirname, 'static'))); app.use(express.static(path.join(__dirname, 'fallback'))); // In development stage the client side has its own server -if (params.cors.enable) -{ +if (params.cors.enable) { app.use(function(req, res, next) { res.header("Access-Control-Allow-Origin", params.cors.allowedOrigin); res.header("Access-Control-Allow-Credentials", true); //for cookies @@ -63,8 +59,7 @@ app.use(function(req, res, next) { // Error handler app.use(function(err, req, res, next) { res.status(err.status || 500); - if (app.get('env') === 'development') - console.log(err.stack); + if (app.get('env') === 'development') console.log(err.stack); res.send( "<h1>" + err.message + "</h1>" + "<h2>" + err.status + "</h2>" diff --git a/server/config/parameters.js.dist b/server/config/parameters.js.dist index 264fac50..c1908411 100644 --- a/server/config/parameters.js.dist +++ b/server/config/parameters.js.dist @@ -1,5 +1,4 @@ -module.exports = -{ +module.exports = { // For mail sending. NOTE: *no trailing slash* siteURL: "http://localhost:8080", diff --git a/server/db/create.sql b/server/db/create.sql index 5aba8ff0..533e0f24 100644 --- a/server/db/create.sql +++ b/server/db/create.sql @@ -56,6 +56,8 @@ create table Games ( vid integer, fenStart varchar, --initial state fen varchar, --current state + white integer, + black integer, score varchar default '*', scoreMsg varchar, cadence varchar, @@ -65,7 +67,9 @@ create table Games ( rematchOffer character default '', deletedByWhite boolean, deletedByBlack boolean, - foreign key (vid) references Variants(id) + foreign key (vid) references Variants(id), + foreign key (white) references Users(id), + foreign key (black) references Users(id) ); create table Chats ( @@ -75,15 +79,6 @@ create table Chats ( added datetime ); --- Store informations about players in a corr game -create table Players ( - gid integer, - uid integer, - color character, - foreign key (gid) references Games(id), - foreign key (uid) references Users(id) -); - create table Moves ( gid integer, squares varchar, --description, appear/vanish/from/to @@ -92,4 +87,6 @@ create table Moves ( foreign key (gid) references Games(id) ); +create index scoreIdx on Games(score); + pragma foreign_keys = on; diff --git a/server/models/Challenge.js b/server/models/Challenge.js index 2be700bd..dea0ac39 100644 --- a/server/models/Challenge.js +++ b/server/models/Challenge.js @@ -13,21 +13,18 @@ const UserModel = require("./User"); * cadence: string (3m+2s, 7d ...) */ -const ChallengeModel = -{ - checkChallenge: function(c) - { +const ChallengeModel = { + checkChallenge: function(c) { return ( c.vid.toString().match(/^[0-9]+$/) && c.cadence.match(/^[0-9dhms +]+$/) && c.randomness.toString().match(/^[0-2]$/) && c.fen.match(/^[a-zA-Z0-9, /-]*$/) && - (!c.to || UserModel.checkNameEmail({name: c.to})) + (!c.to || UserModel.checkNameEmail({ name: c.to })) ); }, - create: function(c, cb) - { + create: function(c, cb) { db.serialize(function() { const query = "INSERT INTO Challenges " + @@ -37,14 +34,13 @@ const ChallengeModel = "(" + Date.now() + "," + c.uid + "," + (c.to ? c.to + "," : "") + c.vid + "," + c.randomness + ",'" + c.fen + "','" + c.cadence + "')"; db.run(query, function(err) { - cb(err, {cid: this.lastID}); + cb(err, { id: this.lastID }); }); }); }, // All challenges related to user with ID uid - getByUser: function(uid, cb) - { + getByUser: function(uid, cb) { db.serialize(function() { const query = "SELECT * " + @@ -52,14 +48,13 @@ const ChallengeModel = "WHERE target IS NULL" + " OR uid = " + uid + " OR target = " + uid; - db.all(query, (err,challenges) => { + db.all(query, (err, challenges) => { cb(err, challenges); }); }); }, - remove: function(id) - { + remove: function(id) { db.serialize(function() { const query = "DELETE FROM Challenges " + @@ -68,8 +63,7 @@ const ChallengeModel = }); }, - safeRemove: function(id, uid) - { + safeRemove: function(id, uid) { db.serialize(function() { const query = "SELECT 1 " + diff --git a/server/models/Game.js b/server/models/Game.js index caa15c39..65aedfde 100644 --- a/server/models/Game.js +++ b/server/models/Game.js @@ -7,16 +7,17 @@ const UserModel = require("./User"); * vid: integer (variant id) * fenStart: varchar (initial position) * fen: varchar (current position) + * white: integer + * black: integer * cadence: string * score: varchar (result) * scoreMsg: varchar ("Time", "Mutual agreement"...) * created: datetime * drawOffer: char ('w','b' or '' for none) - * - * Structure table Players: - * gid: ref game id - * uid: ref user id - * color: character + * rematchOffer: char (similar to drawOffer) + * randomness: integer + * deletedByWhite: boolean + * deletedByBlack: boolean * * Structure table Moves: * gid: ref game id @@ -37,174 +38,234 @@ const GameModel = return ( g.vid.toString().match(/^[0-9]+$/) && g.cadence.match(/^[0-9dhms +]+$/) && + g.randomness.match(/^[0-2]$/) && g.fen.match(/^[a-zA-Z0-9, /-]*$/) && g.players.length == 2 && - g.players.every(p => p.uid.toString().match(/^[0-9]+$/)) + g.players.every(p => p.toString().match(/^[0-9]+$/)) ); }, - create: function(vid, fen, cadence, players, cb) - { + create: function(vid, fen, randomness, cadence, players, cb) { db.serialize(function() { let query = "INSERT INTO Games " + - "(vid, fenStart, fen, cadence, created) " + + "(" + + "vid, fenStart, fen, randomness, " + + "white, black, " + + "cadence, created" + + ") " + "VALUES " + - "(" + vid + ",'" + fen + "','" + fen + "','" + cadence + "'," + Date.now() + ")"; + "(" + + vid + ",'" + fen + "','" + fen + "'," + randomness + "," + + "'" + players[0] + "','" + players[1] + "," + + "'" + cadence + "'," + Date.now() + + ")"; db.run(query, function(err) { - if (err) - cb(err) - else - { - players.forEach((p,idx) => { - const color = (idx==0 ? "w" : "b"); - query = - "INSERT INTO Players VALUES " + - "(" + this.lastID + "," + p.uid + ",'" + color + "')"; - db.run(query); - }); - cb(null, {gid: this.lastID}); - } + cb(err, { id: this.lastId }); }); }); }, // TODO: some queries here could be async - getOne: function(id, cb) - { + getOne: function(id, cb) { // NOTE: ignoring errors (shouldn't happen at this stage) db.serialize(function() { let query = - "SELECT g.id, g.vid, g.fen, g.fenStart, g.cadence, g.created, g.score, " + - "g.scoreMsg, g.drawOffer, g.rematchOffer, v.name AS vname " + + "SELECT " + + "g.id, g.vid, g.fen, g.fenStart, g.cadence, g.created, " + + "g.white, g.black, g.score, g.scoreMsg, " + + "g.drawOffer, g.rematchOffer, v.name AS vname " + "FROM Games g " + "JOIN Variants v " + " ON g.vid = v.id " + "WHERE g.id = " + id; db.get(query, (err, gameInfo) => { query = - "SELECT p.uid, p.color, u.name " + - "FROM Players p " + - "JOIN Users u " + - " ON p.uid = u.id " + - "WHERE p.gid = " + id; - db.all(query, (err2, players) => { + "SELECT squares, played, idx " + + "FROM Moves " + + "WHERE gid = " + id; + db.all(query, (err3, moves) => { query = - "SELECT squares, played, idx " + - "FROM Moves " + + "SELECT msg, name, added " + + "FROM Chats " + "WHERE gid = " + id; - db.all(query, (err3, moves) => { - query = - "SELECT msg, name, added " + - "FROM Chats " + - "WHERE gid = " + id; - db.all(query, (err4, chats) => { - const game = Object.assign( - {}, - gameInfo, - { - players: players, - moves: moves, - chats: chats, - } - ); - cb(null, game); - }); + db.all(query, (err4, chats) => { + const game = Object.assign( + {}, + gameInfo, + { + moves: moves, + chats: chats + } + ); + cb(null, game); }); }); }); }); }, - // For display on MyGames or Hall: no need for moves or chats - getByUser: function(uid, excluded, cb) - { - // Some fields are not required when showing a games list: - const getOneLight = (id, cb2) => { + // For display on Hall: no need for moves or chats + getObserved: function(uid, cursor, cb) { + db.serialize(function() { + let query = + "SELECT g.id, g.vid, g.cadence, g.created, " + + " g.score, g.white, g.black " + + "FROM Games g "; + if (uid > 0) query += + "WHERE " + + " created < " + cursor + " AND " + + " white <> " + uid + " AND " + + " black <> " + uid + " "; + query += + "ORDER BY created DESC " + + "LIMIT 20"; //TODO: 20 hard-coded... + db.all(query, (err, games) => { + // Query players names + let pids = {}; + games.forEach(g => { + if (!pids[g.white]) pids[g.white] = true; + if (!pids[g.black]) pids[g.black] = true; + }); + UserModel.getByIds(Object.keys(pids), (err2, users) => { + let names = {}; + users.forEach(u => { names[u.id] = u.name; }); + cb( + games.map( + g => { + return { + id: g.id, + cadence: g.cadence, + vname: g.vname, + created: g.created, + score: g.score, + white: names[g.white], + black: names[g.black] + }; + } + ) + ); + }); + }); + }); + }, + + // For display on MyGames: registered user only + getRunning: function(uid, cb) { + db.serialize(function() { let query = - "SELECT g.id, g.vid, g.fen, g.cadence, g.created, g.score, " + - "g.scoreMsg, g.deletedByWhite, g.deletedByBlack, v.name AS vname " + + "SELECT g.id, g.cadence, g.created, g.score, " + + "g.white, g.black, v.name AS vname " + "FROM Games g " + "JOIN Variants v " + " ON g.vid = v.id " + - "WHERE g.id = " + id; - db.get(query, (err, gameInfo) => { + "WHERE white = " + uid + " OR black = " + uid; + db.all(query, (err, games) => { + // Get movesCount (could be done in // with next query) query = - "SELECT p.uid, p.color, u.name " + - "FROM Players p " + - "JOIN Users u " + - " ON p.uid = u.id " + - "WHERE p.gid = " + id; - db.all(query, (err2, players) => { - query = - "SELECT COUNT(*) AS nbMoves " + - "FROM Moves " + - "WHERE gid = " + id; - db.get(query, (err,ret) => { - const game = Object.assign( - {}, - gameInfo, - { - players: players, - movesCount: ret.nbMoves - } + "SELECT gid, COUNT(*) AS nbMoves " + + "FROM Moves " + + "WHERE gid IN " + "(" + games.map(g => g.id).join(",") + ") " + + "GROUP BY gid"; + db.all(query, (err, mstats) => { + let movesCounts = {}; + mstats.forEach(ms => { movesCounts[ms.gid] = ms.nbMoves; }); + // Query player names + let pids = {}; + games.forEach(g => { + if (!pids[g.white]) pids[g.white] = true; + if (!pids[g.black]) pids[g.black] = true; + }); + UserModel.getByIds(pids, (err2, users) => { + let names = {}; + users.forEach(u => { names[u.id] = u.name; }); + cb( + games.map( + g => { + return { + id: g.id, + cadence: g.cadence, + vname: g.vname, + created: g.created, + score: g.score, + movesCount: movesCounts[g.id], + white: names[g.white], + black: names[g.black] + }; + } + ) ); - cb2(game); }); }); }); - }; + }); + }, + + // These games could be deleted on some side. movesCount not required + getCompleted: function(uid, cursor, cb) { db.serialize(function() { - let query = ""; - if (uid == 0) { - // Special case anonymous user: show all games - query = - "SELECT id AS gid " + - "FROM Games"; - } - else { - // Registered user: - query = - "SELECT gid " + - "FROM Players " + - "GROUP BY gid " + - "HAVING COUNT(uid = " + uid + " OR NULL) " + - (excluded ? " = 0" : " > 0"); - } - db.all(query, (err,gameIds) => { - if (err || gameIds.length == 0) cb(err, []); - else { - let gameArray = []; - let gCounter = 0; - for (let i=0; i<gameIds.length; i++) { - getOneLight(gameIds[i]["gid"], (game) => { - gameArray.push(game); - gCounter++; //TODO: let's hope this is atomic?! - // Call callback function only when gameArray is complete: - if (gCounter == gameIds.length) - cb(null, gameArray); - }); - } - } + let query = + "SELECT g.id, g.cadence, g.created, g.score, g.scoreMsg, " + + "g.white, g.black, g.deletedByWhite, g.deletedByBlack, " + + "v.name AS vname " + + "FROM Games g " + + "JOIN Variants v " + + " ON g.vid = v.id " + + "WHERE " + + " created < " + cursor + " AND " + + " (" + + " (" + uid + " = white AND NOT deletedByWhite) OR " + + " (" + uid + " = black AND NOT deletedByBlack)" + + " ) "; + query += + "ORDER BY created DESC " + + "LIMIT 20"; + db.all(query, (err, games) => { + // Query player names + let pids = {}; + games.forEach(g => { + if (!pids[g.white]) pids[g.white] = true; + if (!pids[g.black]) pids[g.black] = true; + }); + UserModel.getByIds(pids, (err2, users) => { + let names = {}; + users.forEach(u => { names[u.id] = u.name; }); + cb( + games.map( + g => { + return { + id: g.id, + cadence: g.cadence, + vname: g.vname, + created: g.created, + score: g.score, + scoreMsg: g.scoreMsg, + white: names[g.white], + black: names[g.black], + deletedByWhite: g.deletedByWhite, + deletedByBlack: g.deletedByBlack + }; + } + ) + ); + }); }); }); }, - getPlayers: function(id, cb) - { + getPlayers: function(id, cb) { db.serialize(function() { const query = - "SELECT uid " + - "FROM Players " + + "SELECT white, black " + + "FROM Games " + "WHERE gid = " + id; - db.all(query, (err,players) => { + db.all(query, (err, players) => { return cb(err, players); }); }); }, - checkGameUpdate: function(obj) - { + checkGameUpdate: function(obj) { // Check all that is possible (required) in obj: return ( ( @@ -229,8 +290,7 @@ const GameModel = }, // obj can have fields move, chat, fen, drawOffer and/or score + message - update: function(id, obj, cb) - { + update: function(id, obj, cb) { db.parallelize(function() { let query = "UPDATE Games " + @@ -320,56 +380,72 @@ const GameModel = }); }, - remove: function(id) - { + remove: function(id_s) { + const suffix = + Array.isArray(id_s) + ? " IN (" + id_s.join(",") + ")" + : " = " + id_s; db.parallelize(function() { let query = "DELETE FROM Games " + - "WHERE id = " + id; - db.run(query); - query = - "DELETE FROM Players " + - "WHERE gid = " + id; + "WHERE id " + suffix; db.run(query); query = "DELETE FROM Moves " + - "WHERE gid = " + id; + "WHERE gid " + suffix; db.run(query); query = "DELETE FROM Chats " + - "WHERE gid = " + id; + "WHERE gid " + suffix; db.run(query); }); }, - cleanGamesDb: function() - { + cleanGamesDb: function() { const tsNow = Date.now(); // 86400000 = 24 hours in milliseconds const day = 86400000; db.serialize(function() { let query = "SELECT id, created " + - "FROM Games "; - db.all(query, (err,games) => { - games.forEach(g => { - query = - "SELECT count(*) as nbMoves, max(played) AS lastMaj " + - "FROM Moves " + - "WHERE gid = " + g.id; - db.get(query, (err2,mstats) => { - // Remove games still not really started, - // with no action in the last 3 months: - if ((mstats.nbMoves == 0 && tsNow - g.created > 91*day) || - (mstats.nbMoves == 1 && tsNow - mstats.lastMaj > 91*day)) - { - GameModel.remove(g.id); + "FROM Games"; + db.all(query, (err, games) => { + query = + "SELECT gid, count(*) AS nbMoves, MAX(played) AS lastMaj " + + "FROM Moves " + + "GROUP BY gid"; + db.get(query, (err2, mstats) => { + // Reorganize moves data to avoid too many array lookups: + let movesGroups = {}; + mstats.forEach(ms => { + movesGroups[ms.gid] = { + nbMoves: ms.nbMoves, + lastMaj: ms.lastMaj + }; + }); + // Remove games still not really started, + // with no action in the last 3 months: + let toRemove = []; + games.forEach(g => { + if ( + ( + !movesGroups[g.id] && + tsNow - g.created > 91*day + ) + || + ( + movesGroups[g.id].nbMoves == 1 && + tsNow - movesGroups[g.id].lastMaj > 91*day + ) + ) { + toRemove.push(g.id); } }); + if (toRemove.length > 0) GameModel.remove(toRemove); }); }); }); - }, + } } module.exports = GameModel; diff --git a/server/models/News.js b/server/models/News.js index 3fa3caae..2dde566f 100644 --- a/server/models/News.js +++ b/server/models/News.js @@ -19,7 +19,7 @@ const NewsModel = "VALUES " + "(" + Date.now() + "," + uid + ",?)"; db.run(query, content, function(err) { - cb(err, {nid: this.lastID}); + cb(err, { id: this.lastID }); }); }); }, @@ -30,7 +30,8 @@ const NewsModel = const query = "SELECT * " + "FROM News " + - "WHERE id > " + cursor + " " + + "WHERE added < " + cursor + " " + + "ORDER BY added DESC " + "LIMIT 10"; //TODO: 10 currently hard-coded db.all(query, (err,newsList) => { cb(err, newsList); diff --git a/server/models/Problem.js b/server/models/Problem.js index 136fb649..5c9af0b1 100644 --- a/server/models/Problem.js +++ b/server/models/Problem.js @@ -11,10 +11,8 @@ const db = require("../utils/database"); * solution: text */ -const ProblemModel = -{ - checkProblem: function(p) - { +const ProblemModel = { + checkProblem: function(p) { return ( p.id.toString().match(/^[0-9]+$/) && p.vid.toString().match(/^[0-9]+$/) && @@ -22,8 +20,7 @@ const ProblemModel = ); }, - create: function(p, cb) - { + create: function(p, cb) { db.serialize(function() { const query = "INSERT INTO Problems " + @@ -31,13 +28,12 @@ const ProblemModel = "VALUES " + "(" + Date.now() + "," + p.uid + "," + p.vid + ",'" + p.fen + "',?,?)"; db.run(query, [p.instruction,p.solution], function(err) { - cb(err, {pid: this.lastID}); + cb(err, { id: this.lastID }); }); }); }, - getAll: function(cb) - { + getAll: function(cb) { db.serialize(function() { const query = "SELECT * " + @@ -48,8 +44,7 @@ const ProblemModel = }); }, - getOne: function(id, cb) - { + getOne: function(id, cb) { db.serialize(function() { const query = "SELECT * " + @@ -61,8 +56,7 @@ const ProblemModel = }); }, - safeUpdate: function(prob, uid) - { + safeUpdate: function(prob, uid) { db.serialize(function() { const query = "UPDATE Problems " + @@ -76,8 +70,7 @@ const ProblemModel = }); }, - safeRemove: function(id, uid) - { + safeRemove: function(id, uid) { db.serialize(function() { const query = "DELETE FROM Problems " + diff --git a/server/models/User.js b/server/models/User.js index 021cadcb..0ff8e1c8 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -13,34 +13,31 @@ const sendEmail = require('../utils/mailer'); * sessionToken: token in cookies for authentication * notify: boolean (send email notifications for corr games) * created: datetime + * newsRead: datetime */ -const UserModel = -{ - checkNameEmail: function(o) - { +const UserModel = { + checkNameEmail: function(o) { return ( (!o.name || !!(o.name.match(/^[\w-]+$/))) && (!o.email || !!(o.email.match(/^[\w.+-]+@[\w.+-]+$/))) ); }, - create: function(name, email, notify, cb) - { + create: function(name, email, notify, cb) { db.serialize(function() { const query = "INSERT INTO Users " + "(name, email, notify, created) VALUES " + "('" + name + "','" + email + "'," + notify + "," + Date.now() + ")"; db.run(query, function(err) { - cb(err, {uid: this.lastID}); + cb(err, { id: this.lastID }); }); }); }, // Find one user by id, name, email, or token - getOne: function(by, value, cb) - { + getOne: function(by, value, cb) { const delimiter = (typeof value === "string" ? "'" : ""); db.serialize(function() { const query = @@ -64,24 +61,22 @@ const UserModel = ///////// // MODIFY - setLoginToken: function(token, uid) - { + setLoginToken: function(token, id) { db.serialize(function() { const query = "UPDATE Users " + "SET loginToken = '" + token + "',loginTime = " + Date.now() + " " + - "WHERE id = " + uid; + "WHERE id = " + id; db.run(query); }); }, - setNewsRead: function(uid) - { + setNewsRead: function(id) { db.serialize(function() { const query = "UPDATE Users " + "SET newsRead = " + Date.now() + " " + - "WHERE id = " + uid; + "WHERE id = " + id; db.run(query); }); }, @@ -89,13 +84,12 @@ const UserModel = // Set session token only if empty (first login) // NOTE: weaker security (but avoid to re-login everywhere after each logout) // TODO: option would be to reset all tokens periodically, e.g. every 3 months - trySetSessionToken: function(uid, cb) - { + trySetSessionToken: function(id, cb) { db.serialize(function() { let query = "SELECT sessionToken " + "FROM Users " + - "WHERE id = " + uid; + "WHERE id = " + id; db.get(query, (err,ret) => { const token = ret.sessionToken || genToken(params.token.length); query = @@ -103,15 +97,14 @@ const UserModel = // Also empty the login token to invalidate future attempts "SET loginToken = NULL" + (!ret.sessionToken ? (", sessionToken = '" + token + "'") : "") + " " + - "WHERE id = " + uid; + "WHERE id = " + id; db.run(query); cb(token); }); }); }, - updateSettings: function(user) - { + updateSettings: function(user) { db.serialize(function() { const query = "UPDATE Users " + @@ -126,27 +119,23 @@ const UserModel = ///////////////// // NOTIFICATIONS - notify: function(user, message) - { + notify: function(user, message) { const subject = "vchess.club - notification"; const body = "Hello " + user.name + " !" + ` ` + message; sendEmail(params.mail.noreply, user.email, subject, body); }, - tryNotify: function(id, message) - { + tryNotify: function(id, message) { UserModel.getOne("id", id, (err,user) => { - if (!err && user.notify) - UserModel.notify(user, message); + if (!err && user.notify) UserModel.notify(user, message); }); }, //////////// // CLEANING - cleanUsersDb: function() - { + cleanUsersDb: function() { const tsNow = Date.now(); // 86400000 = 24 hours in milliseconds const day = 86400000; @@ -155,8 +144,9 @@ const UserModel = "SELECT id, sessionToken, created, name, email " + "FROM Users"; db.all(query, (err, users) => { + let toRemove = []; users.forEach(u => { - // Remove unlogged users for > 24h + // Remove users unlogged for > 24h if (!u.sessionToken && tsNow - u.created > day) { notify( @@ -164,9 +154,14 @@ const UserModel = "Your account has been deleted because " + "you didn't log in for 24h after registration" ); - db.run("DELETE FROM Users WHERE id = " + u.id); } }); + if (toRemove.length > 0) { + db.run( + "DELETE FROM Users " + + "WHERE id IN (" + toRemove.join(",") + ")" + ); + } }); }); }, diff --git a/server/models/Variant.js b/server/models/Variant.js index d9911071..85abf5ae 100644 --- a/server/models/Variant.js +++ b/server/models/Variant.js @@ -7,10 +7,8 @@ const db = require("../utils/database"); * description: varchar */ -const VariantModel = -{ - getAll: function(callback) - { +const VariantModel = { + getAll: function(callback) { db.serialize(function() { const query = "SELECT * " + diff --git a/server/routes/challenges.js b/server/routes/challenges.js index 680a69fd..d27f81ee 100644 --- a/server/routes/challenges.js +++ b/server/routes/challenges.js @@ -5,10 +5,8 @@ const UserModel = require("../models/User"); //for name check const params = require("../config/parameters"); router.post("/challenges", access.logged, access.ajax, (req,res) => { - if (ChallengeModel.checkChallenge(req.body.chall)) - { - let challenge = - { + if (ChallengeModel.checkChallenge(req.body.chall)) { + let challenge = { fen: req.body.chall.fen, cadence: req.body.chall.cadence, randomness: req.body.chall.randomness, @@ -17,17 +15,15 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => { to: req.body.chall.to, //string: user name (may be empty) }; const insertChallenge = () => { - ChallengeModel.create(challenge, (err,ret) => { - res.json(err || {cid:ret.cid}); + ChallengeModel.create(challenge, (err, ret) => { + res.json(err || ret); }); }; - if (req.body.chall.to) - { + if (req.body.chall.to) { UserModel.getOne("name", challenge.to, (err,user) => { if (err || !user) res.json(err || {errmsg: "Typo in player name"}); - else - { + else { challenge.to = user.id; //ready now to insert challenge insertChallenge(); if (user.notify) @@ -36,26 +32,22 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => { "New challenge: " + params.siteURL + "/#/?disp=corr"); } }); - } - else - insertChallenge(); + } else insertChallenge(); } }); router.get("/challenges", access.ajax, (req,res) => { const uid = req.query.uid; - if (uid.match(/^[0-9]+$/)) - { + if (uid.match(/^[0-9]+$/)) { ChallengeModel.getByUser(uid, (err,challenges) => { - res.json(err || {challenges:challenges}); + res.json(err || { challenges: challenges }); }); } }); router.delete("/challenges", access.logged, access.ajax, (req,res) => { const cid = req.query.id; - if (cid.match(/^[0-9]+$/)) - { + if (cid.match(/^[0-9]+$/)) { ChallengeModel.safeRemove(cid, req.userId); res.json({}); } diff --git a/server/routes/games.js b/server/routes/games.js index a86a76dd..77877a2e 100644 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -12,46 +12,58 @@ router.post("/games", access.logged, access.ajax, (req,res) => { const cid = req.body.cid; if ( Array.isArray(gameInfo.players) && - gameInfo.players.some(p => p.uid == req.userId) && + gameInfo.players.some(p => p.id == req.userId) && (!cid || cid.toString().match(/^[0-9]+$/)) && GameModel.checkGameInfo(gameInfo) ) { if (!!cid) ChallengeModel.remove(cid); GameModel.create( gameInfo.vid, gameInfo.fen, gameInfo.cadence, gameInfo.players, - (err,ret) => { + (err, ret) => { const oppIdx = (gameInfo.players[0].id == req.userId ? 1 : 0); const oppId = gameInfo.players[oppIdx].id; UserModel.tryNotify(oppId, - "Game started: " + params.siteURL + "/#/game/" + ret.gid); - res.json({gameId: ret.gid}); + "Game started: " + params.siteURL + "/#/game/" + ret.id); + res.json(err || ret); } ); } }); +// Get only one game (for Game page) router.get("/games", access.ajax, (req,res) => { const gameId = req.query["gid"]; - if (gameId) - { - if (gameId.match(/^[0-9]+$/)) - { - GameModel.getOne(gameId, (err,game) => { - res.json({game: game}); - }); - } + if (!!gameId && gameId.match(/^[0-9]+$/)) { + GameModel.getOne(gameId, (err, game) => { + res.json({ game: game }); + }); } - else - { - // Get by (non-)user ID: - const userId = req.query["uid"]; - if (userId.match(/^[0-9]+$/)) - { - const excluded = !!req.query["excluded"]; - GameModel.getByUser(userId, excluded, (err,games) => { - res.json({games: games}); - }); - } +}); + +// Get by (non-)user ID, for Hall +router.get("/observedgames", access.ajax, (req,res) => { + const userId = req.query["uid"]; + const cursor = req.query["cursor"]; + if (!!userId.match(/^[0-9]+$/) && !!cursor.match(/^[0-9]+$/)) { + GameModel.getObserved(userId, (err, games) => { + res.json({ games: games }); + }); + } +}); + +// Get by user ID, for MyGames page +router.get("/runninggames", access.ajax, access.logged, (req,res) => { + GameModel.getRunning(req.userId, (err, games) => { + res.json({ games: games }); + }); +}); + +router.get("/completedgames", access.ajax, access.logged, (req,res) => { + const cursor = req.query["cursor"]; + if (!!cursor.match(/^[0-9]+$/)) { + GameModel.getCompleted(req.userId, cursor, (err, games) => { + res.json({ games: games }); + }); } }); @@ -59,28 +71,29 @@ router.get("/games", access.ajax, (req,res) => { router.put("/games", access.logged, access.ajax, (req,res) => { const gid = req.body.gid; let obj = req.body.newObj; - if (gid.toString().match(/^[0-9]+$/) && GameModel.checkGameUpdate(obj)) - { + if (gid.toString().match(/^[0-9]+$/) && GameModel.checkGameUpdate(obj)) { GameModel.getPlayers(gid, (err,players) => { - const myIdx = players.findIndex(p => p.uid == req.userId) - if (myIdx >= 0) { + let myColor = ''; + if (players.white == req.userId) myColor = 'w'; + else if (players.black == req.userId) myColor = 'b'; + if (!!myColor) { // Did I mark the game for deletion? if (!!obj.removeFlag) { - obj.deletedBy = ["w","b"][myIdx]; + obj.deletedBy = myColor; delete obj["removeFlag"]; } GameModel.update(gid, obj, (err) => { - if (!err && (!!obj.move || !!obj.score)) - { + if (!err && (!!obj.move || !!obj.score)) { // Notify opponent if he enabled notifications: - const oppid = players[0].uid == req.userId - ? players[1].uid - : players[0].uid; - const messagePrefix = obj.move - ? "New move in game: " - : "Game ended: "; - UserModel.tryNotify(oppid, - messagePrefix + params.siteURL + "/#/game/" + gid); + const oppid = (myColor == 'w' ? players.black : players.white); + const messagePrefix = + !!obj.move + ? "New move in game: " + : "Game ended: "; + UserModel.tryNotify( + oppid, + messagePrefix + params.siteURL + "/#/game/" + gid + ); } res.json(err || {}); }); @@ -93,10 +106,10 @@ router.put("/games", access.logged, access.ajax, (req,res) => { // Moves update also could, although logical unit in a game. router.delete("/chats", access.logged, access.ajax, (req,res) => { const gid = req.query["gid"]; - GameModel.getPlayers(gid, (err,players) => { - if (players.some(p => p.uid == req.userId)) + GameModel.getPlayers(gid, (err, players) => { + if ([players.white, players.black].includes(req.userId)) { - GameModel.update(gid, {delchat: true}, () => { + GameModel.update(gid, { delchat: true }, () => { res.json({}); }); } diff --git a/server/routes/news.js b/server/routes/news.js index e1efbdd9..4c2a74e5 100644 --- a/server/routes/news.js +++ b/server/routes/news.js @@ -5,19 +5,18 @@ const sanitizeHtml = require('sanitize-html'); const devs = [1]; //hard-coded list of developers IDs, allowed to post news router.post("/news", access.logged, access.ajax, (req,res) => { - if (devs.includes(req.userId)) - { + if (devs.includes(req.userId)) { const content = sanitizeHtml(req.body.news.content); - NewsModel.create(content, req.userId, (err,ret) => { - res.json(err || { id: ret.nid }); + NewsModel.create(content, req.userId, (err, ret) => { + res.json(err || ret); }); } }); router.get("/news", access.ajax, (req,res) => { const cursor = req.query["cursor"]; - if (cursor.match(/^[0-9]+$/)) { - NewsModel.getNext(cursor, (err,newsList) => { + if (!!cursor.match(/^[0-9]+$/)) { + NewsModel.getNext(cursor, (err, newsList) => { res.json(err || { newsList: newsList }); }); } diff --git a/server/routes/problems.js b/server/routes/problems.js index 732ea710..6cebb8f0 100644 --- a/server/routes/problems.js +++ b/server/routes/problems.js @@ -4,18 +4,16 @@ const ProblemModel = require("../models/Problem"); const sanitizeHtml = require('sanitize-html'); router.post("/problems", access.logged, access.ajax, (req,res) => { - if (ProblemModel.checkProblem(req.body.prob)) - { - const problem = - { + if (ProblemModel.checkProblem(req.body.prob)) { + const problem = { vid: req.body.prob.vid, fen: req.body.prob.fen, uid: req.userId, instruction: sanitizeHtml(req.body.prob.instruction), solution: sanitizeHtml(req.body.prob.solution), }; - ProblemModel.create(problem, (err,ret) => { - res.json(err || {id:ret.pid}); + ProblemModel.create(problem, (err, ret) => { + res.json(err || ret); }); } else @@ -24,24 +22,20 @@ router.post("/problems", access.logged, access.ajax, (req,res) => { router.get("/problems", access.ajax, (req,res) => { const probId = req.query["pid"]; - if (probId && probId.match(/^[0-9]+$/)) - { + if (probId && probId.match(/^[0-9]+$/)) { ProblemModel.getOne(req.query["pid"], (err,problem) => { res.json(err || {problem: problem}); }); - } - else - { + } else { ProblemModel.getAll((err,problems) => { - res.json(err || {problems:problems}); + res.json(err || { problems: problems }); }); } }); router.put("/problems", access.logged, access.ajax, (req,res) => { let obj = req.body.prob; - if (ProblemModel.checkProblem(obj)) - { + if (ProblemModel.checkProblem(obj)) { obj.instruction = sanitizeHtml(obj.instruction); obj.solution = sanitizeHtml(obj.solution); ProblemModel.safeUpdate(obj, req.userId); diff --git a/server/routes/users.js b/server/routes/users.js index fc29730c..7898ac8a 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -9,20 +9,16 @@ router.post('/register', access.unlogged, access.ajax, (req,res) => { const name = req.body.name; const email = req.body.email; const notify = !!req.body.notify; - if (UserModel.checkNameEmail({name: name, email: email})) - { - UserModel.create(name, email, notify, (err,ret) => { - if (err) - { + if (UserModel.checkNameEmail({name: name, email: email})) { + UserModel.create(name, email, notify, (err, ret) => { + if (!!err) { const msg = err.code == "SQLITE_CONSTRAINT" ? "User name or email already in use" : "User creation failed. Try again"; res.json({errmsg: msg}); - } - else - { + } else { const user = { - id: ret.uid, + id: ret.id, name: name, email: email, }; @@ -51,10 +47,8 @@ router.get("/whoami", access.ajax, (req,res) => { notify: false, newsRead: 0 }; - if (!req.cookies.token) - callback(anonymous); - else if (req.cookies.token.match(/^[a-z0-9]+$/)) - { + if (!req.cookies.token) callback(anonymous); + else if (req.cookies.token.match(/^[a-z0-9]+$/)) { UserModel.getOne("sessionToken", req.cookies.token, (err, user) => { callback(user || anonymous); }); @@ -64,8 +58,8 @@ router.get("/whoami", access.ajax, (req,res) => { // NOTE: this method is safe because only IDs and names are returned router.get("/users", access.ajax, (req,res) => { const ids = req.query["ids"]; - if (ids.match(/^([0-9]+,?)+$/)) //NOTE: slightly too permissive - { + // NOTE: slightly too permissive RegExp + if (ids.match(/^([0-9]+,?)+$/)) { UserModel.getByIds(ids, (err,users) => { res.json({users:users}); }); @@ -75,8 +69,7 @@ router.get("/users", access.ajax, (req,res) => { router.put('/update', access.logged, access.ajax, (req,res) => { const name = req.body.name; const email = req.body.email; - if (UserModel.checkNameEmail({name: name, email: email})); - { + if (UserModel.checkNameEmail({name: name, email: email})) { const user = { id: req.userId, name: name, @@ -97,8 +90,7 @@ router.put('/newsread', access.logged, access.ajax, (req,res) => { // Authentication-related methods: // to: object user (to who we send an email) -function setAndSendLoginToken(subject, to, res) -{ +function setAndSendLoginToken(subject, to, res) { // Set login token and send welcome(back) email with auth link const token = genToken(params.token.length); UserModel.setLoginToken(token, to.id); @@ -115,8 +107,7 @@ function setAndSendLoginToken(subject, to, res) router.get('/sendtoken', access.unlogged, access.ajax, (req,res) => { const nameOrEmail = decodeURIComponent(req.query.nameOrEmail); const type = (nameOrEmail.indexOf('@') >= 0 ? "email" : "name"); - if (UserModel.checkNameEmail({[type]: nameOrEmail})) - { + if (UserModel.checkNameEmail({[type]: nameOrEmail})) { UserModel.getOne(type, nameOrEmail, (err,user) => { access.checkRequest(res, err, user, "Unknown user", () => { setAndSendLoginToken("Token for " + params.siteURL, user, res); @@ -134,8 +125,7 @@ router.get('/authenticate', access.unlogged, access.ajax, (req,res) => { // If token older than params.tokenExpire, do nothing if (Date.now() > user.loginTime + params.token.expire) res.json({errmsg: "Token expired"}); - else - { + else { // Generate session token (if not exists) + destroy login token UserModel.trySetSessionToken(user.id, (token) => { res.cookie("token", token, { diff --git a/server/sockets.js b/server/sockets.js index b16135bd..7de7abb9 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -269,7 +269,7 @@ module.exports = function(wss) { case "notifynewgame": if (!!clients["/mygames"]) { obj.targets.forEach(t => { - const k = t.sid || idToSid[t.uid]; + const k = t.sid || idToSid[t.id]; if (!!clients["/mygames"][k]) { Object.keys(clients["/mygames"][k]).forEach(x => { send( diff --git a/server/utils/access.js b/server/utils/access.js index 36511dba..732353f2 100644 --- a/server/utils/access.js +++ b/server/utils/access.js @@ -10,22 +10,16 @@ module.exports = else next(); }; let loggedIn = undefined; - if (!req.cookies.token) - { + if (!req.cookies.token) { loggedIn = false; callback(); - } - else - { + } else { UserModel.getOne("sessionToken", req.cookies.token, function(err, user) { - if (!!user) - { + if (!!user) { req.userId = user.id; req.userName = user.name; loggedIn = true; - } - else - { + } else { // Token in cookies presumably wrong: erase it res.clearCookie("token"); loggedIn = false; @@ -39,28 +33,25 @@ module.exports = unlogged: function(req, res, next) { // Just a quick heuristic, which should be enough const loggedIn = !!req.cookies.token; - if (loggedIn) - res.json({errmsg: "Error: try to delete cookies"}); + if (loggedIn) res.json({errmsg: "Error: try to delete cookies"}); else next(); }, // Prevent direct access to AJAX results ajax: function(req, res, next) { - if (!req.xhr) - res.json({errmsg: "Unauthorized access"}); + if (!req.xhr) res.json({errmsg: "Unauthorized access"}); else next(); }, // Check for errors before callback (continue page loading). TODO: better name. checkRequest: function(res, err, out, msg, cb) { - if (err) - res.json({errmsg: err.errmsg || err.toString()}); - else if (!out - || (Array.isArray(out) && out.length == 0) - || (typeof out === "object" && Object.keys(out).length == 0)) - { + if (!!err) res.json({errmsg: err.errmsg || err.toString()}); + else if ( + !out || + (Array.isArray(out) && out.length == 0) || + (typeof out === "object" && Object.keys(out).length == 0) + ) { res.json({errmsg: msg}); - } - else cb(); - }, + } else cb(); + } } diff --git a/server/utils/database.js b/server/utils/database.js index 2904f2ae..7c20d521 100644 --- a/server/utils/database.js +++ b/server/utils/database.js @@ -1,8 +1,7 @@ const sqlite3 = require('sqlite3'); const params = require("../config/parameters") -if (params.env == "development") - sqlite3.verbose(); +if (params.env == "development") sqlite3.verbose(); const DbPath = __dirname.replace("/utils", "/db/vchess.sqlite"); const db = new sqlite3.Database(DbPath); diff --git a/server/utils/mailer.js b/server/utils/mailer.js index b0a0bace..df60e699 100644 --- a/server/utils/mailer.js +++ b/server/utils/mailer.js @@ -1,24 +1,21 @@ const nodemailer = require('nodemailer'); const params = require("../config/parameters"); -module.exports = function(from, to, subject, body, cb) -{ +module.exports = function(from, to, subject, body, cb) { // Avoid the actual sending in development mode - if (params.env === 'development') - { + if (params.env === 'development') { console.log("New mail: from " + from + " / to " + to); console.log("Subject: " + subject); console.log(body); - if (!cb) - cb = (err) => { if (err) console.log(err); } + if (!cb) cb = (err) => { if (err) console.log(err); } cb(); return; } // Production-only code from here: - if (!cb) - cb = () => {}; //default: do nothing (TODO: log somewhere) + // Default: do nothing (TODO: log somewhere) + if (!cb) cb = () => {}; // Create reusable transporter object using the default SMTP transport const transporter = nodemailer.createTransport({ diff --git a/server/utils/tokenGenerator.js b/server/utils/tokenGenerator.js index 2c21b4e5..d89b4ccf 100644 --- a/server/utils/tokenGenerator.js +++ b/server/utils/tokenGenerator.js @@ -1,14 +1,11 @@ -function randString() -{ +function randString() { return Math.random().toString(36).substr(2); // remove `0.` } -module.exports = function(tokenLength) -{ +module.exports = function(tokenLength) { let res = ""; // 10 = min length of a rand() string - let nbRands = Math.ceil(tokenLength/10); - for (let i = 0; i < nbRands; i++) - res += randString(); + const nbRands = Math.ceil(tokenLength/10); + for (let i = 0; i < nbRands; i++) res += randString(); return res.substr(0, tokenLength); } -- 2.44.0