User management logic half-debugged
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 9 Jan 2019 10:50:07 +0000 (11:50 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 9 Jan 2019 10:50:07 +0000 (11:50 +0100)
14 files changed:
TODO
models/User.js
models/Variant.js
public/javascripts/components/upsertUser.js
public/javascripts/shared/userCheck.js [new file with mode: 0644]
public/javascripts/utils/ajax.js
public/javascripts/utils/misc.js
routes/users.js
views/error.pug
views/index.pug
views/layout.pug
views/translations/en.pug
views/translations/es.pug
views/translations/fr.pug

diff --git a/TODO b/TODO
index cbe3285..667f8cd 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,3 +1,5 @@
+// TODO: decodeURIComponent() for GET/DELETE parameters
+
 1) Finish problems tab
 2) Integrate computer play into rules tab
 3) Retrieve users system from old code
 1) Finish problems tab
 2) Integrate computer play into rules tab
 3) Retrieve users system from old code
index 777eeaa..6e91458 100644 (file)
@@ -16,13 +16,12 @@ var TokenGen = require("../utils/tokenGenerator");
 // User creation
 exports.create = function(name, email, notify, callback)
 {
 // User creation
 exports.create = function(name, email, notify, callback)
 {
-       if (!notify)
-               notify = false; //default
        db.serialize(function() {
        db.serialize(function() {
-               db.run(
+               const query =
                        "INSERT INTO Users " +
                        "(name, email, notify) VALUES " +
                        "INSERT INTO Users " +
                        "(name, email, notify) VALUES " +
-                       "(" + name + "," + email + "," + notify + ")");
+                       "('" + name + "', '" + email + "', " + notify + ")";
+               db.run(query, callback); //TODO: need to get the inserted user (how ?)
        });
 }
 
        });
 }
 
@@ -31,10 +30,10 @@ exports.getOne = function(by, value, cb)
 {
        const delimiter = (typeof value === "string" ? "'" : "");
        db.serialize(function() {
 {
        const delimiter = (typeof value === "string" ? "'" : "");
        db.serialize(function() {
-               db.get(
+               const query =
                        "SELECT * FROM Users " +
                        "SELECT * FROM Users " +
-                       "WHERE " + by + " = " + delimiter + value + delimiter,
-                       callback);
+                       "WHERE " + by + " = " + delimiter + value + delimiter;
+               db.get(query, cb);
        });
 }
 
        });
 }
 
@@ -44,10 +43,11 @@ exports.getOne = function(by, value, cb)
 exports.setLoginToken = function(token, uid, cb)
 {
        db.serialize(function() {
 exports.setLoginToken = function(token, uid, cb)
 {
        db.serialize(function() {
-               db.run(
+               const query =
                        "UPDATE Users " +
                        "SET loginToken = " + token + " AND loginTime = " + Date.now() + " " +
                        "UPDATE Users " +
                        "SET loginToken = " + token + " AND loginTime = " + Date.now() + " " +
-                       "WHERE id = " + uid);
+                       "WHERE id = " + uid;
+               db.run(query, cb);
        });
 }
 
        });
 }
 
@@ -57,18 +57,20 @@ exports.trySetSessionToken = function(uid, cb)
 {
        // Also empty the login token to invalidate future attempts
        db.serialize(function() {
 {
        // Also empty the login token to invalidate future attempts
        db.serialize(function() {
-               db.get(
+               const querySessionTOken =
                        "SELECT sessionToken " +
                        "FROM Users " +
                        "SELECT sessionToken " +
                        "FROM Users " +
-                       "WHERE id = " + uid, (err,token) => {
-                               if (!!err)
-                                       return cb(err);
-                               const newToken = token || TokenGen.generate(params.token.length);
-                               db.run(
-                                       "UPDATE Users " +
-                                       "SET loginToken = NULL " +
-                                       (!token ? "AND sessionToken = " + newToken + " " : "") +
-                                       "WHERE id = " + uid);
+                       "WHERE id = " + uid;
+               db.get(querySessionToken, (err,token) => {
+                       if (!!err)
+                               return cb(err);
+                       const newToken = token || TokenGen.generate(params.token.length);
+                       const queryUpdate =
+                               "UPDATE Users " +
+                               "SET loginToken = NULL " +
+                               (!token ? "AND sessionToken = " + newToken + " " : "") +
+                               "WHERE id = " + uid;
+                       db.run(queryUpdate);
                                cb(null, newToken);
                });
        });
                                cb(null, newToken);
                });
        });
@@ -77,11 +79,12 @@ exports.trySetSessionToken = function(uid, cb)
 exports.updateSettings = function(user, cb)
 {
        db.serialize(function() {
 exports.updateSettings = function(user, cb)
 {
        db.serialize(function() {
-               db.run(
+               const query =
                        "UPDATE Users " +
                        "SET name = " + user.name +
                        " AND email = " + user.email +
                        " AND notify = " + user.notify + " " +
                        "UPDATE Users " +
                        "SET name = " + user.name +
                        " AND email = " + user.email +
                        " AND notify = " + user.notify + " " +
-                       "WHERE id = " + user._id);
+                       "WHERE id = " + user._id;
+               db.run(query, cb);
        });
 }
        });
 }
index 9a19f18..ce5329c 100644 (file)
@@ -10,17 +10,18 @@ var db = require("../utils/database");
 exports.getByName = function(name, callback)
 {
        db.serialize(function() {
 exports.getByName = function(name, callback)
 {
        db.serialize(function() {
-               db.get(
+               const query =
                        "SELECT * FROM Variants " +
                        "SELECT * FROM Variants " +
-                       "WHERE name='" + name + "'",
-                       callback);
+                       "WHERE name='" + name + "'";
+               db.get(query, callback);
        });
 }
 
 exports.getAll = function(callback)
 {
        db.serialize(function() {
        });
 }
 
 exports.getAll = function(callback)
 {
        db.serialize(function() {
-               db.all("SELECT * FROM Variants", callback);
+               const query = "SELECT * FROM Variants";
+               db.all(query, callback);
        });
 }
 
        });
 }
 
index 29444b4..f996ea1 100644 (file)
@@ -1,41 +1,56 @@
 // Logic to login, or create / update a user (and also logout)
 Vue.component('my-upsert-user', {
 // Logic to login, or create / update a user (and also logout)
 Vue.component('my-upsert-user', {
-       props: ["initUser"], //to find the game in storage (assumption: it exists)
        data: function() {
                return {
        data: function() {
                return {
-                       user: initUser, //initialized with prop value
-                       stage: (!initUser.email ? "Login" : "Update"),
+                       user: user, //initialized with global user object
+                       nameOrEmail: "", //for login
+                       stage: (!user.email ? "Login" : "Update"),
                        infoMsg: "",
                        infoMsg: "",
+                       enterTime: Number.MAX_SAFE_INTEGER, //for a basic anti-bot strategy
                };
        },
        template: `
                <div>
                };
        },
        template: `
                <div>
-                       <input id="modalUser" class="modal" type="checkbox"/>
+                       <input id="modalUser" class="modal" type="checkbox"
+                                       @change="trySetEnterTime"/>
                        <div role="dialog">
                                <div class="card">
                        <div role="dialog">
                                <div class="card">
-                                       <label class="modal-close" for="modalUser">
+                                       <label class="modal-close" for="modalUser"></label>
                                        <h3>{{ stage }}</h3>
                                        <form id="userForm" @submit.prevent="submit">
                                        <h3>{{ stage }}</h3>
                                        <form id="userForm" @submit.prevent="submit">
+                                               <div v-show="stage!='Login'">
+                                                       <fieldset>
+                                                               <label for="username">Name</label>
+                                                               <input id="username" type="text" v-model="user.name"/>
+                                                       </fieldset>
+                                                       <fieldset>
+                                                               <label for="useremail">Email</label>
+                                                               <input id="useremail" type="email" v-model="user.email"/>
+                                                       </fieldset>
+                                                       <fieldset>
+                                                               <label for="notifyNew">Notify new moves &amp; games</label>
+                                                               <input id="notifyNew" type="checkbox" v-model="user.notify"/>
+                                                       </fieldset>
+                                               </div>
+                                               <div v-show="stage=='Login'">
+                                                       <fieldset>
+                                                               <label for="nameOrEmail">Name or Email</label>
+                                                               <input id="nameOrEmail" type="text" v-model="nameOrEmail"/>
+                                                       </fieldset>
+                                               </div>
                                                <fieldset>
                                                <fieldset>
-                                                       <label for="useremail">Email</label>
-                                                       <input id="useremail" type="email" v-model="user.email"/>
-                                               <fieldset>
-                                                       <label for="username">Name</label>
-                                                       <input id="username" type="text" v-model="user.name"/>
+                                                       <button id="submit" @click.prevent="submit">
+                                                               <span>{{ submitMessage }}</span>
+                                                               <i class="material-icons">send</i>
+                                                       </button>
                                                </fieldset>
                                                </fieldset>
-                                               <fieldset>
-                                                       <label for="notifyNew">Notify new moves &amp; games</label>
-                                                       <input id="notifyNew" type="checkbox" v-model="user.notify"/>
-                                               <button id="submit" @click.prevent="submit">
-                                                       <span>{{ submitMessage }}</span>
-                                                       <i class="material-icons">send</i>
-                               <p v-if="stage!='Update'">
-                                       <button @click.prevent="toggleStage()">
+                                       </form>
+                                       <button v-if="stage!='Update'" @click.prevent="toggleStage()">
                                                <span>{{ stage=="Login" ? "Register" : "Login" }}</span>
                                        </button>
                                                <span>{{ stage=="Login" ? "Register" : "Login" }}</span>
                                        </button>
-                                       <button>Logout</button>
-                               </p>
-                               <div id="dialog" :style="{display: displayInfo}">{{ infoMsg }}</div>
+                                       <button v-if="stage=='Update'">Logout</button>
+                                       <div id="dialog" :style="{display: displayInfo}">{{ infoMsg }}</div>
+                               </div>
                        </div>
                </div>
        `,
                        </div>
                </div>
        `,
@@ -56,7 +71,12 @@ Vue.component('my-upsert-user', {
                },
        },
        methods: {
                },
        },
        methods: {
+               trySetEnterTime: function(event) {
+                       if (!!event.target.checked)
+                               this.enterTime = Date.now();
+               },
                toggleStage: function() {
                toggleStage: function() {
+                       // Loop login <--> register (update is for logged-in users)
                        this.stage = (this.stage == "Login" ? "Register" : "Login");
                },
                ajaxUrl: function() {
                        this.stage = (this.stage == "Login" ? "Register" : "Login");
                },
                ajaxUrl: function() {
@@ -93,19 +113,28 @@ Vue.component('my-upsert-user', {
                        }
                },
                submit: function() {
                        }
                },
                submit: function() {
-                       // TODO: re-activate simple measures like this: (using time of click on modal)
-//                     const exitTime = new Date();
-//                     if (this.stage=="Register" && exitTime.getTime() - enterTime.getTime() < 5000)
-//                             return;
-                       if (!this.user.name.match(/[a-z0-9_]+/i))
-                               return alert("User name: only alphanumerics and underscore");
+                       // Basic anti-bot strategy:
+                       const exitTime = Date.now();
+                       if (this.stage == "Register" && exitTime - this.enterTime < 5000)
+                               return; //silently return, in (curious) case of it was legitimate
+                       let error = undefined;
+                       if (this.stage == 'Login')
+                       {
+                               const type = (this.nameOrEmail.indexOf('@') >= 0 ? "email" : "name");
+                               error = checkNameEmail({[type]: this.nameOrEmail});
+                       }
+                       else
+                               error = checkNameEmail(this.user);
+                       if (!!error)
+                               return alert(error);
                        this.infoMsg = "Processing... Please wait";
                        ajax(this.ajaxUrl(), this.ajaxMethod(),
                        this.infoMsg = "Processing... Please wait";
                        ajax(this.ajaxUrl(), this.ajaxMethod(),
-                               this.stage == "Login" ? "PUT" : "POST", this.user,
+                               this.stage == "Login" ? { nameOrEmail: this.nameOrEmail } : this.user,
                                res => {
                                        this.infoMsg = this.infoMessage();
                                        if (this.stage != "Update")
                                        {
                                res => {
                                        this.infoMsg = this.infoMessage();
                                        if (this.stage != "Update")
                                        {
+                                               this.nameOrEmail = "";
                                                this.user["email"] = "";
                                                this.user["name"] = "";
                                        }
                                                this.user["email"] = "";
                                                this.user["name"] = "";
                                        }
@@ -113,6 +142,10 @@ Vue.component('my-upsert-user', {
                                                this.infoMsg = "";
                                                document.getElementById("modalUser").checked = false;
                                        }, 2000);
                                                this.infoMsg = "";
                                                document.getElementById("modalUser").checked = false;
                                        }, 2000);
+                               },
+                               err => {
+                                       this.infoMsg = "";
+                                       alert(err);
                                }
                        );
                },
                                }
                        );
                },
diff --git a/public/javascripts/shared/userCheck.js b/public/javascripts/shared/userCheck.js
new file mode 100644 (file)
index 0000000..bd282ba
--- /dev/null
@@ -0,0 +1,19 @@
+function checkNameEmail(o)
+{
+       if (!!o.name)
+       {
+               if (o.name.length == 0)
+                       return "Empty name";
+               if (!o.name.match(/^[\w]+$/))
+                       return "Bad characters in name";
+       }
+       if (!!o.email)
+       {
+               if (o.email.length == 0)
+                       return "Empty email";
+               if (!o.email.match(/^[\w.+-]+@[\w.+-]+$/))
+                       return "Bad characters in email";
+       }
+}
+
+try { module.exports = checkNameEmail; } catch(e) { } //for server
index a354690..dc21705 100644 (file)
@@ -42,7 +42,6 @@ function ajax(url, method, data, success, error)
                // Append query params to URL
                url += "/?" + toQueryString(data);
        }
                // Append query params to URL
                url += "/?" + toQueryString(data);
        }
-
        xhr.open(method, url, true);
        xhr.setRequestHeader('X-Requested-With', "XMLHttpRequest");
        if (["POST","PUT"].includes(method))
        xhr.open(method, url, true);
        xhr.setRequestHeader('X-Requested-With', "XMLHttpRequest");
        if (["POST","PUT"].includes(method))
index d72e3ce..4ad0766 100644 (file)
@@ -35,3 +35,9 @@ function setLanguage(e)
        setCookie("lang", e.target.value);
        location.reload(); //to include the right .pug file
 }
        setCookie("lang", e.target.value);
        location.reload(); //to include the right .pug file
 }
+
+// Shortcut for an often used click (on a modal)
+function doClick(elemId)
+{
+       document.getElementById(elemId).click(); //or ".checked = true"
+}
index 297072d..f3f5199 100644 (file)
@@ -4,6 +4,7 @@ var sendEmail = require('../utils/mailer');
 var TokenGen = require("../utils/tokenGenerator");
 var access = require("../utils/access");
 var params = require("../config/parameters");
 var TokenGen = require("../utils/tokenGenerator");
 var access = require("../utils/access");
 var params = require("../config/parameters");
+var checkNameEmail = require("../public/javascripts/shared/userCheck")
 
 // to: object user
 function setAndSendLoginToken(subject, to, res)
 
 // to: object user
 function setAndSendLoginToken(subject, to, res)
@@ -13,7 +14,7 @@ function setAndSendLoginToken(subject, to, res)
        UserModel.setLoginToken(token, to._id, (err,ret) => {
                access.checkRequest(res, err, ret, "Cannot set login token", () => {
                        const body =
        UserModel.setLoginToken(token, to._id, (err,ret) => {
                access.checkRequest(res, err, ret, "Cannot set login token", () => {
                        const body =
-                               "Hello " + to.initials + "!\n" +
+                               "Hello " + to.name + "!\n" +
                                "Access your account here: " +
                                params.siteURL + "/authenticate?token=" + token + "\\n" +
                                "Token will expire in " + params.token.expire/(1000*60) + " minutes."
                                "Access your account here: " +
                                params.siteURL + "/authenticate?token=" + token + "\\n" +
                                "Token will expire in " + params.token.expire/(1000*60) + " minutes."
@@ -27,24 +28,26 @@ function setAndSendLoginToken(subject, to, res)
 // AJAX user life cycle...
 
 router.post('/register', access.unlogged, access.ajax, (req,res) => {
 // AJAX user life cycle...
 
 router.post('/register', access.unlogged, access.ajax, (req,res) => {
-       let name = decodeURIComponent(req.body.name);
-       let email = decodeURIComponent(req.body.email);
-       let error = checkObject({name:name, email:email}, "User");
-       if (error.length > 0)
+       const name = req.body.name;
+       const email = req.body.email;
+       const notify = !!req.body.notify;
+       const error = checkNameEmail({name: name, email: email});
+       if (!!error)
                return res.json({errmsg: error});
                return res.json({errmsg: error});
-       UserModel.create(name, email, (err,user) => {
+       UserModel.create(name, email, notify, (err,user) => {
                access.checkRequest(res, err, user, "Registration failed", () => {
                        setAndSendLoginToken("Welcome to " + params.siteURL, user, res);
                });
        });
 });
 
                access.checkRequest(res, err, user, "Registration failed", () => {
                        setAndSendLoginToken("Welcome to " + params.siteURL, user, res);
                });
        });
 });
 
-router.put('/sendtoken', access.unlogged, access.ajax, (req,res) => {
-       let email = decodeURIComponent(req.body.email);
-       let error = checkObject({email:email}, "User");
-       if (error.length > 0)
+router.get('/sendtoken', access.unlogged, access.ajax, (req,res) => {
+       const nameOrEmail = decodeURIComponent(req.query.nameOrEmail);
+       const type = (nameOrEmail.indexOf('@') >= 0 ? "email" : "name");
+       const error = checkNameEmail({[type]: nameOrEmail});
+       if (!!error)
                return res.json({errmsg: error});
                return res.json({errmsg: error});
-       UserModel.getOne("email", email, (err,user) => {
+       UserModel.getOne(type, nameOrEmail, (err,user) => {
                access.checkRequest(res, err, user, "Unknown user", () => {
                        setAndSendLoginToken("Token for " + params.siteURL, user, res);
                });
                access.checkRequest(res, err, user, "Unknown user", () => {
                        setAndSendLoginToken("Token for " + params.siteURL, user, res);
                });
@@ -54,7 +57,6 @@ router.put('/sendtoken', access.unlogged, access.ajax, (req,res) => {
 router.get('/authenticate', access.unlogged, (req,res) => {
        UserModel.getByLoginToken(req.query.token, (err,user) => {
                access.checkRequest(res, err, user, "Invalid token", () => {
 router.get('/authenticate', access.unlogged, (req,res) => {
        UserModel.getByLoginToken(req.query.token, (err,user) => {
                access.checkRequest(res, err, user, "Invalid token", () => {
-                       let tsNow = Date.now();
                        // If token older than params.tokenExpire, do nothing
                        if (Date.now() > user.loginTime + params.token.expire)
                                return res.json({errmsg: "Token expired"});
                        // If token older than params.tokenExpire, do nothing
                        if (Date.now() > user.loginTime + params.token.expire)
                                return res.json({errmsg: "Token expired"});
@@ -75,12 +77,12 @@ router.get('/authenticate', access.unlogged, (req,res) => {
 });
 
 router.put('/settings', access.logged, access.ajax, (req,res) => {
 });
 
 router.put('/settings', access.logged, access.ajax, (req,res) => {
-       const user = JSON.parse(req.body.user);
-       // TODO: either verify email + name, or re-apply the following logic:
-       //let error = checkObject(user, "User");
-       //if (error.length > 0)
-       //      return res.json({errmsg: error});
-       user._id = req.user._id; //TODO:
+       let user = JSON.parse(req.body.user);
+       const error = checkNameEmail({name: user.name, email: user.email});
+       if (!!error)
+               return res.json({errmsg: error});
+       user.notify = !!user.notify; //in case of...
+       user._id = res.locals.user._id; //in case of...
        UserModel.updateSettings(user, (err,ret) => {
                access.checkRequest(res, err, ret, "Settings update failed", () => {
                        res.json({});
        UserModel.updateSettings(user, (err,ret) => {
                access.checkRequest(res, err, ret, "Settings update failed", () => {
                        res.json({});
index 51ec12c..7eb2aff 100644 (file)
@@ -1,6 +1,7 @@
-extends layout
+doctype html
+html
 
 
-block content
+body
   h1= message
   h2= error.status
   pre #{error.stack}
   h1= message
   h2= error.status
   pre #{error.stack}
index 5dfa903..45a2ec7 100644 (file)
@@ -19,11 +19,9 @@ block content
                                        .info-container
                                                p vchess.club
                                        img(src="/images/index/wildebeest.svg")
                                        .info-container
                                                p vchess.club
                                        img(src="/images/index/wildebeest.svg")
-                               #flagMenu.clickable(
-                                               onClick="document.getElementById('modalLang').checked=true")
+                               #flagMenu.clickable(onClick="doClick('modalLang')")
                                        img(src="/images/flags/" + lang + ".svg")
                                        img(src="/images/flags/" + lang + ".svg")
-                               #userMenu.clickable(
-                                               onClick="document.getElementById('modalUser').checked=true")
+                               #userMenu.clickable(onClick="doClick('modalUser')")
                                        .info-container
                                                if !user.email
                                                        p
                                        .info-container
                                                if !user.email
                                                        p
@@ -33,8 +31,7 @@ block content
                                                        p
                                                                span Update
                                                                i.material-icons person
                                                        p
                                                                span Update
                                                                i.material-icons person
-                               #introductionMenu.clickable(
-                                               onClick="document.getElementById('modalWelcome').checked=true")
+                               #introductionMenu.clickable(onClick="doClick('modalWelcome')")
                                        .info-container
                                                p Introduction
                .row
                                        .info-container
                                                p Introduction
                .row
index 03249be..6004fc3 100644 (file)
@@ -24,7 +24,6 @@ html
                block css
 
        body
                block css
 
        body
-
                include langNames
                case lang
                        when "en"
                include langNames
                case lang
                        when "en"
@@ -33,10 +32,10 @@ html
                                include translations/es
                        when "fr"
                                include translations/fr
                                include translations/es
                        when "fr"
                                include translations/fr
-               include contactForm
                include modalLang
                include modalLang
+               include contactForm
                main#VueElement
                main#VueElement
-                       my-upsert-user(:user="user" :stage="stage")
+                       my-upsert-user()
                        block content
                footer.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2.text-center
                        div
                        block content
                footer.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2.text-center
                        div
@@ -53,6 +52,7 @@ html
                        script(src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js")
                else
                        script(src="https://cdn.jsdelivr.net/npm/vue")
                        script(src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js")
                else
                        script(src="https://cdn.jsdelivr.net/npm/vue")
+               script(src="/javascripts/shared/userCheck.js")
                script(src="/javascripts/components/upsertUser.js")
                script.
                        const translations = !{JSON.stringify(translations)};
                script(src="/javascripts/components/upsertUser.js")
                script.
                        const translations = !{JSON.stringify(translations)};
index 9663311..611236c 100644 (file)
@@ -2,7 +2,7 @@
        var translations =
        {
                "Language": "Language",
        var translations =
        {
                "Language": "Language",
-               "Contact": "Contact",
+               "Contact form": "Contact form",
                "Email": "Email",
                "Subject": "Subject",
                "Content": "Content",
                "Email": "Email",
                "Subject": "Subject",
                "Content": "Content",
@@ -32,7 +32,7 @@
                "Pawns move diagonally": "Pawns move diagonally",
                "In the shadow": "In the shadow",
                "Move twice": "Move twice",
                "Pawns move diagonally": "Pawns move diagonally",
                "In the shadow": "In the shadow",
                "Move twice": "Move twice",
-               "Head upside down": "Head upside down",
+               "Board upside down": "Board upside down",
 
                // Variant page:
                "New game": "New game",
 
                // Variant page:
                "New game": "New game",
index 642059e..ce21966 100644 (file)
@@ -25,7 +25,7 @@
                "Pawns move diagonally": "Peones se mueven en diagonal",
                "In the shadow": "En la sombra",
                "Move twice": "Mover dos veces",
                "Pawns move diagonally": "Peones se mueven en diagonal",
                "In the shadow": "En la sombra",
                "Move twice": "Mover dos veces",
-               "Head upside down": "Cabeza al revés",
+               "Board upside down": "Tablero al revés",
 
                // Variant page:
                "New game": "Nueva partida",
 
                // Variant page:
                "New game": "Nueva partida",
index 78b1074..3f3b1fe 100644 (file)
@@ -25,7 +25,7 @@
                "Pawns move diagonally": "Les pions vont en diagonale",
                "In the shadow": "Dans l'ombre",
                "Move twice": "Jouer deux coups",
                "Pawns move diagonally": "Les pions vont en diagonale",
                "In the shadow": "Dans l'ombre",
                "Move twice": "Jouer deux coups",
-               "Head upside down": "La tête à l'envers",
+               "Board upside down": "Échiquier à l'envers",
 
                // Variant page:
                "New game": "Nouvelle partie",
 
                // Variant page:
                "New game": "Nouvelle partie",