From 84433f29ab0a2565e6556ee6cfbbe7e30d5057c1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 7 Nov 2018 12:34:12 -0500 Subject: [PATCH] Do not require a full refresh on login/logout (#6841) * no-refresh login as well, plus lots of fixes for missing config on login * replace config with new set on logout as well * passing new payload data into new action:app.loggedIn hook, and old action:app.loggedOut hook * fixed issues with socket.io not properly representing uid on server * some light refactoring and cleanup * minor cleanup, fixed spa logout not working after login * have reconnection handler for socket.io wait 2s to confirm disconnection before reporting -- stops flicker if reconnecting immediately * Dynamically replace chat and slideout menu on updateHeader() ... instead of just the menu items. * more efficient calls to Benchpress and translator /cc @pitaj * fix: chats and notification handlers not working after login * fix: accidentally calling cb multiple times --- public/src/app.js | 62 ++++++++++++++++++++++++++----- public/src/client/login.js | 12 +++--- public/src/sockets.js | 9 ++++- src/controllers/authentication.js | 38 ++++++++++++++++--- src/middleware/header.js | 14 +++++-- 5 files changed, 110 insertions(+), 25 deletions(-) diff --git a/public/src/app.js b/public/src/app.js index f9f5f53ba5..038ce5a280 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -56,9 +56,7 @@ app.cacheBuster = null; app.newTopic(); }); - require(['components'], function (components) { - components.get('user/logout').on('click', app.logout); - }); + $('#header-menu .container').on('click', '[component="user/logout"]', app.logout); Visibility.change(function (event, state) { if (state === 'visible') { @@ -106,6 +104,45 @@ app.cacheBuster = null; }); }; + app.updateHeader = function (data, callback) { + /** + * data: + * header (obj) + * config (obj) + * next (string) + */ + require(['benchpress', 'translator', 'notifications', 'chat'], function (Benchpress, translator, Notifications, Chat) { + app.user = data.header.user; + data.header.config = data.config; + config = data.config; + Benchpress.setGlobal('config', config); + + // Manually reconnect socket.io + socket.close(); + socket.open(); + + // Re-render top bar menu + var toRender = { + menu: $('#header-menu .container'), + 'chats-menu': $('#chats-menu'), + 'slideout-menu': $('.slideout-menu'), + }; + Promise.all(Object.keys(toRender).map(function (tpl) { + return Benchpress.render('partials/' + tpl, data.header).then(function (render) { + return translator.Translator.create().translate(render); + }); + })).then(function (html) { + Object.values(toRender).forEach(function (element, idx) { + element.html(html[idx]); + }); + + Notifications.prepareDOM(); + Chat.prepareDOM(); + callback(); + }); + }); + }; + app.logout = function (e) { if (e) { e.preventDefault(); @@ -124,13 +161,18 @@ app.cacheBuster = null; headers: { 'x-csrf-token': config.csrf_token, }, - success: function () { - var payload = { - next: config.relative_path + '/', - }; - - $(window).trigger('action:app.loggedOut', payload); - window.location.href = payload.next; + success: function (data) { + app.updateHeader(data, function () { + // Overwrite in hook (below) to redirect elsewhere + data.next = data.next || undefined; + + $(window).trigger('action:app.loggedOut', data); + if (data.next) { + ajaxify.go(data.next); + } else { + ajaxify.refresh(); + } + }); }, }); }; diff --git a/public/src/client/login.js b/public/src/client/login.js index 1a8f96861e..70c96088de 100644 --- a/public/src/client/login.js +++ b/public/src/client/login.js @@ -35,14 +35,14 @@ define('forum/login', [], function () { headers: { 'x-csrf-token': config.csrf_token, }, - success: function (returnTo) { - var pathname = utils.urlToLocation(returnTo).pathname; - - var params = utils.params({ url: returnTo }); + success: function (data) { + var params = utils.params({ url: data.next }); params.loggedin = true; - var qs = decodeURIComponent($.param(params)); - window.location.href = pathname + '?' + qs; + app.updateHeader(data, function () { + ajaxify.go(data.next); + $(window).trigger('action:app.loggedIn', data); + }); }, error: function (data) { if (data.status === 403 && data.responseText === 'Forbidden') { diff --git a/public/src/sockets.js b/public/src/sockets.js index 0a0c295217..63ef91f8cf 100644 --- a/public/src/sockets.js +++ b/public/src/sockets.js @@ -24,7 +24,14 @@ app.isConnected = false; function addHandlers() { socket.on('connect', onConnect); - socket.on('reconnecting', onReconnecting); + socket.on('reconnecting', function () { + // Wait 2s before firing + setTimeout(function () { + if (socket.disconnected) { + onReconnecting(); + } + }, 2000); + }); socket.on('disconnect', onDisconnect); diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 2ce06ca683..0e2e699a3f 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -14,10 +14,12 @@ var plugins = require('../plugins'); var utils = require('../utils'); var translator = require('../translator'); var helpers = require('./helpers'); +var middleware = require('../middleware'); var privileges = require('../privileges'); var sockets = require('../socket.io'); var authenticationController = module.exports; +var apiController = require('./api'); authenticationController.register = function (req, res) { var registrationType = meta.config.registrationType || 'normal'; @@ -277,10 +279,16 @@ function continueLogin(req, res, next) { return helpers.noScriptErrors(req, res, err.message, 403); } - res.status(200).send(nconf.get('relative_path') + '/reset/' + code); + res.status(200).send({ + next: nconf.get('relative_path') + '/reset/' + code, + }); }); } else { - authenticationController.doLogin(req, userData.uid, function (err) { + async.parallel({ + doLogin: async.apply(authenticationController.doLogin, req, userData.uid), + header: async.apply(middleware.generateHeader, req, res, {}), + config: async.apply(apiController.loadConfig, req), + }, function (err, payload) { if (err) { return helpers.noScriptErrors(req, res, err.message, 403); } @@ -296,7 +304,11 @@ function continueLogin(req, res, next) { if (req.body.noscript === 'true') { res.redirect(destination + '?loggedin'); } else { - res.status(200).send(destination); + res.status(200).send({ + next: destination, + header: payload.header, + config: payload.config, + }); } }); } @@ -320,6 +332,9 @@ authenticationController.doLogin = function (req, uid, callback) { authenticationController.onSuccessfulLogin = function (req, uid, callback) { var uuid = utils.generateUUID(); + req.uid = uid; + req.loggedIn = true; + async.waterfall([ function (next) { meta.blacklist.test(req.ip, next); @@ -451,7 +466,8 @@ authenticationController.logout = function (req, res, next) { }, function (next) { req.logout(); - req.session.destroy(function (err) { + req.session.regenerate(function (err) { + req.uid = 0; next(err); }); }, @@ -467,7 +483,19 @@ authenticationController.logout = function (req, res, next) { if (req.body.noscript === 'true') { res.redirect(nconf.get('relative_path') + '/'); } else { - res.status(200).send(''); + async.parallel({ + header: async.apply(middleware.generateHeader, req, res, {}), + config: async.apply(apiController.loadConfig, req), + }, function (err, payload) { + if (err) { + return res.status(500); + } + + res.status(200).send({ + header: payload.header, + config: payload.config, + }); + }); } }, ], next); diff --git a/src/middleware/header.js b/src/middleware/header.js index fb617b1fae..54f5022a31 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -45,7 +45,7 @@ module.exports = function (middleware) { ], next); }; - middleware.renderHeader = function (req, res, data, callback) { + middleware.generateHeader = function (req, res, data, callback) { var registrationType = meta.config.registrationType || 'normal'; res.locals.config = res.locals.config || {}; var templateValues = { @@ -209,8 +209,16 @@ module.exports = function (middleware) { templateValues: templateValues, }, next); }, - function (data, next) { - req.app.render('header', data.templateValues, next); + ], function (err, data) { + callback(err, data.templateValues); + }); + }; + + middleware.renderHeader = function (req, res, data, callback) { + async.waterfall([ + async.apply(middleware.generateHeader, req, res, data), + function (templateValues, next) { + req.app.render('header', templateValues, next); }, ], callback); };