From badeb466c977ed9a8e1b464a2236001126decb9e Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Thu, 17 Jan 2019 12:57:43 +0100 Subject: [PATCH] Debugging problems page; TODO: hash navigation is wrong --- models/Problem.js | 39 ++--- models/User.js | 9 +- .../javascripts/components/problemSummary.js | 5 +- public/javascripts/components/problems.js | 135 +++++++++--------- public/javascripts/components/tabGames.js | 1 + public/stylesheets/variant.sass | 5 + routes/problems.js | 26 +++- routes/users.js | 4 +- utils/database.js | 5 + utils/tokenGenerator.js | 21 ++- 10 files changed, 138 insertions(+), 112 deletions(-) diff --git a/models/Problem.js b/models/Problem.js index 7ac92f78..99e5b620 100644 --- a/models/Problem.js +++ b/models/Problem.js @@ -10,20 +10,20 @@ var db = require("../utils/database"); * solution: text */ -// TODO: callback ? -exports.create = function(vid, fen, instructions, solution) +exports.create = function(uid, vid, fen, instructions, solution, cb) { db.serialize(function() { - const query = - "INSERT INTO Problems (added, vid, fen, instructions, solution) VALUES " + - "(" + - Date.now() + "," + - vid + "," + - fen + "," + - instructions + "," + - solution + - ")"; - db.run(query); + const insertQuery = + "INSERT INTO Problems (added, uid, vid, fen, instructions, solution) " + + "VALUES (" + Date.now() + "," + uid + "," + vid + ",'" + fen + "',?,?)"; + db.run(insertQuery, [instructions, solution], err => { + if (!!err) + return cb(err); + db.get("SELECT last_insert_rowid() AS rowid", cb); + }); +// const stmt = db.prepare(query); +// stmt.run(instructions, solution); +// stmt.finalize(); }); } @@ -43,7 +43,7 @@ exports.fetchN = function(vid, uid, type, directionStr, lastDt, MaxNbProblems, c db.serialize(function() { let typeLine = ""; if (uid > 0) - typeLine = "AND id " + (type=="others" ? "!=" : "=") + " " + uid; + typeLine = "AND uid " + (type=="others" ? "!=" : "=") + " " + uid; const query = "SELECT * FROM Problems " + "WHERE vid = " + vid + @@ -54,16 +54,17 @@ exports.fetchN = function(vid, uid, type, directionStr, lastDt, MaxNbProblems, c }); } -exports.update = function(id, uid, fen, instructions, solution) +// TODO: update fails (but insert is OK) +exports.update = function(id, uid, fen, instructions, solution, cb) { db.serialize(function() { const query = - "UPDATE Problems " + - "fen = " + fen + ", " + - "instructions = " + instructions + ", " + - "solution = " + solution + " " + + "UPDATE Problems SET " + + "fen = '" + fen + "', " + + "instructions = ?, " + + "solution = ? " + "WHERE id = " + id + " AND uid = " + uid; - db.run(query); + db.run(query, [instructions,solution], cb); }); } diff --git a/models/User.js b/models/User.js index 171dc2c2..4b5c840a 100644 --- a/models/User.js +++ b/models/User.js @@ -1,6 +1,6 @@ var db = require("../utils/database"); var maild = require("../utils/mailer.js"); -var TokenGen = require("../utils/tokenGenerator"); +var genToken = require("../utils/tokenGenerator"); var params = require("../config/parameters"); /* @@ -14,10 +14,7 @@ var params = require("../config/parameters"); * notify: boolean (send email notifications for corr games) */ -// TODO: consider sanitizing http://www.unixwiz.net/techtips/sql-injection.html -// But parameters are supposed to already be cleaned (in controller). - -// User creation +// NOTE: parameters are already cleaned (in controller), thus no sanitization here exports.create = function(name, email, notify, callback) { db.serialize(function() { @@ -73,7 +70,7 @@ exports.trySetSessionToken = function(uid, cb) db.get(querySessionToken, (err,ret) => { if (!!err) return cb(err); - const token = ret.sessionToken || TokenGen.generate(params.token.length); + const token = ret.sessionToken || genToken(params.token.length); const queryUpdate = "UPDATE Users " + "SET loginToken = NULL" + diff --git a/public/javascripts/components/problemSummary.js b/public/javascripts/components/problemSummary.js index d7f239ee..3d579156 100644 --- a/public/javascripts/components/problemSummary.js +++ b/public/javascripts/components/problemSummary.js @@ -1,6 +1,6 @@ // Preview a problem on variant page Vue.component('my-problem-summary', { - props: ['prob','userid'], + props: ['prob','userid','preview'], template: ` <div class="row problem"> <div class="col-sm-12 col-md-6 diagram" @@ -10,7 +10,8 @@ Vue.component('my-problem-summary', { <p v-html="prob.instructions"></p> <p v-if="!!prob.preview" v-html="prob.solution"></p> <p v-else class="problem-time">{{ timestamp2date(prob.added) }}</p> - <div v-show="prob.uid==userid" class="button-group"> + <button @click="$emit('show-problem')">Show</button> + <div v-show="prob.uid==userid && !preview" class="button-group"> <button @click="$emit('edit-problem')">Edit</button> <button @click="$emit('delete-problem')">Delete</button> </div> diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js index 6f03fd97..6edab93d 100644 --- a/public/javascripts/components/problems.js +++ b/public/javascripts/components/problems.js @@ -14,6 +14,7 @@ Vue.component('my-problems', { // New problem (to upload), or existing problem to edit: modalProb: { id: 0, //defined if it's an edit + uid: 0, //...also fen: "", instructions: "", solution: "", @@ -24,48 +25,51 @@ Vue.component('my-problems', { template: ` <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> <div id="problemControls" class="button-group"> - <button :aria-label='translate("Previous problem(s)")' class="tooltip" @click="showNext('backward')"> + <button :aria-label='translate("Previous problem(s)")' class="tooltip" + @click="showNext('backward')" + > <i class="material-icons">skip_previous</i> </button> - <button :aria-label='translate("Add a problem")' class="tooltip" onClick="doClick('modal-newproblem')"> + <button v-if="!!userId" :aria-label='translate("Add a problem")' + class="tooltip" onClick="doClick('modal-newproblem')" + > {{ translate("New") }} </button> - <button :aria-label='translate("Next problem(s)")' class="tooltip" @click="showNext('forward')"> + <button :aria-label='translate("Next problem(s)")' class="tooltip" + @click="showNext('forward')" + > <i class="material-icons">skip_next</i> </button> </div> <div id="mainBoard" v-if="!!curProb"> <div id="instructions-div" class="section-content"> - <p id="problem-instructions"> - {{ curProb.instructions }} - </p> + <p id="problem-instructions">{{ curProb.instructions }}</p> </div> - <my-game :fen="curProb.fen" :mode="analyze" :allowMovelist="true" :settings="settings"> + <my-game :fen="curProb.fen" :mode="analyze" :allowMovelist="true" + :settings="settings" + > </my-game> <div id="solution-div" class="section-content"> <h3 class="clickable" @click="showSolution = !showSolution"> {{ translations["Show solution"] }} </h3> - <p id="problem-solution" v-show="showSolution"> - {{ curProb.solution }} - </p> + <p id="problem-solution" v-show="showSolution">{{ curProb.solution }}</p> </div> - <button @click="displayList()"> - <span>Back to list display</span> - </button> + <button @click="displayList">Back to list display</button> </div> <div> <input type="text" placeholder="Type problem number" v-model="pbNum"/> - <button @click="showProblem()"> - <span>Show problem</span> - </button> + <button @click="showProblem">Show problem</button> </div> - <button v-if="!!userId" @click="toggleListDisplay()"> - <span>My problems (only)</span> + <button v-if="!!userId" @click="toggleListDisplay" + :class="{'only-mine':display=='mine'}" + > + My problems (only) </button> <my-problem-summary v-show="!curProb" v-on:edit-problem="editProblem(p)" v-on:delete-problem="deleteProblem(p.id)" - v-for="p in curProblems" @click="curProb=p" + v-on:show-problem="() => showProblem(p.id)" + v-for="p in curProblems()" @click="curProb=p" v-bind:prob="p" v-bind:userid="userId" v-bind:key="p.id"> </my-problem-summary> <input type="checkbox" id="modal-newproblem" class="modal"/> @@ -73,9 +77,7 @@ Vue.component('my-problems', { <div v-show="!modalProb.preview" class="card newproblem-form"> <label for="modal-newproblem" class="modal-close"> </label> - <h3 id="modalProblemTxt"> - {{ translate("Add a problem") }} - </h3> + <h3 id="modalProblemTxt">{{ translate("Add a problem") }}</h3> <form @submit.prevent="previewProblem()"> <fieldset> <label for="newpbFen">FEN</label> @@ -83,39 +85,28 @@ Vue.component('my-problems', { :placeholder='translate("Full FEN description")'/> </fieldset> <fieldset> - <p class="emphasis"> - {{ translate("Safe HTML tags allowed") }} - </p> - <label for="newpbInstructions"> - {{ translate("Instructions") }} - </label> + <p class="emphasis">{{ translate("Safe HTML tags allowed") }}</p> + <label for="newpbInstructions">{{ translate("Instructions") }}</label> <textarea id="newpbInstructions" v-model="modalProb.instructions" :placeholder='translate("Describe the problem goal")'> </textarea> - <label for="newpbSolution"> - {{ translate("Solution") }} - </label> + <label for="newpbSolution">{{ translate("Solution") }}</label> <textarea id="newpbSolution" v-model="modalProb.solution" :placeholder='translate("How to solve the problem?")'> </textarea> - <button class="center-btn"> - {{ translate("Preview") }} - </button> + <button class="center-btn">{{ translate("Preview") }}</button> </fieldset> </form> </div> <div v-show="modalProb.preview" class="card newproblem-preview"> - <label for="modal-newproblem" class="modal-close"> + <label for="modal-newproblem" class="modal-close" + @click="modalProb.preview=false"> </label> - <my-problem-summary v-bind:prob="modalProb" v-bind:userid="userId"> + <my-problem-summary :prob="modalProb" :userid="userId" :preview="true"> </my-problem-summary> <div class="button-group"> - <button @click="modalProb.preview=false"> - {{ translate("Cancel") }} - </button> - <button @click="sendProblem()"> - {{ translate("Send") }} - </button> + <button @click="modalProb.preview=false">{{ translate("Cancel") }}</button> + <button @click="sendProblem()">{{ translate("Send") }}</button> </div> </div> </div> @@ -123,9 +114,7 @@ Vue.component('my-problems', { <div role="dialog" aria-labelledby="nomoreMessage"> <div class="card smallpad small-modal text-center"> <label for="modalNomore" class="modal-close"></label> - <h3 id="nomoreMessage" class="section"> - {{ nomoreMessage }} - </h3> + <h3 id="nomoreMessage" class="section">{{ nomoreMessage }}</h3> </div> </div> </div> @@ -160,11 +149,17 @@ Vue.component('my-problems', { }, showProblem: function(num) { const pid = num || this.pbNum; - location.hash = "#" + pid; - const pIdx = this.singletons.findIndex(p => p.id == pid); - if (pIdx >= 0) - curProb = this.singletons[pIdx]; - else + location.hash = "#problems?id=" + pid; + for (let parray of [this.singletons,this.problems,this.myProblems]) + { + const pIdx = parray.findIndex(p => p.id == pid); + if (pIdx >= 0) + { + curProb = parray[pIdx]; + break; + } + } + if (!curProb) { // Cannot find problem in current set; get from server, and add to singletons. ajax( @@ -240,27 +235,30 @@ Vue.component('my-problems', { }, displayList: function() { this.curProb = null; - location.hash = ""; + location.hash = "#problems"; // Fetch problems if first call (if #num, and then lists) if (!this.listsInitialized) this.firstFetch(); }, toggleListDisplay: function() { - this.display = (this.display == "others" ? "mine" : "others"); + const displays = ["mine","others"]; + const curIndex = displays.findIndex(item => item == this.display); + this.display = displays[1-curIndex]; }, fetchProblems: function(type, direction) { let problems = (type == "others" ? this.problems : this.myProblems); + // "last datetime" set at a value OK for an empty initial array let last_dt = (direction=="forward" ? 0 : Number.MAX_SAFE_INTEGER); - if (this.problems.length > 0) + if (problems.length > 0) { // Search for newest date (or oldest) last_dt = problems[0].added; for (let i=1; i<problems.length; i++) { - if ((direction == "forward" && this.problems[i].added > last_dt) || - (direction == "backward" && this.problems[i].added < last_dt)) + if ((direction == "forward" && problems[i].added > last_dt) || + (direction == "backward" && problems[i].added < last_dt)) { - last_dt = this.problems[i].added; + last_dt = problems[i].added; } } } @@ -276,7 +274,7 @@ Vue.component('my-problems', { if (response.problems.length > 0) { Array.prototype.push.apply(problems, - response.problems.sort((p1,p2) => { return p1.added - p2.added; })); + response.problems.sort((p1,p2) => { return p2.added - p1.added; })); // If one list is empty but not the other, show the non-empty const otherArray = (type == "mine" ? this.problems : this.myProblems); if (problems.length > 0 && otherArray.length == 0) @@ -286,21 +284,22 @@ Vue.component('my-problems', { ); }, previewProblem: function() { - if (!V.IsGoodFen(this.newProblem.fen)) + if (!V.IsGoodFen(this.modalProb.fen)) return alert(translations["Bad FEN description"]); - if (this.newProblem.instructions.trim().length == 0) + if (this.modalProb.instructions.trim().length == 0) return alert(translations["Empty instructions"]); - if (this.newProblem.solution.trim().length == 0) + if (this.modalProb.solution.trim().length == 0) return alert(translations["Empty solution"]); - this.modalProb.preview = true; + Vue.set(this.modalProb, "preview", true); }, editProblem: function(prob) { this.modalProb = prob; + Vue.set(this.modalProb, "preview", false); document.getElementById("modal-newproblem").checked = true; }, deleteProblem: function(pid) { ajax( - "/problems/" + variant.id + "/" + pid, + "/problems/" + pid, "DELETE", response => { // Delete problem from the list on client side @@ -318,11 +317,17 @@ Vue.component('my-problems', { this.modalProb, response => { document.getElementById("modal-newproblem").checked = false; + Vue.set(this.modalProb, "preview", false); if (this.modalProb.id == 0) { - this.modalProb.added = Date.now(); - this.modalProb.preview = false; - this.myProblems.push(JSON.parse(JSON.stringify(this.modalProb))); + this.myProblems.unshift({ + added: Date.now(), + id: response.id, + uid: user.id, + fen: this.modalProb.fen, + instructions: this.modalProb.instructions, + solution: this.modalProb.solution, + }); } else this.modalProb.id = 0; diff --git a/public/javascripts/components/tabGames.js b/public/javascripts/components/tabGames.js index 3181be6b..3861dbac 100644 --- a/public/javascripts/components/tabGames.js +++ b/public/javascripts/components/tabGames.js @@ -24,6 +24,7 @@ Vue.component("my-tab-games", { </my-game-list> <my-game-list v-show="display=='imported'" :games="imported"> </my-game-list> + <button @click="update">Refresh</button> </div> `, created: function() { diff --git a/public/stylesheets/variant.sass b/public/stylesheets/variant.sass index 1141d2ca..ed55f96b 100644 --- a/public/stylesheets/variant.sass +++ b/public/stylesheets/variant.sass @@ -385,3 +385,8 @@ ul:not(.browser-default) > li .problem margin: 10px 0 + +.only-mine + background-color: yellow + &:hover + background-color: yellow diff --git a/routes/problems.js b/routes/problems.js index 3434f0cc..777543b1 100644 --- a/routes/problems.js +++ b/routes/problems.js @@ -55,27 +55,39 @@ router.get("/problems/:vid([0-9]+)", access.ajax, (req,res) => { // Upload a problem (sanitize inputs) router.post("/problems/:vid([0-9]+)", access.logged, access.ajax, (req,res) => { const vid = req.params["vid"]; - const s = sanitizeUserInput(req.body["fen"], req.body["instructions"], req.body["solution"]); + const s = sanitizeUserInput( + req.body["fen"], req.body["instructions"], req.body["solution"]); if (typeof s === "string") return res.json({errmsg: s}); - ProblemModel.create(vid, s.fen, s.instructions, s.solution); - res.json({}); + ProblemModel.create(req.userId, vid, s.fen, s.instructions, s.solution, + (err,pid) => { + if (!!err) + return res.json(err); + res.json({id: pid["rowid"]}); + } + ); }); // 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"]); + 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.userId, fen, instructions, solution); - res.json({}); + ProblemModel.update(pid, req.userId, s.fen, s.instructions, s.solution, + err => { + if (!!err) + return res.json(err); + 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.userId); + ProblemModel.remove(pid, req.userId); res.json({}); }); diff --git a/routes/users.js b/routes/users.js index 9c88d08c..95992c53 100644 --- a/routes/users.js +++ b/routes/users.js @@ -3,7 +3,7 @@ var router = require("express").Router(); var UserModel = require('../models/User'); var sendEmail = require('../utils/mailer'); -var TokenGen = require("../utils/tokenGenerator"); +var genToken = require("../utils/tokenGenerator"); var access = require("../utils/access"); var params = require("../config/parameters"); var checkNameEmail = require("../public/javascripts/shared/userCheck") @@ -12,7 +12,7 @@ var checkNameEmail = require("../public/javascripts/shared/userCheck") function setAndSendLoginToken(subject, to, res) { // Set login token and send welcome(back) email with auth link - const token = TokenGen.generate(params.token.length); + const token = genToken(params.token.length); UserModel.setLoginToken(token, to.id, err => { if (!!err) return res.json({errmsg: err.toString()}); diff --git a/utils/database.js b/utils/database.js index 39c7e5e0..ae7c7a66 100644 --- a/utils/database.js +++ b/utils/database.js @@ -1,4 +1,9 @@ const sqlite3 = require('sqlite3'); +const params = require("../config/parameters") + +if (params.env == "development") + sqlite3.verbose(); + const DbPath = __dirname.replace("/utils", "/db/vchess.sqlite"); const db = new sqlite3.Database(DbPath); diff --git a/utils/tokenGenerator.js b/utils/tokenGenerator.js index 1bc172cd..b549198a 100644 --- a/utils/tokenGenerator.js +++ b/utils/tokenGenerator.js @@ -1,14 +1,13 @@ -module.exports = +function randString() { - rand: function() { - return Math.random().toString(36).substr(2); // remove `0.` - }, + return Math.random().toString(36).substr(2); // remove `0.` +} - generate: function(tlen) { - var res = ""; - var nbRands = Math.ceil(tlen/10); //10 = min length of a rand() string - for (var i = 0; i < nbRands; i++) - res += TokenGen.rand(); - return res.substr(0, tlen); - }, +module.exports = function(tlen) +{ + let res = ""; + let nbRands = Math.ceil(tlen/10); //10 = min length of a rand() string + for (let i = 0; i < nbRands; i++) + res += randString(); + return res.substr(0, tlen); } -- 2.44.0