Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
[MM-12958] Support running two Mattermost instances on the same domai…
Browse files Browse the repository at this point in the history
…n using subpaths (#2533)

* [MM-12958] Support running two Mattermost instances on the same domain using subpaths

* leverage getBasePath within LocalStorage

* update comments re: clearing cookies

* path-scope all local storage keys

* fix linting issues
  • Loading branch information
d28park authored and crspeller committed May 3, 2019
1 parent 32d5332 commit 523dfb1
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 14 deletions.
8 changes: 6 additions & 2 deletions actions/views/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@ export function loadTranslations(locale, url) {
}

export function clearUserCookie() {
// We need to clear the cookie both with and without the domain set because we can't tell if the server set
// the cookie with it. At this time, the domain will be set if ServiceSettings.EnableCookiesForSubdomains is true.
// We need to clear the cookie without the domain, with the domain, and with both the domain and path set because we
// can't tell if the server set the cookie with or without the domain.
// The server will have set the domain if ServiceSettings.EnableCookiesForSubdomains is true
// The server will have set a non-default path if Mattermost is also served from a subpath.
document.cookie = 'MMUSERID=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/';
document.cookie = `MMUSERID=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain=${window.location.hostname};path=/`;
document.cookie = `MMUSERID=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain=${window.location.hostname};path=${window.basename}`;
document.cookie = 'MMCSRF=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/';
document.cookie = `MMCSRF=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain=${window.location.hostname};path=/`;
document.cookie = `MMCSRF=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain=${window.location.hostname};path=${window.basename}`;
}
47 changes: 47 additions & 0 deletions cypress/integration/login/subpath_login_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

// ***************************************************************
// - [number] indicates a test step (e.g. 1. Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************

/* eslint max-nested-callbacks: ["error", 4] */

describe('Cookie with Subpath', () => {
before(() => {
// 1. Remove whitelisted cookies
cy.clearCookie('MMAUTHTOKEN');
cy.clearCookie('MMUSERID');
cy.clearCookie('MMCSRF');
});

it('should generate cookie with subpath', () => {
cy.getSubpath().then((subpath) => {
// * Check login page is loaded
cy.get('#login_section').should('be.visible');

// 2. Login as user-1
cy.get('#loginId').should('be.visible').type('user-1');
cy.get('#loginPassword').should('be.visible').type('user-1');
cy.get('#loginButton').should('be.visible').click();

// * Check login success
cy.get('#channel_view').should('be.visible');

// * Check subpath included in url
cy.url().should('include', subpath);
cy.url().should('include', '/channels/town-square');

// * Check cookies have correct path parameter
cy.getCookies().should('have.length', 3).each((cookie) => {
if (subpath) {
expect(cookie).to.have.property('path', subpath);
} else {
expect(cookie).to.have.property('path', '/');
}
});
});
});
});
14 changes: 14 additions & 0 deletions cypress/support/ui_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ Cypress.Commands.add('toIntegrationSettings', () => {
cy.get('#integrations').should('be.visible').click();
});

Cypress.Commands.add('getSubpath', () => {
cy.visit('/');
cy.url().then((url) => {
cy.location().its('origin').then((origin) => {
if (url === origin) {
return '';
}

// Remove trailing slash
return url.replace(origin, '').substring(0, url.length - origin.length - 1);
});
});
});

// ***********************************************************
// Account Settings Modal
// ***********************************************************
Expand Down
2 changes: 1 addition & 1 deletion selectors/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export function getBasePath(state) {
return new URL(config.SiteURL).pathname;
}

return window.basename || window.location.origin;
return window.basename || '/';
}
47 changes: 36 additions & 11 deletions stores/local_storage_store.jsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getRedirectChannelNameForTeam} from 'utils/channel_utils.jsx';
import store from 'stores/redux_store.jsx';
import {getBasePath} from 'selectors/general';

const getPreviousTeamIdKey = (userId) => ['user_prev_team', userId].join(':');
const getPreviousChannelNameKey = (userId, teamId) => ['user_team_prev_channel', userId, teamId].join(':');
const getPenultimateChannelNameKey = (userId, teamId) => ['user_team_penultimate_channel', userId, teamId].join(':');
const getRecentEmojisKey = (userId) => ['recent_emojis', userId].join(':');
const getWasLoggedInKey = () => 'was_logged_in';

const getPathScopedKey = (path, key) => {
if (path === '' || path === '/') {
return key;
}

return [path, key].join(':');
};

// LocalStorageStore exposes an interface for accessing entries in the localStorage.
//
// Note that this excludes keys managed by redux-persist. The latter cannot currently be used for
// key/value storage that persists beyond logout. Ideally, we could purge all but certain parts
// of the Redux store so as to allow them to be used on re-login.
class LocalStorageStoreClass {
getItem(key) {
const state = store.getState();
const basePath = getBasePath(state);

return localStorage.getItem(getPathScopedKey(basePath, key));
}

setItem(key, value) {
const state = store.getState();
const basePath = getBasePath(state);

localStorage.setItem(getPathScopedKey(basePath, key), value);
}

getPreviousChannelName(userId, teamId) {
return localStorage.getItem(getPreviousChannelNameKey(userId, teamId)) || getRedirectChannelNameForTeam(teamId);
return this.getItem(getPreviousChannelNameKey(userId, teamId)) || getRedirectChannelNameForTeam(teamId);
}

setPreviousChannelName(userId, teamId, channelName) {
localStorage.setItem(getPreviousChannelNameKey(userId, teamId), channelName);
this.setItem(getPreviousChannelNameKey(userId, teamId), channelName);
}

getPenultimateChannelName(userId, teamId) {
return localStorage.getItem(getPenultimateChannelNameKey(userId, teamId)) || getRedirectChannelNameForTeam(teamId);
return this.getItem(getPenultimateChannelNameKey(userId, teamId)) || getRedirectChannelNameForTeam(teamId);
}

setPenultimateChannelName(userId, teamId, channelName) {
localStorage.setItem(getPenultimateChannelNameKey(userId, teamId), channelName);
this.setItem(getPenultimateChannelNameKey(userId, teamId), channelName);
}

getPreviousTeamId(userId) {
return localStorage.getItem(getPreviousTeamIdKey(userId));
return this.getItem(getPreviousTeamIdKey(userId));
}

setPreviousTeamId(userId, teamId) {
localStorage.setItem(getPreviousTeamIdKey(userId), teamId);
this.setItem(getPreviousTeamIdKey(userId), teamId);
}

getRecentEmojis(userId) {
const recentEmojis = localStorage.getItem(getRecentEmojisKey(userId));
const recentEmojis = this.getItem(getRecentEmojisKey(userId));
if (!recentEmojis) {
return null;
}
Expand All @@ -48,20 +73,20 @@ class LocalStorageStoreClass {

setRecentEmojis(userId, recentEmojis = []) {
if (recentEmojis.length) {
localStorage.setItem(getRecentEmojisKey(userId), JSON.stringify(recentEmojis));
this.setItem(getRecentEmojisKey(userId), JSON.stringify(recentEmojis));
}
}

setWasLoggedIn(wasLoggedIn) {
if (wasLoggedIn) {
localStorage.setItem('was_logged_in', 'true');
this.setItem(getWasLoggedInKey(), 'true');
} else {
localStorage.setItem('was_logged_in', 'false');
this.setItem(getWasLoggedInKey(), 'false');
}
}

getWasLoggedIn() {
return localStorage.getItem('was_logged_in') === 'true';
return this.getItem(getWasLoggedInKey()) === 'true';
}
}

Expand Down
62 changes: 62 additions & 0 deletions stores/local_storage_store.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,66 @@ describe('stores/LocalStorageStore', () => {
const recentEmojisForUser2 = LocalStorageStore.getRecentEmojis(userId2);
assert.deepEqual(recentEmojisForUser2, recentEmojis2);
});

describe('should persist separately for different subpaths', () => {
test('getWasLoggedIn', () => {
delete window.basename;

// Initially false
assert.equal(LocalStorageStore.getWasLoggedIn(), false);

// True after set
LocalStorageStore.setWasLoggedIn(true);
assert.equal(LocalStorageStore.getWasLoggedIn(), true);

// Still true when basename explicitly set
window.basename = '/';
assert.equal(LocalStorageStore.getWasLoggedIn(), true);

// Different with different basename
window.basename = '/subpath';
assert.equal(LocalStorageStore.getWasLoggedIn(), false);
LocalStorageStore.setWasLoggedIn(true);
assert.equal(LocalStorageStore.getWasLoggedIn(), true);

// Back to old value with original basename
window.basename = '/';
assert.equal(LocalStorageStore.getWasLoggedIn(), true);
LocalStorageStore.setWasLoggedIn(false);
assert.equal(LocalStorageStore.getWasLoggedIn(), false);

// Value with different basename remains unchanged.
window.basename = '/subpath';
assert.equal(LocalStorageStore.getWasLoggedIn(), true);
});

test('recentEmojis', () => {
delete window.basename;

const userId = 'userId';
const recentEmojis1 = ['smile', 'joy', 'grin'];
const recentEmojis2 = ['customEmoji', '+1', 'mattermost'];

// Initially empty
assert.equal(LocalStorageStore.getRecentEmojis(userId), null);

// After set
LocalStorageStore.setRecentEmojis(userId, recentEmojis1);
assert.deepEqual(LocalStorageStore.getRecentEmojis(userId), recentEmojis1);

// Still set when basename explicitly set
window.basename = '/';
assert.deepEqual(LocalStorageStore.getRecentEmojis(userId), recentEmojis1);

// Different with different basename
window.basename = '/subpath';
assert.equal(LocalStorageStore.getRecentEmojis(userId), null);
LocalStorageStore.setRecentEmojis(userId, recentEmojis2);
assert.deepEqual(LocalStorageStore.getRecentEmojis(userId), recentEmojis2);

// Back to old value with original basename
window.basename = '/';
assert.deepEqual(LocalStorageStore.getRecentEmojis(userId), recentEmojis1);
});
});
});

0 comments on commit 523dfb1

Please sign in to comment.