From 425e081dd68e84e0210ddd06ad51ee30d3d7bf6c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 11 Nov 2024 18:14:45 +0100 Subject: [PATCH] fix: Redirect user to login if session is terminated If a session timed out or was closed in another tab, then currently the user gets random error messages. This intercepts 401 responses (should only happen if logged out, or the users does something wrong). If we get a 401, we make sure its because of the session, by checking if the user can access the files app. If that is also the case we forward the user to the login page and set the redirect URL to the last used URL. Signed-off-by: Ferdinand Thiessen --- core/src/utils/xhr-request.js | 59 +++++++++++++++++++++--- cypress/e2e/login/login-redirect.cy.ts | 62 ++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 cypress/e2e/login/login-redirect.cy.ts diff --git a/core/src/utils/xhr-request.js b/core/src/utils/xhr-request.js index ff8b7641b07d8..75f99e3f671b7 100644 --- a/core/src/utils/xhr-request.js +++ b/core/src/utils/xhr-request.js @@ -19,7 +19,8 @@ * along with this program. If not, see . */ -import { getRootUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' +import { generateUrl, getRootUrl } from '@nextcloud/router' /** * @@ -42,6 +43,41 @@ const isNextcloudUrl = (url) => { || (isRelativeUrl(url) && url.startsWith(getRootUrl())) } +/** + * Check if a user was logged in but is now logged-out. + * If this is the case then the user will be forwarded to the login page. + * @returns {Promise} + */ +async function checkLoginStatus() { + // skip if no logged in user + if (getCurrentUser() === null) { + return + } + + // skip if already running + if (checkLoginStatus.running === true) { + return + } + + // only run one request in parallel + checkLoginStatus.running = true + + try { + // We need to check this as a 401 in the first place could also come from other reasons + const { status } = await window.fetch(generateUrl('/apps/files')) + if (status === 401) { + console.warn('User session was terminated, forwarding to login page.') + window.location = generateUrl('/login?redirect_url={url}', { + url: window.location.pathname + window.location.search + window.location.hash, + }) + } + } catch (error) { + console.warn('Could not check login-state') + } finally { + delete checkLoginStatus.running + } +} + /** * Intercept XMLHttpRequest and fetch API calls to add X-Requested-With header * @@ -51,17 +87,24 @@ export const interceptRequests = () => { XMLHttpRequest.prototype.open = (function(open) { return function(method, url, async) { open.apply(this, arguments) - if (isNextcloudUrl(url) && !this.getResponseHeader('X-Requested-With')) { - this.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + if (isNextcloudUrl(url)) { + if (!this.getResponseHeader('X-Requested-With')) { + this.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + } + this.addEventListener('loadend', function() { + if (this.status === 401) { + checkLoginStatus() + } + }) } } })(XMLHttpRequest.prototype.open) window.fetch = (function(fetch) { - return (resource, options) => { + return async (resource, options) => { // fetch allows the `input` to be either a Request object or any stringifyable value if (!isNextcloudUrl(resource.url ?? resource.toString())) { - return fetch(resource, options) + return await fetch(resource, options) } if (!options) { options = {} @@ -76,7 +119,11 @@ export const interceptRequests = () => { options.headers['X-Requested-With'] = 'XMLHttpRequest' } - return fetch(resource, options) + const response = await fetch(resource, options) + if (response.status === 401) { + checkLoginStatus() + } + return response } })(window.fetch) } diff --git a/cypress/e2e/login/login-redirect.cy.ts b/cypress/e2e/login/login-redirect.cy.ts new file mode 100644 index 0000000000000..eb0710dcbccf5 --- /dev/null +++ b/cypress/e2e/login/login-redirect.cy.ts @@ -0,0 +1,62 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Test that when a session expires / the user logged out in another tab, + * the user gets redirected to the login on the next request. + */ +describe('Logout redirect ', { testIsolation: true }, () => { + + let user + + before(() => { + cy.createRandomUser() + .then(($user) => { + user = $user + }) + }) + + it('Redirects to login if session timed out', () => { + // Login and see settings + cy.login(user) + cy.visit('/settings/user#profile') + cy.findByRole('checkbox', { name: /Enable profile/i }) + .should('exist') + + // clear session + cy.clearAllCookies() + + // trigger an request + cy.findByRole('checkbox', { name: /Enable profile/i }) + .click({ force: true }) + + // See that we are redirected + cy.url() + .should('match', /\/login/i) + .and('include', `?redirect_url=${encodeURIComponent('/index.php/settings/user#profile')}`) + + cy.get('form[name="login"]').should('be.visible') + }) + + it('Redirect from login works', () => { + cy.logout() + // visit the login + cy.visit(`/login?redirect_url=${encodeURIComponent('/index.php/settings/user#profile')}`) + + // see login + cy.get('form[name="login"]').should('be.visible') + cy.get('form[name="login"]').within(() => { + cy.get('input[name="user"]').type(user.userId) + cy.get('input[name="password"]').type(user.password) + cy.contains('button[data-login-form-submit]', 'Log in').click() + }) + + // see that we are correctly redirected + cy.url().should('include', '/index.php/settings/user#profile') + cy.findByRole('checkbox', { name: /Enable profile/i }) + .should('exist') + }) + +})