From 298c42e63ae321526693e9ce418c4113af36e025 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Mon, 7 Jan 2019 17:36:27 +0100 Subject: [PATCH] Index page almost OK. Now work on variant page (main hall...) --- .gitignore | 6 +- app.js | 5 +- package-lock.json | 39 +-- package.json | 3 +- public/javascripts/components/game.js | 20 +- public/javascripts/components/problems.js | 14 +- public/javascripts/contactForm.js | 31 ++ public/javascripts/index.js | 54 +-- public/javascripts/utils/md5.js | 379 ---------------------- public/javascripts/variant.js | 1 + public/stylesheets/index.sass | 27 +- public/stylesheets/layout.sass | 30 +- routes/all.js | 140 +------- routes/index.js | 21 ++ routes/messages.js | 20 ++ routes/problems.js | 58 ++++ routes/variant.js | 33 ++ utils/language.js | 27 ++ utils/sendEmail.js.dist | 32 ++ views/contactForm.pug | 18 + views/index.pug | 14 +- views/layout.pug | 27 +- views/settings.pug | 8 +- views/translations/en.pug | 7 + views/variant.pug | 7 +- 25 files changed, 384 insertions(+), 637 deletions(-) create mode 100644 public/javascripts/contactForm.js delete mode 100644 public/javascripts/utils/md5.js create mode 100644 routes/index.js create mode 100644 routes/messages.js create mode 100644 routes/problems.js create mode 100644 routes/variant.js create mode 100644 utils/language.js create mode 100644 utils/sendEmail.js.dist create mode 100644 views/contactForm.pug diff --git a/.gitignore b/.gitignore index 30651eea..c273cf0c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,11 +15,9 @@ pids *.swp *~ -# Demo video, database -*.webm +# Various files /db/vchess.sqlite - -# socket URL file +/utils/sendEmail.js /public/javascripts/socket_url.js # CSS generated files diff --git a/app.js b/app.js index b44be791..914e29b1 100644 --- a/app.js +++ b/app.js @@ -6,8 +6,6 @@ var logger = require('morgan'); var sassMiddleware = require('node-sass-middleware'); var favicon = require('serve-favicon'); -var router = require('./routes/all'); - var app = express(); app.use(favicon(path.join(__dirname, "public", "images", "favicon", "favicon.ico"))); @@ -45,7 +43,8 @@ app.use(sassMiddleware({ })); app.use(express.static(path.join(__dirname, 'public'))); -app.use('/', router); +const routes = require(path.join(__dirname, "routes", "all")); +app.use('/', routes); // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/package-lock.json b/package-lock.json index cc552a26..2acac2b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1141,9 +1141,9 @@ } }, "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } @@ -1948,14 +1948,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1970,20 +1968,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2100,8 +2095,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2113,7 +2107,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2128,7 +2121,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2136,14 +2128,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -2162,7 +2152,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2243,8 +2232,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2256,7 +2244,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2378,7 +2365,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3891,6 +3877,11 @@ "node-sass": "^4.3.0" } }, + "nodemailer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-5.0.0.tgz", + "integrity": "sha512-XI4PI5L7GYcJyHkPcHlvPyRrYohNYBNRNbt1tU8PXNU3E1ADJC84a13V0vbL9AM431OP+ETacaGXAF8fGn1JvA==" + }, "nodemon": { "version": "1.18.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.9.tgz", diff --git a/package.json b/package.json index 622ca8ff..4a67cf93 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,12 @@ }, "dependencies": { "cookie-parser": "~1.4.3", - "debug": "~4.1.0", + "debug": "^4.1.1", "express": "~4.16.4", "http-errors": "~1.7.1", "morgan": "~1.9.1", "node-sass-middleware": "~0.11.0", + "nodemailer": "^5.0.0", "pug": "~2.0.3", "sanitize-html": "^1.20.0", "serve-favicon": "~2.5.0", diff --git a/public/javascripts/components/game.js b/public/javascripts/components/game.js index fec3df54..c2cce62c 100644 --- a/public/javascripts/components/game.js +++ b/public/javascripts/components/game.js @@ -1480,17 +1480,15 @@ Vue.component('my-game', { }, }) -// TODO: keep moves list here -get lastMove() - { - const L = this.moves.length; - return (L>0 ? this.moves[L-1] : null); - } - -// here too: - move.notation = this.getNotation(move); - // Hash of current game state *after move*, to detect repetitions - move.hash = hex_md5(this.getBaseFen() + this.getTurnFen() + this.getFlagsFen()); +//// TODO: keep moves list here +//get lastMove() +// { +// const L = this.moves.length; +// return (L>0 ? this.moves[L-1] : null); +// } +// +//// here too: +// move.notation = this.getNotation(move); //TODO: confirm dialog with "opponent offers draw", avec possible bouton "prevent future offers" + bouton "proposer nulle" //+ bouton "abort" avec score == "?" + demander confirmation pour toutes ces actions, //comme sur lichess diff --git a/public/javascripts/components/problems.js b/public/javascripts/components/problems.js index 4caabb55..bcd069cc 100644 --- a/public/javascripts/components/problems.js +++ b/public/javascripts/components/problems.js @@ -1,7 +1,7 @@ Vue.component('my-problems', { data: function () { return { - problems: problemArray, //initial value + problems: [], newProblem: { fen: "", instructions: "", @@ -71,14 +71,18 @@ Vue.component('my-problems', { return this.problems.sort((p1,p2) => { return p2.added - p1.added; }); }, }, + created: function() { + // TODO: fetch most recent problems from server + }, methods: { translate: function(text) { return translations[text]; }, - // Propagate "show problem" event to parent component (my-variant) - bubbleUp: function(problem) { - this.$emit('show-problem', JSON.stringify(problem)); - }, + // TODO: obsolete: +// // Propagate "show problem" event to parent component (my-variant) +// bubbleUp: function(problem) { +// this.$emit('show-problem', JSON.stringify(problem)); +// }, fetchProblems: function(direction) { if (this.problems.length == 0) return; //what could we do?! diff --git a/public/javascripts/contactForm.js b/public/javascripts/contactForm.js new file mode 100644 index 00000000..0c4fea3a --- /dev/null +++ b/public/javascripts/contactForm.js @@ -0,0 +1,31 @@ +// Note: not using Vue, but would be possible +function trySendMessage() +{ + let email = document.getElementById("userEmail"); + let subject = document.getElementById("mailSubject"); + let content = document.getElementById("mailContent"); + if (!email.value.match(/^[^@]+@[^@]+\.[^@]+$/)) + return alert("Bad email"); + if (content.value.trim().length == 0) + return alert("Empty message"); + if (subject.value.trim().length == 0 && !confirm("No subject. Send anyway?")) + return; + + // Message sending: + ajax( + "/messages", + "POST", + { + email: email.value, + subject: subject.value, + content: content.value, + }, + () => { + subject.value = ""; + content.value = ""; + let emailSent = document.getElementById("emailSent"); + emailSent.style.display = "inline-block"; + setTimeout(() => { emailSent.style.display = "none"; }, 2000); + } + ); +} diff --git a/public/javascripts/index.js b/public/javascripts/index.js index 64489262..6859e8bf 100644 --- a/public/javascripts/index.js +++ b/public/javascripts/index.js @@ -49,33 +49,33 @@ new Vue({ this.conn.onmessage = socketMessageListener; this.conn.onclose = socketCloseListener; }, - mounted: function() { - // Handle key stroke - document.onkeydown = event => { - // Is it Back or Esc? If yes, apply action on current word - if (event.keyCode == 8) //Back - { - event.preventDefault(); - this.curPrefix = this.curPrefix.slice(0,-1); - } - else if (event.keyCode == 27) //Esc - { - event.preventDefault(); - this.curPrefix = ""; - } - // Is it alphanumeric? If yes, stack it - else if (_.range(48,58).includes(event.keyCode) - || _.range(65,91).includes(event.keyCode) - || _.range(97,123).includes(event.keyCode)) - { - let newChar = String.fromCharCode(event.keyCode); - this.curPrefix += this.curPrefix.length==0 - ? newChar.toUpperCase() - : newChar.toLowerCase(); - } - // ...ignore everything else - }; - }, +// mounted: function() { +// // Handle key stroke +// document.onkeydown = event => { +// // Is it Back or Esc? If yes, apply action on current word +// if (event.keyCode == 8) //Back +// { +// event.preventDefault(); +// this.curPrefix = this.curPrefix.slice(0,-1); +// } +// else if (event.keyCode == 27) //Esc +// { +// event.preventDefault(); +// this.curPrefix = ""; +// } +// // Is it alphanumeric? If yes, stack it +// else if (_.range(48,58).includes(event.keyCode) +// || _.range(65,91).includes(event.keyCode) +// || _.range(97,123).includes(event.keyCode)) +// { +// let newChar = String.fromCharCode(event.keyCode); +// this.curPrefix += this.curPrefix.length==0 +// ? newChar.toUpperCase() +// : newChar.toLowerCase(); +// } +// // ...ignore everything else +// }; +// }, }); // TODO: diff --git a/public/javascripts/utils/md5.js b/public/javascripts/utils/md5.js deleted file mode 100644 index 24d190e4..00000000 --- a/public/javascripts/utils/md5.js +++ /dev/null @@ -1,379 +0,0 @@ -/* - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - -/* - * Configurable variables. You may need to tweak these to be compatible with - * the server-side, but the defaults work in most cases. - */ -var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ -var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ - -/* - * These are the functions you'll usually want to call - * They take string arguments and return either hex or base-64 encoded strings - */ -function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); } -function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); } -function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); } -function hex_hmac_md5(k, d) - { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } -function b64_hmac_md5(k, d) - { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } -function any_hmac_md5(k, d, e) - { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); } - -/* - * Perform a simple self-test to see if the VM is working - */ -function md5_vm_test() -{ - return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72"; -} - -/* - * Calculate the MD5 of a raw string - */ -function rstr_md5(s) -{ - return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)); -} - -/* - * Calculate the HMAC-MD5, of a key and some data (raw strings) - */ -function rstr_hmac_md5(key, data) -{ - var bkey = rstr2binl(key); - if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8); - - var ipad = Array(16), opad = Array(16); - for(var i = 0; i < 16; i++) - { - ipad[i] = bkey[i] ^ 0x36363636; - opad[i] = bkey[i] ^ 0x5C5C5C5C; - } - - var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); - return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); -} - -/* - * Convert a raw string to a hex string - */ -function rstr2hex(input) -{ - try { hexcase } catch(e) { hexcase=0; } - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var output = ""; - var x; - for(var i = 0; i < input.length; i++) - { - x = input.charCodeAt(i); - output += hex_tab.charAt((x >>> 4) & 0x0F) - + hex_tab.charAt( x & 0x0F); - } - return output; -} - -/* - * Convert a raw string to a base-64 string - */ -function rstr2b64(input) -{ - try { b64pad } catch(e) { b64pad=''; } - var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - var output = ""; - var len = input.length; - for(var i = 0; i < len; i += 3) - { - var triplet = (input.charCodeAt(i) << 16) - | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) - | (i + 2 < len ? input.charCodeAt(i+2) : 0); - for(var j = 0; j < 4; j++) - { - if(i * 8 + j * 6 > input.length * 8) output += b64pad; - else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); - } - } - return output; -} - -/* - * Convert a raw string to an arbitrary string encoding - */ -function rstr2any(input, encoding) -{ - var divisor = encoding.length; - var i, j, q, x, quotient; - - /* Convert to an array of 16-bit big-endian values, forming the dividend */ - var dividend = Array(Math.ceil(input.length / 2)); - for(i = 0; i < dividend.length; i++) - { - dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); - } - - /* - * Repeatedly perform a long division. The binary array forms the dividend, - * the length of the encoding is the divisor. Once computed, the quotient - * forms the dividend for the next step. All remainders are stored for later - * use. - */ - var full_length = Math.ceil(input.length * 8 / - (Math.log(encoding.length) / Math.log(2))); - var remainders = Array(full_length); - for(j = 0; j < full_length; j++) - { - quotient = Array(); - x = 0; - for(i = 0; i < dividend.length; i++) - { - x = (x << 16) + dividend[i]; - q = Math.floor(x / divisor); - x -= q * divisor; - if(quotient.length > 0 || q > 0) - quotient[quotient.length] = q; - } - remainders[j] = x; - dividend = quotient; - } - - /* Convert the remainders to the output string */ - var output = ""; - for(i = remainders.length - 1; i >= 0; i--) - output += encoding.charAt(remainders[i]); - - return output; -} - -/* - * Encode a string as utf-8. - * For efficiency, this assumes the input is valid utf-16. - */ -function str2rstr_utf8(input) -{ - var output = ""; - var i = -1; - var x, y; - - while(++i < input.length) - { - /* Decode utf-16 surrogate pairs */ - x = input.charCodeAt(i); - y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; - if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) - { - x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); - i++; - } - - /* Encode output as utf-8 */ - if(x <= 0x7F) - output += String.fromCharCode(x); - else if(x <= 0x7FF) - output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), - 0x80 | ( x & 0x3F)); - else if(x <= 0xFFFF) - output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), - 0x80 | ((x >>> 6 ) & 0x3F), - 0x80 | ( x & 0x3F)); - else if(x <= 0x1FFFFF) - output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), - 0x80 | ((x >>> 12) & 0x3F), - 0x80 | ((x >>> 6 ) & 0x3F), - 0x80 | ( x & 0x3F)); - } - return output; -} - -/* - * Encode a string as utf-16 - */ -function str2rstr_utf16le(input) -{ - var output = ""; - for(var i = 0; i < input.length; i++) - output += String.fromCharCode( input.charCodeAt(i) & 0xFF, - (input.charCodeAt(i) >>> 8) & 0xFF); - return output; -} - -function str2rstr_utf16be(input) -{ - var output = ""; - for(var i = 0; i < input.length; i++) - output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, - input.charCodeAt(i) & 0xFF); - return output; -} - -/* - * Convert a raw string to an array of little-endian words - * Characters >255 have their high-byte silently ignored. - */ -function rstr2binl(input) -{ - var output = Array(input.length >> 2); - for(var i = 0; i < output.length; i++) - output[i] = 0; - for(var i = 0; i < input.length * 8; i += 8) - output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32); - return output; -} - -/* - * Convert an array of little-endian words to a string - */ -function binl2rstr(input) -{ - var output = ""; - for(var i = 0; i < input.length * 32; i += 8) - output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF); - return output; -} - -/* - * Calculate the MD5 of an array of little-endian words, and a bit length. - */ -function binl_md5(x, len) -{ - /* append padding */ - x[len >> 5] |= 0x80 << ((len) % 32); - x[(((len + 64) >>> 9) << 4) + 14] = len; - - var a = 1732584193; - var b = -271733879; - var c = -1732584194; - var d = 271733878; - - for(var i = 0; i < x.length; i += 16) - { - var olda = a; - var oldb = b; - var oldc = c; - var oldd = d; - - a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); - d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); - c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); - b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); - a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); - d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); - c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); - b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); - a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); - d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); - c = md5_ff(c, d, a, b, x[i+10], 17, -42063); - b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); - a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); - d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); - c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); - b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); - - a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); - d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); - c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); - b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); - a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); - d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); - c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); - b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); - a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); - d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); - c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); - b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); - a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); - d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); - c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); - b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); - - a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); - d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); - c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); - b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); - a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); - d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); - c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); - b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); - a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); - d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); - c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); - b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); - a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); - d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); - c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); - b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); - - a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); - d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); - c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); - b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); - a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); - d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); - c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); - b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); - a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); - d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); - c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); - b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); - a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); - d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); - c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); - b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); - - a = safe_add(a, olda); - b = safe_add(b, oldb); - c = safe_add(c, oldc); - d = safe_add(d, oldd); - } - return Array(a, b, c, d); -} - -/* - * These functions implement the four basic operations the algorithm uses. - */ -function md5_cmn(q, a, b, x, s, t) -{ - return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); -} -function md5_ff(a, b, c, d, x, s, t) -{ - return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); -} -function md5_gg(a, b, c, d, x, s, t) -{ - return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); -} -function md5_hh(a, b, c, d, x, s, t) -{ - return md5_cmn(b ^ c ^ d, a, b, x, s, t); -} -function md5_ii(a, b, c, d, x, s, t) -{ - return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); -} - -/* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ -function safe_add(x, y) -{ - var lsw = (x & 0xFFFF) + (y & 0xFFFF); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); -} - -/* - * Bitwise rotate a 32-bit number to the left. - */ -function bit_rol(num, cnt) -{ - return (num << cnt) | (num >>> (32 - cnt)); -} diff --git a/public/javascripts/variant.js b/public/javascripts/variant.js index ae576d89..736d8b3d 100644 --- a/public/javascripts/variant.js +++ b/public/javascripts/variant.js @@ -2,6 +2,7 @@ new Vue({ el: "#variantPage", data: { display: "room", //default: main hall + gameid: "undefined", //...yet }, created: function() { // TODO: navigation becomes a little more complex diff --git a/public/stylesheets/index.sass b/public/stylesheets/index.sass index 0d7803f4..e38f87dd 100644 --- a/public/stylesheets/index.sass +++ b/public/stylesheets/index.sass @@ -14,7 +14,7 @@ display: inline-block padding: 3px border: 1px solid black; - margin: 27px 15px 5px 7px + margin: 25px 15px 5px 7px @media screen and (max-width: 767px) margin-top: 7px @@ -34,31 +34,19 @@ font-weight: bold padding: 0 border: none - margin-top: 22px + margin-top: 21px font-size: 1.5em @media screen and (max-width: 767px) margin-top: 10px font-size: 1em -#helpMenu +#settingsMenu, #introductionMenu float: right @media screen and (max-width: 767px) .info-container p margin-right: 5px -#flagMenu - float: right - margin-right: 10px - @media screen and (max-width: 767px) - margin-right: 5px - img - display: inline-block - height: 30px - padding-top: 27px - @media screen and (max-width: 767px) - padding-top: 8px - // TODO: box-shadow or box-sizing ? https://stackoverflow.com/a/13517809 .variant box-sizing: border-box @@ -76,11 +64,6 @@ @media screen and (max-width: 767px) margin-top: 0 -#readThis - margin-top: 0 - color: var(--a-link-color) - text-decoration: underline - #welcome max-width: 767px @media screen and (max-width: 767px) @@ -92,7 +75,7 @@ max-width: 552px ul list-style-type: none - // TODO: bad practice, use table to align things... + // TODO: bad practice, shouldn't use table to align things... table.list-table width: 300px margin: 0 auto @@ -107,5 +90,3 @@ padding: 0 text-align: left border: 0 - #disableMsg - color: darkred diff --git a/public/stylesheets/layout.sass b/public/stylesheets/layout.sass index d4f6d6d0..dedab08e 100644 --- a/public/stylesheets/layout.sass +++ b/public/stylesheets/layout.sass @@ -7,7 +7,6 @@ html, * body padding: 0 min-width: 320px - margin-bottom: 10px .container padding: 0 @@ -34,6 +33,35 @@ body border-left: 1px solid var(--button-group-border-color) border-top: 0 +#settings, #contactForm + max-width: 767px + @media screen and (max-width: 767px) + max-width: 100vw + +#emailSent + color: blue + display: none + +footer + height: 77px + background-color: #000033 + div + line-height: 77px + a + margin: 0 10px 0 0 + display: inline-block + &:visited, &:link + color: white + p + margin: 0 0 0 10px + display: inline-block + color: white + text-decoration: underline + @media screen and (max-width: 767px) + height: 43px + div + line-height: 43px + a text-decoration: underline diff --git a/routes/all.js b/routes/all.js index 79f7c3d8..7c2a6da9 100644 --- a/routes/all.js +++ b/routes/all.js @@ -1,138 +1,8 @@ -let express = require('express'); -let router = express.Router(); -const createError = require('http-errors'); -const sqlite3 = require('sqlite3');//.verbose(); -const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite"); -const db = new sqlite3.Database(DbPath); -const sanitizeHtml = require('sanitize-html'); -const MaxNbProblems = 20; +var router = require("express").Router(); -const supportedLang = ["en","es","fr"]; -function selectLanguage(req, res) -{ - // If preferred language already set: - if (!!req.cookies["lang"]) - return req.cookies["lang"]; - - // Else: search and set it - const langString = req.headers["accept-language"]; - let langArray = langString - .replace(/;q=[0-9.]+/g, "") //priority - .replace(/-[A-Z]+/g, "") //region (skipped for now...) - .split(",") //may have some duplicates, but removal is too costly - let bestLang = "en"; //default: English - for (let lang of langArray) - { - if (supportedLang.includes(lang)) - { - bestLang = lang; - break; - } - } - // Cookie expires in 183 days (expressed in milliseconds) - res.cookie('lang', bestLang, { maxAge: 183*24*3600*1000 }); - return bestLang; -} - -// Home -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), - languages: supportedLang, - }); - }); - }); -}); - -// Variant -router.get("/:variant([a-zA-Z0-9]+)", (req,res,next) => { - const vname = req.params["variant"]; - db.serialize(function() { - db.all("SELECT * FROM Variants WHERE name='" + vname + "'", (err,variant) => { - if (!!err) - return next(err); - if (!variant || variant.length==0) - return next(createError(404)); - // Get only N most recent problems - const query2 = "SELECT * FROM Problems " + - "WHERE variant='" + vname + "' " + - "ORDER BY added DESC " + - "LIMIT " + MaxNbProblems; - db.all(query2, (err2,problems) => { - if (!!err2) - return next(err2); - res.render('variant', { - title: vname + ' Variant', - variant: vname, - problemArray: problems, - lang: selectLanguage(req, res), - languages: supportedLang, - }); - }); - }); - }); -}); - -// Load a rules page (AJAX) -router.get("/rules/:variant([a-zA-Z0-9]+)", (req,res) => { - if (!req.xhr) - return res.json({errmsg: "Unauthorized access"}); - const lang = selectLanguage(req, res); - res.render("rules/" + req.params["variant"] + "/" + lang); -}); - -// 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"]; - 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}); - }); - }); -}); - -// 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"]; - 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(); - if (instructions.length == 0) - return res.json({errmsg: "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({}); -}); +router.use("/", require("./index")); +router.use("/", require("./variant")); +router.use("/", require("./problems")); +router.use("/", require("./messages")); module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 00000000..ab44cf5e --- /dev/null +++ b/routes/index.js @@ -0,0 +1,21 @@ +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")); + +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), + }); + }); + }); +}); + +module.exports = router; diff --git a/routes/messages.js b/routes/messages.js new file mode 100644 index 00000000..7e60b7cd --- /dev/null +++ b/routes/messages.js @@ -0,0 +1,20 @@ +let router = require("express").Router(); +const sendEmail = require(__dirname.replace("/routes", "/utils/sendEmail")); + +// 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 subject = req.body["subject"]; + const content = req.body["content"]; + // TODO: sanitize ? + sendEmail(email, subject, content, err => { + if (!!err) + return res.json({errmsg:err}); + // OK, everything fine + res.json({}); //ignored + }); +}); + +module.exports = router; diff --git a/routes/problems.js b/routes/problems.js new file mode 100644 index 00000000..62a2da15 --- /dev/null +++ b/routes/problems.js @@ -0,0 +1,58 @@ +let router = require("express").Router(); +const sqlite3 = require('sqlite3'); +const DbPath = __dirname.replace("/routes", "/db/vchess.sqlite"); +const db = new sqlite3.Database(DbPath); +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"]; + 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}); + }); + }); +}); + +// 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"]; + 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(); + if (instructions.length == 0) + return res.json({errmsg: "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({}); +}); + +// TODO: edit, delete a problem + +module.exports = router; diff --git a/routes/variant.js b/routes/variant.js new file mode 100644 index 00000000..44b7d804 --- /dev/null +++ b/routes/variant.js @@ -0,0 +1,33 @@ +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")); + +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), + }); + }); + }); +}); + +// Load a rules page (AJAX) +router.get("/rules/:variant([a-zA-Z0-9]+)", (req,res) => { + if (!req.xhr) + return res.json({errmsg: "Unauthorized access"}); + const lang = selectLanguage(req, res); + res.render("rules/" + req.params["variant"] + "/" + lang); +}); + +module.exports = router; diff --git a/utils/language.js b/utils/language.js new file mode 100644 index 00000000..52c3842d --- /dev/null +++ b/utils/language.js @@ -0,0 +1,27 @@ +// Select a language based on browser preferences, or cookie +module.exports = function(req, res) +{ + // If preferred language already set: + if (!!req.cookies["lang"]) + return req.cookies["lang"]; + + // Else: search and set it + const supportedLang = ["en","es","fr"]; + const langString = req.headers["accept-language"]; + let langArray = langString + .replace(/;q=[0-9.]+/g, "") //priority + .replace(/-[A-Z]+/g, "") //region (skipped for now...) + .split(",") //may have some duplicates, but removal is too costly + let bestLang = "en"; //default: English + for (let lang of langArray) + { + if (supportedLang.includes(lang)) + { + bestLang = lang; + break; + } + } + // Cookie expires in 183 days (expressed in milliseconds) + res.cookie('lang', bestLang, { maxAge: 183*24*3600*1000 }); + return bestLang; +} diff --git a/utils/sendEmail.js.dist b/utils/sendEmail.js.dist new file mode 100644 index 00000000..cad123e8 --- /dev/null +++ b/utils/sendEmail.js.dist @@ -0,0 +1,32 @@ +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(); + }); +}; diff --git a/views/contactForm.pug b/views/contactForm.pug new file mode 100644 index 00000000..a07369e3 --- /dev/null +++ b/views/contactForm.pug @@ -0,0 +1,18 @@ +input#modalContact.modal(type="checkbox") +div(role="dialog" aria-labelledby="contactTitle") + form.card.smallpad + label.modal-close(for="modalContact") + h3#contactTitle.section= translations["Contact form"] + fieldset + label(for="userEmail")= translations["Email"] + input#userEmail(type="email") + fieldset + label(for="mailSubject")= translations["Subject"] + input#mailSubject(type="text") + fieldset + label(for="mailContent")= translations["Content"] + br + textarea#mailContent + fieldset + button(type="button" onClick="trySendMessage()") Send + p#emailSent= translations["Email sent!"] diff --git a/views/index.pug b/views/index.pug index 5d27eb8a..dd095931 100644 --- a/views/index.pug +++ b/views/index.pug @@ -14,21 +14,23 @@ block content include welcome/fr .row #header.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 - #mainTitle.clickable( - onClick="document.getElementById('modalWelcome').checked=true") + #mainTitle img(src="/images/index/unicorn.svg") .info-container p vchess.club img(src="/images/index/wildebeest.svg") - #settings.clickable( + #settingsMenu.clickable( onClick="document.getElementById('modalSettings').checked=true") - i.material-icons settings + .info-container + p Settings + #introductionMenu.clickable( + onClick="document.getElementById('modalWelcome').checked=true") + .info-container + p Introduction .row my-variant-summary(v-for="(v,idx) in sortedCounts" v-bind:vobj="v" v-bind:index="idx" v-bind:key="v.name") - redesign index page :: lien github, lien contact mail, settings - block javascripts script. const variantArray = !{JSON.stringify(variantArray)}; diff --git a/views/layout.pug b/views/layout.pug index d894ff0c..da99fc31 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -25,20 +25,29 @@ html body + include langNames + case lang + when "en" + include translations/en + when "es" + include translations/es + when "fr" + include translations/fr + include contactForm + include settings main - include langNames - case lang - when "en" - include translations/en - when "es" - include translations/es - when "fr" - include translations/fr - include settings block content + footer.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2.text-center + div + a(href="https://github.com/yagu0/vchess") Source code + p.clickable(onClick="document.getElementById('modalContact').checked=true") + =translations["Contact"] script(src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js") + script(src="/javascripts/utils/ajax.js") script(src="/javascripts/layout.js") + script(src="/javascripts/settings.js") + script(src="/javascripts/contactForm.js") if development script(src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js") else diff --git a/views/settings.pug b/views/settings.pug index c46c5f8f..063e61a1 100644 --- a/views/settings.pug +++ b/views/settings.pug @@ -1,15 +1,15 @@ -input#modal-settings.modal(type="checkbox") +input#modalSettings.modal(type="checkbox") div(role="dialog" aria-labelledby="settingsTitle") .card.smallpad(onChange="blabla(event)") - label#close-settings.modal-close(for="modal-settings") + label.modal-close(for="modalSettings") h3#settingsTitle.section= translations["Preferences"] fieldset label(for="langSelect")= translations["Language"] // image avec drapeau + select language ici select#langSelect - each langCode in languages + each language,langCode in langName option(value=langCode selected=(lang==langCode)) - =langName[langCode] + =language fieldset label(for="nameSetter") =translations["My name is..."] diff --git a/views/translations/en.pug b/views/translations/en.pug index fe67aba9..96633111 100644 --- a/views/translations/en.pug +++ b/views/translations/en.pug @@ -2,6 +2,13 @@ var translations = { "Language": "Language", + "Contact": "Contact", + "Email": "Email", + "Subject": "Subject", + "Content": "Content", + "Email sent!": "Email sent!", + "Hall": "Hall", + "My games": "My games", // Index page: "Help": "Help", diff --git a/views/variant.pug b/views/variant.pug index ca9091bf..a43ba601 100644 --- a/views/variant.pug +++ b/views/variant.pug @@ -16,7 +16,7 @@ block content a(href="#room" @click="setDisplay('room')") =translations["Hall"] a(href="#gameList" @click="setDisplay('gameList')") - =translations["Play"] + =translations["My games"] a(href="#rules" @click="setDisplay('rules')") =translations["Rules"] a(href="#problems" @click="setDisplay('problems')") @@ -30,14 +30,12 @@ block content my-rules(v-show="display=='rules'") my-problems(v-show="display=='problems'") // my-game: for room and games-list components - my-game(v-show="display=='game'" :gameId="") + my-game(v-show="display=='game'" :gameId="gameid") block javascripts script(src="/javascripts/utils/misc.js") script(src="/javascripts/utils/array.js") - script(src="/javascripts/utils/md5.js") script(src="/javascripts/utils/printDiagram.js") - script(src="/javascripts/utils/ajax.js") script(src="/javascripts/utils/datetime.js") script(src="/javascripts/socket_url.js") script(src="/javascripts/base_rules.js") @@ -45,7 +43,6 @@ block javascripts script. const V = VariantRules; //because this variable is often used const variant = "#{variant}"; - const problemArray = !{JSON.stringify(problemArray)}; script(src="/javascripts/components/rules.js") script(src="/javascripts/components/game.js") script(src="/javascripts/components/problemSummary.js") -- 2.44.0