From a3ac374ba213c7044db6cbcfafb81d4b66a0a290 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sun, 9 Feb 2020 13:14:36 +0100
Subject: [PATCH] Experimental improved behavior of login/logout/multitabs

---
 client/src/components/UpsertUser.vue | 41 +++++++----------------
 client/src/router.js                 | 29 ++++------------
 client/src/translations/en.js        |  2 ++
 client/src/translations/es.js        |  2 ++
 client/src/translations/fr.js        |  2 ++
 client/src/views/About.vue           | 30 -----------------
 client/src/views/Auth.vue            | 50 ++++++++++++++++++++++++++++
 client/src/views/Game.vue            | 13 +-------
 client/src/views/Hall.vue            | 36 ++------------------
 client/src/views/Logout.vue          | 43 ++++++++++++++++++++++++
 server/sockets.js                    | 44 ++++++++++++++----------
 11 files changed, 148 insertions(+), 144 deletions(-)
 create mode 100644 client/src/views/Auth.vue
 create mode 100644 client/src/views/Logout.vue

diff --git a/client/src/components/UpsertUser.vue b/client/src/components/UpsertUser.vue
index 2b28f64b..60970b08 100644
--- a/client/src/components/UpsertUser.vue
+++ b/client/src/components/UpsertUser.vue
@@ -9,13 +9,13 @@ div
         div(v-show="stage!='Login'")
           fieldset
             label(for="username") {{ st.tr["Name"] }}
-            input#username(type="text" v-model="user.name")
+            input#username(type="text" v-model="st.user.name")
           fieldset
             label(for="useremail") {{ st.tr["Email"] }}
-            input#useremail(type="email" v-model="user.email")
+            input#useremail(type="email" v-model="st.user.email")
           fieldset
             label(for="notifyNew") {{ st.tr["Notifications by email"] }}
-            input#notifyNew(type="checkbox" v-model="user.notify")
+            input#notifyNew(type="checkbox" v-model="st.user.notify")
         div(v-show="stage=='Login'")
           fieldset
             label(for="nameOrEmail") {{ st.tr["Name or Email"] }}
@@ -38,7 +38,6 @@ export default {
   name: 'my-upsert-user',
   data: function() {
     return {
-      user: store.state.user,
       nameOrEmail: "", //for login
       logStage: "Login", //or Register
       infoMsg: "",
@@ -50,13 +49,13 @@ export default {
     nameOrEmail: function(newValue) {
       if (newValue.indexOf('@') >= 0)
       {
-        this.user.email = newValue;
-        this.user.name = "";
+        this.st.user.email = newValue;
+        this.st.user.name = "";
       }
       else
       {
-        this.user.name = newValue;
-        this.user.email = "";
+        this.st.user.name = newValue;
+        this.st.user.email = "";
       }
     },
   },
@@ -76,7 +75,7 @@ export default {
       return (this.infoMsg.length > 0 ? "block" : "none");
     },
     stage: function() {
-      return this.user.id > 0 ? "Update" : this.logStage;
+      return this.st.user.id > 0 ? "Update" : this.logStage;
     },
   },
   methods: {
@@ -133,12 +132,12 @@ export default {
         error = checkNameEmail({[type]: this.nameOrEmail});
       }
       else
-        error = checkNameEmail(this.user);
+        error = checkNameEmail(this.st.user);
       if (!!error)
         return alert(error);
       this.infoMsg = "Processing... Please wait";
       ajax(this.ajaxUrl(), this.ajaxMethod(),
-        this.stage == "Login" ? { nameOrEmail: this.nameOrEmail } : this.user,
+        this.stage == "Login" ? { nameOrEmail: this.nameOrEmail } : this.st.user,
         res => {
           this.infoMsg = this.infoMessage();
           if (this.stage != "Update")
@@ -155,24 +154,8 @@ export default {
       );
     },
     doLogout: function() {
-      let logoutBtn = document.getElementById("logoutBtn");
-      logoutBtn.disabled = true;
-      // NOTE: this local cleaning would logically happen when we're sure
-      // that token is erased. But in the case a user clear the cookies,
-      // it would lead to situations where he cannot ("locally") log out.
-      // At worst, if token deletion fails the user can erase cookie manually.
-      this.user.id = 0;
-      this.user.name = "";
-      this.user.email = "";
-      this.user.notify = false;
-      localStorage.removeItem("myid");
-      localStorage.removeItem("myname");
-      ajax("/logout", "GET", () => {
-        logoutBtn.disabled = false; //for symmetry, but not very useful...
-        document.getElementById("modalUser").checked = false;
-        // this.$router.push("/") will fail if logout from Hall, so:
-        document.location.reload(true);
-      });
+      document.getElementById("modalUser").checked = false;
+      this.$router.push("/logout");
     },
   },
 };
diff --git a/client/src/router.js b/client/src/router.js
index 599cfcb3..5563980b 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -31,29 +31,12 @@ const router = new Router({
     {
       path: "/authenticate/:token",
       name: "authenticate",
-      beforeEnter: (to, from, next) => {
-        ajax(
-          "/authenticate",
-          "GET",
-          {token: to.params["token"]},
-          (res) => {
-            if (!res.errmsg) //if not already logged in
-            {
-              store.state.user.id = res.id;
-              store.state.user.name = res.name;
-              store.state.user.email = res.email;
-              store.state.user.notify = res.notify;
-              localStorage["myname"] = res.name;
-              localStorage["myid"] = res.id;
-            }
-            // TODO: I don't like these 2 lines, "next('/')" should be enough
-            window.location = "/";
-            next();
-          }
-        );
-      },
-      component: Hall,
-      //redirect: "/", //problem: redirection before end of AJAX request
+      component: loadView("Auth"),
+    },
+    {
+      path: "/logout",
+      name: "logout",
+      component: loadView("Logout"),
     },
     {
       path: "/mygames",
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index ae1101df..ee24720b 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -6,6 +6,7 @@ export const translations =
   "All": "All",
   "Analyze": "Analyze",
   "Analyze in Dark mode makes no sense!": "Analyze in Dark mode makes no sense!",
+  "Authentication successful!": "Authentication successful!",
   "Apply": "Apply",
   "Available": "Available",
   "Black": "Black",
@@ -45,6 +46,7 @@ export const translations =
   "Live games": "Live games",
   "Login": "Login",
   "Logout": "Logout",
+  "Logout successful!": "Logout successful!",
   "Modifications applied!": "Modifications applied!",
   "Mutual agreement": "Mutual agreement",
   "My games": "My games",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 0e9724b8..32d6e294 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -7,6 +7,7 @@ export const translations =
   "Analyze": "Analizar",
   "Analyze in Dark mode makes no sense!": "¡ Analizar en modo Dark no tiene sentido !",
   "Apply": "Aplicar",
+  "Authentication successful!": "¡ Autenticación exitosa !",
   "Available": "Disponible",
   "Black": "Negras",
   "Black to move": "Juegan las negras",
@@ -45,6 +46,7 @@ export const translations =
   "Live games": "Partidas en vivo",
   "Login": "Login",
   "Logout": "Logout",
+  "Logout successful!": "¡ Desconexión exitosa !",
   "Modifications applied!": "¡ Modificaciones aplicadas !",
   "Mutual agreement": "Acuerdo mutuo",
   "My games": "Mis partidas",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 15673519..525eb20c 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -7,6 +7,7 @@ export const translations =
   "Analyze": "Analyser",
   "Analyze in Dark mode makes no sense!": "Analyser en mode Dark n'a pas de sens !",
   "Apply": "Appliquer",
+  "Authentication successful!": "Authentification réussie !",
   "Available": "Disponible",
   "Black": "Noirs",
   "Black to move": "Trait aux noirs",
@@ -45,6 +46,7 @@ export const translations =
   "Live games": "Parties en direct",
   "Login": "Login",
   "Logout": "Logout",
+  "Logout successful!": "Déconnection réussie !",
   "Modifications applied!": "Modifications effectuées !",
   "Mutual agreement": "Accord mutuel",
   "My games": "Mes parties",
diff --git a/client/src/views/About.vue b/client/src/views/About.vue
index 3b1a2cb9..7f3e687b 100644
--- a/client/src/views/About.vue
+++ b/client/src/views/About.vue
@@ -20,33 +20,3 @@ export default {
   },
 };
 </script>
-
-<style lang="sass">
-.warn
-  padding: 3px
-  color: red
-  background-color: lightgrey
-  font-weight: bold
-
-p.boxed
-  background-color: #FFCC66
-  padding: 5px
-
-.stageDelimiter
-  color: purple
-
-.section-title
-  padding: 0
-
-.section-title > h4
-  padding: 5px
-
-ol, ul:not(.browser-default)
-  padding-left: 20px
-
-ul:not(.browser-default)
-  margin-top: 5px
-
-ul:not(.browser-default) > li
-  list-style-type: disc
-</style>
diff --git a/client/src/views/Auth.vue b/client/src/views/Auth.vue
new file mode 100644
index 00000000..f892fd55
--- /dev/null
+++ b/client/src/views/Auth.vue
@@ -0,0 +1,50 @@
+<template lang="pug">
+main
+  .row
+    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+      p(:class="{warn:!!this.errmsg}")
+        | {{ errmsg || st.tr["Authentication successful!"] }}
+</template>
+
+<script>
+import { store } from "@/store";
+import { ajax } from "@/utils/ajax";
+
+export default {
+  name: 'my-auth',
+  data: function() {
+    return {
+      st: store.state,
+      errmsg: "",
+    };
+  },
+  created: function() {
+		ajax(
+			"/authenticate",
+			"GET",
+			{token: this.$route.params["token"]},
+			(res) => {
+				if (!res.errmsg) //if not already logged in
+				{
+					this.st.user.id = res.id;
+					this.st.user.name = res.name;
+					this.st.user.email = res.email;
+					this.st.user.notify = res.notify;
+					localStorage["myname"] = res.name;
+					localStorage["myid"] = res.id;
+				}
+				else
+          this.errmsg = res.errmsg;
+			}
+    );
+  },
+};
+</script>
+
+<style lang="sass" scoped>
+.warn
+  padding: 3px
+  color: red
+  background-color: lightgrey
+  font-weight: bold
+</style>
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 0072bd43..e3a9408b 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -165,11 +165,11 @@ export default {
       switch (data.code)
       {
         case "duplicate":
+          this.st.conn.send(JSON.stringify({code:"duplicate"}));
           alert(this.st.tr["Warning: multi-tabs not supported"]);
           break;
         // 0.2] Receive clients list (just socket IDs)
         case "pollclients":
-        {
           data.sockIds.forEach(sid => {
             if (!!this.people[sid])
               return;
@@ -178,9 +178,7 @@ export default {
             this.st.conn.send(JSON.stringify({code:"askidentity", target:sid}));
           });
           break;
-        }
         case "askidentity":
-        {
           // Request for identification: reply if I'm not anonymous
           if (this.st.user.id > 0)
           {
@@ -194,9 +192,7 @@ export default {
               target:data.from}));
           }
           break;
-        }
         case "identity":
-        {
           this.$set(this.people, data.user.sid,
             {id: data.user.id, name: data.user.name});
           // Ask potentially missed last state, if opponent and I play
@@ -207,9 +203,7 @@ export default {
             this.st.conn.send(JSON.stringify({code:"asklastate", target:data.user.sid}));
           }
           break;
-        }
         case "asklastate":
-        {
           // Sending last state if I played a move or score != "*"
           if ((this.game.moves.length > 0 && this.vr.turn != this.game.mycolor)
               || this.game.score != "*" || this.drawOffer == "sent")
@@ -234,7 +228,6 @@ export default {
             }));
           }
           break;
-        }
         case "askgame":
           // Send current (live) game if I play in (not an observer),
           // and not asked by opponent (!)
@@ -271,13 +264,11 @@ export default {
             document.getElementById("chatBtn").style.backgroundColor = "#c5fefe";
           break;
         case "lastate": //got opponent infos about last move
-        {
           this.lastate = data.state;
           if (this.game.rendered) //game is rendered (Board component)
             this.processLastate();
           //else: will be processed when game is ready
           break;
-        }
         case "resign":
           this.gameOver(data.side=="b" ? "1-0" : "0-1", "Resign");
           break;
@@ -300,11 +291,9 @@ export default {
           this.loadGame(data.game, this.roomInit);
           break;
         case "connect":
-        {
           this.$set(this.people, data.from, {name:"", id:0});
           this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
           break;
-        }
         case "disconnect":
           this.$delete(this.people, data.from);
           break;
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index bbff9b95..5fcbb31a 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -136,20 +136,6 @@ export default {
     // Always add myself to players' list
     const my = this.st.user;
     this.$set(this.people, my.sid, {id:my.id, name:my.name});
-    // Retrieve live challenge (not older than 30 minute) if any:
-    const chall = JSON.parse(localStorage.getItem("challenge") || "false");
-    if (!!chall)
-    {
-      // NOTE: a challenge survives 3 minutes, for potential connection issues
-      if ((Date.now() - chall.added)/1000 <= 3*60)
-      {
-        chall.added = Date.now(); //update added time, for next disconnect...
-        this.challenges.push(chall);
-        localStorage.setItem("challenge", JSON.stringify(chall));
-      }
-      else
-        localStorage.removeItem("challenge");
-    }
     // Ask server for current corr games (all but mines)
     ajax(
       "/games",
@@ -321,6 +307,8 @@ export default {
       switch (data.code)
       {
         case "duplicate":
+          this.st.conn.send(JSON.stringify({code:"duplicate"}));
+          this.st.conn.send = () => {};
           alert(this.st.tr["Warning: multi-tabs not supported"]);
           break;
         // 0.2] Receive clients list (just socket IDs)
@@ -343,7 +331,6 @@ export default {
           this.st.conn.send(JSON.stringify({code:"askgames"}));
           break;
         case "askidentity":
-        {
           // Request for identification: reply if I'm not anonymous
           if (this.st.user.id > 0)
           {
@@ -357,9 +344,7 @@ export default {
               target:data.from}));
           }
           break;
-        }
         case "identity":
-        {
           this.$set(this.people, data.user.sid,
             {
               id: data.user.id,
@@ -367,7 +352,6 @@ export default {
               gamer: this.people[data.user.sid].gamer,
             });
           break;
-        }
         case "askchallenge":
         {
           // Send my current live challenge (if any)
@@ -404,7 +388,6 @@ export default {
           break;
         }
         case "challenge":
-        {
           // Receive challenge from some player (+sid)
           // NOTE about next condition: see "askchallenge" case.
           if (!data.chall.to || data.chall.to == this.st.user.name)
@@ -417,7 +400,6 @@ export default {
             this.challenges.push(newChall);
           }
           break;
-        }
         case "game":
         {
           // Receive game from some player (+sid)
@@ -442,7 +424,6 @@ export default {
           break;
         }
         case "newgame":
-        {
           // New game just started: data contain all information
           if (this.classifyObject(data.gameInfo) == "live")
             this.startNewGame(data.gameInfo);
@@ -456,24 +437,17 @@ export default {
             setTimeout(() => { modalBox.checked = false; }, 3000);
           }
           break;
-        }
         case "newchat":
           this.newChat = data.chat;
           break;
         case "refusechallenge":
-        {
           ArrayFun.remove(this.challenges, c => c.id == data.cid);
-          localStorage.removeItem("challenge");
           alert(this.st.tr["Challenge declined"]);
           break;
-        }
         case "deletechallenge":
-        {
           // NOTE: the challenge may be already removed
           ArrayFun.remove(this.challenges, c => c.id == data.cid);
-          localStorage.removeItem("challenge"); //in case of
           break;
-        }
         case "connect":
         case "gconnect":
           this.$set(this.people, data.from, {name:"", id:0, gamer:data.code[0]=='g'});
@@ -576,9 +550,7 @@ export default {
           name: this.st.user.name,
         };
         this.challenges.push(chall);
-        if (ctype == "live")
-          localStorage.setItem("challenge", JSON.stringify(chall));
-        // Also remember timeControl  + vid for quicker further challenges:
+        // Remember timeControl  + vid for quicker further challenges:
         localStorage.setItem("timeControl", chall.timeControl);
         localStorage.setItem("vid", chall.vid);
         document.getElementById("modalNewgame").checked = false;
@@ -639,8 +611,6 @@ export default {
             {id: c.id}
           );
         }
-        else //live
-          localStorage.removeItem("challenge");
         this.sendSomethingTo({name:c.to}, "deletechallenge", {cid:c.id});
       }
       // In all cases, the challenge is consumed:
diff --git a/client/src/views/Logout.vue b/client/src/views/Logout.vue
new file mode 100644
index 00000000..61259d58
--- /dev/null
+++ b/client/src/views/Logout.vue
@@ -0,0 +1,43 @@
+<template lang="pug">
+main
+  .row
+    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+      p(:class="{warn:!!this.errmsg}")
+        | {{ errmsg || st.tr["Logout successful!"] }}
+</template>
+
+<script>
+import { store } from "@/store";
+import { ajax } from "@/utils/ajax";
+
+export default {
+  name: 'my-logout',
+  data: function() {
+    return {
+      st: store.state,
+      errmsg: "",
+    };
+  },
+  created: function() {
+    // NOTE: this local cleaning would logically happen when we're sure
+    // that token is erased. But in the case a user clear the cookies,
+    // it would lead to situations where he cannot ("locally") log out.
+    // At worst, if token deletion fails the user can erase cookie manually.
+    this.st.user.id = 0;
+    this.st.user.name = "";
+    this.st.user.email = "";
+    this.st.user.notify = false;
+    localStorage.removeItem("myid");
+    localStorage.removeItem("myname");
+    ajax("/logout", "GET"); //TODO: listen for errors?
+  },
+};
+</script>
+
+<style lang="sass" scoped>
+.warn
+  padding: 3px
+  color: red
+  background-color: lightgrey
+  font-weight: bold
+</style>
diff --git a/server/sockets.js b/server/sockets.js
index e7bb556c..df44520d 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -18,16 +18,6 @@ module.exports = function(wss) {
   wss.on("connection", (socket, req) => {
     const query = getJsonFromUrl(req.url);
     const sid = query["sid"];
-    if (!!clients[sid])
-    {
-      // Dummy messages listener: just send "duplicate" event on anything
-      // ('connect' events for Hall and Game, 'askfullgame' for observers)
-      return socket.on("message", objtxt => {
-        if (["connect","askfullgame"].includes(JSON.parse(objtxt).code))
-          socket.send(JSON.stringify({code:"duplicate"}));
-      });
-    }
-    clients[sid] = {sock: socket, page: query["page"]};
     const notifyRoom = (page,code,obj={},excluded=[]) => {
       Object.keys(clients).forEach(k => {
         if (k in excluded)
@@ -39,15 +29,26 @@ module.exports = function(wss) {
         }
       });
     };
-    // Wait for "connect" message to notify connection to the room,
-    // because if game loading is slow the message listener might
-    // not be ready too early.
-    socket.on("message", objtxt => {
+    const messageListener = (objtxt) => {
       let obj = JSON.parse(objtxt);
       if (!!obj.target && !clients[obj.target])
         return; //receiver not connected, nothing we can do
       switch (obj.code)
       {
+        case "duplicate":
+          // Turn off message listening, and send disconnect if needed:
+          socket.removeListener("message", messageListener);
+          socket.removeListener("close", closeListener);
+          if (clients[sid].page != obj.page)
+          {
+            notifyRoom(clients[sid].page, "disconnect");
+            if (clients[sid].page.indexOf("/game/") >= 0)
+              notifyRoom("/", "gdisconnect");
+          }
+          break;
+        // Wait for "connect" message to notify connection to the room,
+        // because if game loading is slow the message listener might
+        // not be ready too early.
         case "connect":
         {
           const curPage = clients[sid].page;
@@ -197,13 +198,22 @@ module.exports = function(wss) {
             {code:"draw", message:obj.message}));
           break;
       }
-    });
-    socket.on("close", () => {
+    };
+    const closeListener = () => {
       const page = clients[sid].page;
       delete clients[sid];
       notifyRoom(page, "disconnect");
       if (page.indexOf("/game/") >= 0)
         notifyRoom("/", "gdisconnect"); //notify main hall
-    });
+    };
+    if (!!clients[sid])
+    {
+      // Turn off old sock through current client:
+      clients[sid].sock.send(JSON.stringify({code:"duplicate"}));
+    }
+    // Potentially replace current connection:
+    clients[sid] = {sock: socket, page: query["page"]};
+    socket.on("message", messageListener);
+    socket.on("close", closeListener);
   });
 }
-- 
2.44.0