Cleanup auth flow

This commit is contained in:
Pavel Djundik 2019-11-05 21:29:51 +02:00
parent fc1c9568e2
commit a1f183f216
11 changed files with 115 additions and 120 deletions

View file

@ -85,7 +85,7 @@ export default {
storage.set("user", values.user); storage.set("user", values.user);
socket.emit("auth", values); socket.emit("auth:perform", values);
}, },
getStoredUser() { getStoredUser() {
return storage.get("user"); return storage.get("user");

View file

@ -53,13 +53,13 @@
<div id="loading-status-container"> <div id="loading-status-container">
<img src="img/logo-vertical-transparent-bg.svg" class="logo" alt="The Lounge" width="256" height="170"> <img src="img/logo-vertical-transparent-bg.svg" class="logo" alt="The Lounge" width="256" height="170">
<img src="img/logo-vertical-transparent-bg-inverted.svg" class="logo-inverted" alt="The Lounge" width="256" height="170"> <img src="img/logo-vertical-transparent-bg-inverted.svg" class="logo-inverted" alt="The Lounge" width="256" height="170">
<p id="loading-page-message"><a href="https://enable-javascript.com/" target="_blank" rel="noopener">Your JavaScript must be enabled.</a></p> <p id="loading-page-message">The Lounge requires a modern browser with JavaScript enabled.</p>
</div> </div>
<div id="loading-reload-container"> <div id="loading-reload-container">
<p id="loading-slow">This is taking longer than it should, there might be connectivity issues.</p> <p id="loading-slow">This is taking longer than it should, there might be connectivity issues.</p>
<button id="loading-reload" class="btn">Reload page</button> <button id="loading-reload" class="btn">Reload page</button>
</div> </div>
<script async src="js/loading-error-handlers.js?v=<%- cacheBust %>"></script> <script src="js/loading-error-handlers.js?v=<%- cacheBust %>"></script>
</div> </div>
</div> </div>
<div id="viewport"></div> <div id="viewport"></div>

View file

@ -1,4 +1,4 @@
/* eslint strict: 0, no-var: 0 */ /* eslint strict: 0 */
"use strict"; "use strict";
/* /*
@ -9,71 +9,66 @@
*/ */
(function() { (function() {
var msg = document.getElementById("loading-page-message"); const msg = document.getElementById("loading-page-message");
msg.textContent = "Loading the app…";
if (msg) { document
msg.textContent = "Loading the app…"; .getElementById("loading-reload")
.addEventListener("click", () => location.reload(true));
document.getElementById("loading-reload").addEventListener("click", function() { const displayReload = () => {
location.reload(true); const loadingReload = document.getElementById("loading-reload");
});
}
var displayReload = function displayReload() {
var loadingReload = document.getElementById("loading-reload");
if (loadingReload) { if (loadingReload) {
loadingReload.style.visibility = "visible"; loadingReload.style.visibility = "visible";
} }
}; };
var loadingSlowTimeout = setTimeout(function() { const loadingSlowTimeout = setTimeout(() => {
var loadingSlow = document.getElementById("loading-slow"); const loadingSlow = document.getElementById("loading-slow");
loadingSlow.style.visibility = "visible";
// The parent element, #loading, is being removed when the app is loaded. displayReload();
// 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();
}
}, 5000); }, 5000);
window.g_LoungeErrorHandler = function LoungeErrorHandler(e) { const errorHandler = (e) => {
var message = document.getElementById("loading-page-message"); msg.textContent = "An error has occurred that prevented the client from loading correctly.";
message.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"; summary.textContent = "More details";
var data = document.createElement("pre"); const data = document.createElement("pre");
data.textContent = e.message; // e is an ErrorEvent 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."; 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(summary);
details.appendChild(data); details.appendChild(data);
details.appendChild(info); details.appendChild(info);
message.parentNode.insertBefore(details, message.nextSibling); msg.parentNode.insertBefore(details, msg.nextSibling);
window.clearTimeout(loadingSlowTimeout); window.clearTimeout(loadingSlowTimeout);
displayReload(); 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 // Trigger early service worker registration
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js"); navigator.serviceWorker.register("service-worker.js");
// Handler for messages coming from the service worker // Handler for messages coming from the service worker
var messageHandler = function ServiceWorkerMessageHandler(event) { const messageHandler = (event) => {
if (event.data.type === "fetch-error") { if (event.data.type === "fetch-error") {
window.g_LoungeErrorHandler({ errorHandler({
message: `Service worker failed to fetch an url: ${event.data.message}`, message: `Service worker failed to fetch an url: ${event.data.message}`,
}); });

View file

@ -25,7 +25,7 @@ const router = new VueRouter({
}); });
router.afterEach((to) => { router.afterEach((to) => {
if (router.app.initialized) { if (store.state.appLoaded) {
router.app.closeSidebarIfNeeded(); router.app.closeSidebarIfNeeded();
} }

View file

@ -5,76 +5,89 @@ const socket = require("../socket");
const storage = require("../localStorage"); const storage = require("../localStorage");
const {getActiveWindowComponent} = require("../vue"); const {getActiveWindowComponent} = require("../vue");
const store = require("../store").default; 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 // If we reconnected and serverHash differs, that means the server restarted
// And we will reload the page to grab the latest version // And we will reload the page to grab the latest version
if (lastServerHash > -1 && data.serverHash > -1 && data.serverHash !== lastServerHash) { if (lastServerHash && serverHash !== lastServerHash) {
socket.disconnect(); return reloadPage("Server restarted, reloading…");
store.commit("isConnected", false);
store.commit("currentUserVisibleError", "Server restarted, reloading…");
location.reload(true);
return;
} }
if (data.serverHash > -1) { lastServerHash = serverHash;
lastServerHash = data.serverHash;
} else {
getActiveWindowComponent().inFlight = false;
}
let token;
const user = storage.get("user"); const user = storage.get("user");
const token = storage.get("token");
const doFastAuth = user && token;
if (!data.success) { // If we reconnect and no longer have a stored token, reload the page
if (store.state.activeWindow !== "SignIn") { if (store.state.appLoaded && !doFastAuth) {
socket.disconnect(); return reloadPage("Authentication failed, reloading…");
store.commit("isConnected", false); }
store.commit("currentUserVisibleError", "Authentication failed, reloading…");
location.reload();
return;
}
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; let lastMessage = -1;
} else if (user) {
token = storage.get("token");
if (token) { for (const network of store.state.networks) {
store.commit("currentUserVisibleError", "Authorizing…"); for (const chan of network.channels) {
$("#loading-page-message").text(store.state.currentUserVisibleError); if (chan.messages.length > 0) {
const id = chan.messages[chan.messages.length - 1].id;
let lastMessage = -1; if (lastMessage < id) {
lastMessage = id;
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;
}
} }
} }
} }
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") $("#footer")
.find(".sign-in") .find(".sign-in")
.trigger("click", { .trigger("click", {
pushState: false, pushState: false,
}); });
}); }
function reloadPage(message) {
socket.disconnect();
store.commit("currentUserVisibleError", message);
location.reload(true);
}

View file

@ -10,17 +10,14 @@ const router = require("../router");
const store = require("../store").default; const store = require("../store").default;
socket.on("init", function(data) { socket.on("init", function(data) {
store.commit("currentUserVisibleError", "Rendering…");
$("#loading-page-message").text(store.state.currentUserVisibleError);
store.commit("networks", mergeNetworkData(data.networks)); store.commit("networks", mergeNetworkData(data.networks));
store.commit("isConnected", true); store.commit("isConnected", true);
store.commit("currentUserVisibleError", null); store.commit("currentUserVisibleError", null);
if (!vueApp.initialized) { if (!store.state.appLoaded) {
router.initialize(); router.initialize();
vueApp.onSocketInit();
store.commit("appLoaded");
if (data.token) { if (data.token) {
storage.set("token", data.token); storage.set("token", data.token);
@ -43,12 +40,10 @@ socket.on("init", function(data) {
vueApp.setUserlist(isUserlistOpen === "true"); vueApp.setUserlist(isUserlistOpen === "true");
$(document.body).removeClass("signed-out"); document.body.classList.remove("signed-out");
$("#loading").remove();
if (window.g_LoungeErrorHandler) { if (window.g_TheLoungeRemoveLoading) {
window.removeEventListener("error", window.g_LoungeErrorHandler); window.g_TheLoungeRemoveLoading();
window.g_LoungeErrorHandler = null;
} }
if (!vueApp.$route.name || vueApp.$route.name === "SignIn") { if (!vueApp.$route.name || vueApp.$route.name === "SignIn") {

View file

@ -38,21 +38,18 @@ socket.on("connect", function() {
$("#loading-page-message").text(store.state.currentUserVisibleError); $("#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) { function handleDisconnect(data) {
const message = data.message || data; const message = data.message || data;
store.commit("isConnected", false); store.commit("isConnected", false);
store.commit("currentUserVisibleError", `Waiting to reconnect… (${message})`); store.commit("currentUserVisibleError", `Waiting to reconnect… (${message})`);
$("#loading-page-message").text(store.state.currentUserVisibleError); $("#loading-page-message").text(store.state.currentUserVisibleError);
// If the server shuts down, socket.io skips reconnection // If the server shuts down, socket.io skips reconnection
// and we have to manually call connect to start the process // 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); requestIdleCallback(() => socket.connect(), 2000);
} }
} }

View file

@ -11,6 +11,7 @@ const store = new Vuex.Store({
settings, settings,
}, },
state: { state: {
appLoaded: false,
activeChannel: null, activeChannel: null,
currentUserVisibleError: null, currentUserVisibleError: null,
desktopNotificationState: "unsupported", desktopNotificationState: "unsupported",
@ -31,6 +32,9 @@ const store = new Vuex.Store({
versionDataExpired: false, versionDataExpired: false,
}, },
mutations: { mutations: {
appLoaded(state) {
state.appLoaded = true;
},
activeChannel(state, channel) { activeChannel(state, channel) {
state.activeChannel = channel; state.activeChannel = channel;
}, },

View file

@ -15,9 +15,6 @@ const appName = document.title;
const vueApp = new Vue({ const vueApp = new Vue({
el: "#viewport", el: "#viewport",
data: {
initialized: false,
},
router, router,
mounted() { mounted() {
if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) { if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) {
@ -43,10 +40,6 @@ const vueApp = new Vue({
}, 1); }, 1);
}, },
methods: { methods: {
onSocketInit() {
this.initialized = true;
this.$store.commit("isConnected", true);
},
setSidebar(state) { setSidebar(state) {
this.$store.commit("sidebarOpen", state); this.$store.commit("sidebarOpen", state);

View file

@ -174,11 +174,8 @@ module.exports = function(options = {}) {
if (Helper.config.public) { if (Helper.config.public) {
performAuthentication.call(socket, {}); performAuthentication.call(socket, {});
} else { } else {
socket.emit("auth", { socket.on("auth:perform", performAuthentication);
serverHash: serverHash, socket.emit("auth:start", serverHash);
success: true,
});
socket.on("auth", performAuthentication);
} }
}); });
@ -337,7 +334,8 @@ function indexRequest(req, res) {
} }
function initializeClient(socket, client, token, lastMessage, openChannel) { function initializeClient(socket, client, token, lastMessage, openChannel) {
socket.emit("authorized"); socket.off("auth:perform", performAuthentication);
socket.emit("auth:success");
client.clientAttach(socket.id, token); client.clientAttach(socket.id, token);
@ -789,7 +787,7 @@ function performAuthentication(data) {
); );
} }
socket.emit("auth", {success: false}); socket.emit("auth:failed");
return; return;
} }

View file

@ -72,7 +72,7 @@ describe("Server", function() {
}); });
it("should emit authorized message", (done) => { it("should emit authorized message", (done) => {
client.on("authorized", done); client.on("auth:success", done);
}); });
it("should create network", (done) => { it("should create network", (done) => {