From a1f183f216626c80afa28bf0e649e9b6580293de Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 5 Nov 2019 21:29:51 +0200 Subject: [PATCH] Cleanup auth flow --- client/components/Windows/SignIn.vue | 2 +- client/index.html.tpl | 4 +- client/js/loading-error-handlers.js | 63 +++++++-------- client/js/router.js | 2 +- client/js/socket-events/auth.js | 113 +++++++++++++++------------ client/js/socket-events/init.js | 17 ++-- client/js/socket.js | 9 +-- client/js/store.js | 4 + client/js/vue.js | 7 -- src/server.js | 12 ++- test/server.js | 2 +- 11 files changed, 115 insertions(+), 120 deletions(-) diff --git a/client/components/Windows/SignIn.vue b/client/components/Windows/SignIn.vue index e69dd572..92736aae 100644 --- a/client/components/Windows/SignIn.vue +++ b/client/components/Windows/SignIn.vue @@ -85,7 +85,7 @@ export default { storage.set("user", values.user); - socket.emit("auth", values); + socket.emit("auth:perform", values); }, getStoredUser() { return storage.get("user"); diff --git a/client/index.html.tpl b/client/index.html.tpl index bbd144fa..d3b9477a 100644 --- a/client/index.html.tpl +++ b/client/index.html.tpl @@ -53,13 +53,13 @@
The Lounge -

Your JavaScript must be enabled.

+

The Lounge requires a modern browser with JavaScript enabled.

This is taking longer than it should, there might be connectivity issues.

- +
diff --git a/client/js/loading-error-handlers.js b/client/js/loading-error-handlers.js index d6c118c6..922175fb 100644 --- a/client/js/loading-error-handlers.js +++ b/client/js/loading-error-handlers.js @@ -1,4 +1,4 @@ -/* eslint strict: 0, no-var: 0 */ +/* eslint strict: 0 */ "use strict"; /* @@ -9,71 +9,66 @@ */ (function() { - var msg = document.getElementById("loading-page-message"); + const msg = document.getElementById("loading-page-message"); + msg.textContent = "Loading the app…"; - if (msg) { - msg.textContent = "Loading the app…"; + document + .getElementById("loading-reload") + .addEventListener("click", () => location.reload(true)); - document.getElementById("loading-reload").addEventListener("click", function() { - location.reload(true); - }); - } - - var displayReload = function displayReload() { - var loadingReload = document.getElementById("loading-reload"); + const displayReload = () => { + const loadingReload = document.getElementById("loading-reload"); if (loadingReload) { loadingReload.style.visibility = "visible"; } }; - var loadingSlowTimeout = setTimeout(function() { - var loadingSlow = document.getElementById("loading-slow"); - - // The parent element, #loading, is being removed when the app is loaded. - // Since the timer is not cancelled, `loadingSlow` can be not found after - // 5s. Wrap everything in this block to make sure nothing happens if the - // element does not exist (i.e. page has loaded). - if (loadingSlow) { - loadingSlow.style.visibility = "visible"; - displayReload(); - } + const loadingSlowTimeout = setTimeout(() => { + const loadingSlow = document.getElementById("loading-slow"); + loadingSlow.style.visibility = "visible"; + displayReload(); }, 5000); - window.g_LoungeErrorHandler = function LoungeErrorHandler(e) { - var message = document.getElementById("loading-page-message"); - message.textContent = - "An error has occurred that prevented the client from loading correctly."; + const errorHandler = (e) => { + msg.textContent = "An error has occurred that prevented the client from loading correctly."; - var summary = document.createElement("summary"); + const summary = document.createElement("summary"); summary.textContent = "More details"; - var data = document.createElement("pre"); + const data = document.createElement("pre"); data.textContent = e.message; // e is an ErrorEvent - var info = document.createElement("p"); + const info = document.createElement("p"); info.textContent = "Open the developer tools of your browser for more information."; - var details = document.createElement("details"); + const details = document.createElement("details"); details.appendChild(summary); details.appendChild(data); details.appendChild(info); - message.parentNode.insertBefore(details, message.nextSibling); + msg.parentNode.insertBefore(details, msg.nextSibling); window.clearTimeout(loadingSlowTimeout); displayReload(); }; - window.addEventListener("error", window.g_LoungeErrorHandler); + window.addEventListener("error", errorHandler); + + window.g_TheLoungeRemoveLoading = () => { + delete window.g_TheLoungeRemoveLoading; + window.clearTimeout(loadingSlowTimeout); + window.removeEventListener("error", errorHandler); + document.getElementById("loading").remove(); + }; // Trigger early service worker registration if ("serviceWorker" in navigator) { navigator.serviceWorker.register("service-worker.js"); // Handler for messages coming from the service worker - var messageHandler = function ServiceWorkerMessageHandler(event) { + const messageHandler = (event) => { if (event.data.type === "fetch-error") { - window.g_LoungeErrorHandler({ + errorHandler({ message: `Service worker failed to fetch an url: ${event.data.message}`, }); diff --git a/client/js/router.js b/client/js/router.js index 202cab6c..64fe147d 100644 --- a/client/js/router.js +++ b/client/js/router.js @@ -25,7 +25,7 @@ const router = new VueRouter({ }); router.afterEach((to) => { - if (router.app.initialized) { + if (store.state.appLoaded) { router.app.closeSidebarIfNeeded(); } diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js index a2836494..296a8f97 100644 --- a/client/js/socket-events/auth.js +++ b/client/js/socket-events/auth.js @@ -5,76 +5,89 @@ const socket = require("../socket"); const storage = require("../localStorage"); const {getActiveWindowComponent} = require("../vue"); const store = require("../store").default; -let lastServerHash = -1; +let lastServerHash = null; -socket.on("auth", function(data) { +socket.on("auth:success", function() { + store.commit("currentUserVisibleError", "Loading messages…"); + $("#loading-page-message").text(store.state.currentUserVisibleError); +}); + +socket.on("auth:failed", function() { + storage.remove("token"); + + if (store.state.appLoaded) { + return reloadPage("Authentication failed, reloading…"); + } + + // TODO: This will most likely fail getActiveWindowComponent + showSignIn(); + + // TODO: getActiveWindowComponent is the SignIn component, find a better way to set this + getActiveWindowComponent().errorShown = true; + getActiveWindowComponent().inFlight = false; +}); + +socket.on("auth:start", function(serverHash) { // If we reconnected and serverHash differs, that means the server restarted // And we will reload the page to grab the latest version - if (lastServerHash > -1 && data.serverHash > -1 && data.serverHash !== lastServerHash) { - socket.disconnect(); - store.commit("isConnected", false); - store.commit("currentUserVisibleError", "Server restarted, reloading…"); - location.reload(true); - return; + if (lastServerHash && serverHash !== lastServerHash) { + return reloadPage("Server restarted, reloading…"); } - if (data.serverHash > -1) { - lastServerHash = data.serverHash; - } else { - getActiveWindowComponent().inFlight = false; - } + lastServerHash = serverHash; - let token; const user = storage.get("user"); + const token = storage.get("token"); + const doFastAuth = user && token; - if (!data.success) { - if (store.state.activeWindow !== "SignIn") { - socket.disconnect(); - store.commit("isConnected", false); - store.commit("currentUserVisibleError", "Authentication failed, reloading…"); - location.reload(); - return; - } + // If we reconnect and no longer have a stored token, reload the page + if (store.state.appLoaded && !doFastAuth) { + return reloadPage("Authentication failed, reloading…"); + } - storage.remove("token"); + // If we have user and token stored, perform auth without showing sign-in first + if (doFastAuth) { + store.commit("currentUserVisibleError", "Authorizing…"); + $("#loading-page-message").text(store.state.currentUserVisibleError); - getActiveWindowComponent().errorShown = true; - } else if (user) { - token = storage.get("token"); + let lastMessage = -1; - if (token) { - store.commit("currentUserVisibleError", "Authorizing…"); - $("#loading-page-message").text(store.state.currentUserVisibleError); + for (const network of store.state.networks) { + for (const chan of network.channels) { + if (chan.messages.length > 0) { + const id = chan.messages[chan.messages.length - 1].id; - let lastMessage = -1; - - for (const network of store.state.networks) { - for (const chan of network.channels) { - if (chan.messages.length > 0) { - const id = chan.messages[chan.messages.length - 1].id; - - if (lastMessage < id) { - lastMessage = id; - } + if (lastMessage < id) { + lastMessage = id; } } } - - const openChannel = - (store.state.activeChannel && store.state.activeChannel.channel.id) || null; - - socket.emit("auth", {user, token, lastMessage, openChannel}); } + + const openChannel = + (store.state.activeChannel && store.state.activeChannel.channel.id) || null; + + socket.emit("auth:perform", {user, token, lastMessage, openChannel}); + } else { + showSignIn(); + } +}); + +function showSignIn() { + // TODO: this flashes grey background because it takes a little time for vue to mount signin + if (window.g_TheLoungeRemoveLoading) { + window.g_TheLoungeRemoveLoading(); } - if (token) { - return; - } - - $("#loading").remove(); $("#footer") .find(".sign-in") .trigger("click", { pushState: false, }); -}); +} + +function reloadPage(message) { + socket.disconnect(); + store.commit("currentUserVisibleError", message); + location.reload(true); +} diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js index 2fc1e4f4..d979db05 100644 --- a/client/js/socket-events/init.js +++ b/client/js/socket-events/init.js @@ -10,17 +10,14 @@ const router = require("../router"); const store = require("../store").default; socket.on("init", function(data) { - store.commit("currentUserVisibleError", "Rendering…"); - - $("#loading-page-message").text(store.state.currentUserVisibleError); - store.commit("networks", mergeNetworkData(data.networks)); store.commit("isConnected", true); store.commit("currentUserVisibleError", null); - if (!vueApp.initialized) { + if (!store.state.appLoaded) { router.initialize(); - vueApp.onSocketInit(); + + store.commit("appLoaded"); if (data.token) { storage.set("token", data.token); @@ -43,12 +40,10 @@ socket.on("init", function(data) { vueApp.setUserlist(isUserlistOpen === "true"); - $(document.body).removeClass("signed-out"); - $("#loading").remove(); + document.body.classList.remove("signed-out"); - if (window.g_LoungeErrorHandler) { - window.removeEventListener("error", window.g_LoungeErrorHandler); - window.g_LoungeErrorHandler = null; + if (window.g_TheLoungeRemoveLoading) { + window.g_TheLoungeRemoveLoading(); } if (!vueApp.$route.name || vueApp.$route.name === "SignIn") { diff --git a/client/js/socket.js b/client/js/socket.js index 8f47c78b..1295ef80 100644 --- a/client/js/socket.js +++ b/client/js/socket.js @@ -38,21 +38,18 @@ socket.on("connect", function() { $("#loading-page-message").text(store.state.currentUserVisibleError); }); -socket.on("authorized", function() { - store.commit("currentUserVisibleError", "Loading messages…"); - $("#loading-page-message").text(store.state.currentUserVisibleError); -}); - function handleDisconnect(data) { const message = data.message || data; store.commit("isConnected", false); + store.commit("currentUserVisibleError", `Waiting to reconnect… (${message})`); $("#loading-page-message").text(store.state.currentUserVisibleError); // If the server shuts down, socket.io skips reconnection // and we have to manually call connect to start the process - if (socket.io.skipReconnect) { + // However, do not reconnect if TL client manually closed the connection + if (socket.io.skipReconnect && message !== "io client disconnect") { requestIdleCallback(() => socket.connect(), 2000); } } diff --git a/client/js/store.js b/client/js/store.js index a9f22645..b1161b2e 100644 --- a/client/js/store.js +++ b/client/js/store.js @@ -11,6 +11,7 @@ const store = new Vuex.Store({ settings, }, state: { + appLoaded: false, activeChannel: null, currentUserVisibleError: null, desktopNotificationState: "unsupported", @@ -31,6 +32,9 @@ const store = new Vuex.Store({ versionDataExpired: false, }, mutations: { + appLoaded(state) { + state.appLoaded = true; + }, activeChannel(state, channel) { state.activeChannel = channel; }, diff --git a/client/js/vue.js b/client/js/vue.js index 2f456a9e..9484e86a 100644 --- a/client/js/vue.js +++ b/client/js/vue.js @@ -15,9 +15,6 @@ const appName = document.title; const vueApp = new Vue({ el: "#viewport", - data: { - initialized: false, - }, router, mounted() { if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) { @@ -43,10 +40,6 @@ const vueApp = new Vue({ }, 1); }, methods: { - onSocketInit() { - this.initialized = true; - this.$store.commit("isConnected", true); - }, setSidebar(state) { this.$store.commit("sidebarOpen", state); diff --git a/src/server.js b/src/server.js index f698f456..a49f7741 100644 --- a/src/server.js +++ b/src/server.js @@ -174,11 +174,8 @@ module.exports = function(options = {}) { if (Helper.config.public) { performAuthentication.call(socket, {}); } else { - socket.emit("auth", { - serverHash: serverHash, - success: true, - }); - socket.on("auth", performAuthentication); + socket.on("auth:perform", performAuthentication); + socket.emit("auth:start", serverHash); } }); @@ -337,7 +334,8 @@ function indexRequest(req, res) { } function initializeClient(socket, client, token, lastMessage, openChannel) { - socket.emit("authorized"); + socket.off("auth:perform", performAuthentication); + socket.emit("auth:success"); client.clientAttach(socket.id, token); @@ -789,7 +787,7 @@ function performAuthentication(data) { ); } - socket.emit("auth", {success: false}); + socket.emit("auth:failed"); return; } diff --git a/test/server.js b/test/server.js index 48798c05..095d21e4 100644 --- a/test/server.js +++ b/test/server.js @@ -72,7 +72,7 @@ describe("Server", function() { }); it("should emit authorized message", (done) => { - client.on("authorized", done); + client.on("auth:success", done); }); it("should create network", (done) => {