fix: Allow users to login even if they have access to more than 15 accounts (#4475)

This commit is contained in:
Pranav Raj S 2022-04-14 20:54:26 +05:30 committed by GitHub
parent 80e5d6d7a0
commit 0319b78eac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 368 additions and 350 deletions

View file

@ -1,5 +1,5 @@
<template>
<div id="app" class="app-wrapper app-root">
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<transition name="fade" mode="out-in">
<router-view></router-view>
@ -11,21 +11,28 @@
<woot-snackbar-box />
<network-notification />
</div>
<loading-state v-else />
</template>
<script>
import { accountIdFromPathname } from './helper/URLHelper';
import { mapGetters } from 'vuex';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification';
import UpdateBanner from './components/app/UpdateBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import {
registerSubscription,
verifyServiceWorkerExistence,
} from './helper/pushHelper';
export default {
name: 'App',
components: {
AddAccountModal,
LoadingState,
NetworkNotification,
UpdateBanner,
WootSnackbarBox,
@ -43,13 +50,12 @@ export default {
getAccount: 'accounts/getAccount',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
authUIFlags: 'getAuthUIFlags',
currentAccountId: 'getCurrentAccountId',
}),
hasAccounts() {
return (
this.currentUser &&
this.currentUser.accounts &&
this.currentUser.accounts.length !== 0
);
const { accounts = [] } = this.currentUser || {};
return accounts.length > 0;
},
},
@ -58,32 +64,37 @@ export default {
if (!this.hasAccounts) {
this.showAddAccountModal = true;
}
verifyServiceWorkerExistence(registration =>
registration.pushManager.getSubscription().then(subscription => {
if (subscription) {
registerSubscription();
}
})
);
},
currentAccountId() {
if (this.currentAccountId) {
this.initializeAccount();
}
},
},
mounted() {
this.$store.dispatch('setUser');
this.setLocale(window.chatwootConfig.selectedLocale);
this.initializeAccount();
},
methods: {
setLocale(locale) {
this.$root.$i18n.locale = locale;
},
async initializeAccount() {
const { pathname } = window.location;
const accountId = accountIdFromPathname(pathname);
if (accountId) {
await this.$store.dispatch('accounts/get');
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(accountId);
this.setLocale(locale);
this.latestChatwootVersion = latestChatwootVersion;
}
await this.$store.dispatch('accounts/get');
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale);
this.latestChatwootVersion = latestChatwootVersion;
vueActionCable.init(pubsubToken);
},
},
};

View file

@ -1,6 +1,4 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
import Cookies from 'js-cookie';
import endPoints from './endPoints';
@ -61,41 +59,15 @@ export default {
});
return fetchPromise;
},
isLoggedIn() {
const hasAuthCookie = !!Cookies.getJSON('auth_data');
const hasUserCookie = !!Cookies.getJSON('user');
return hasAuthCookie && hasUserCookie;
hasAuthCookie() {
return !!Cookies.getJSON('cw_d_session_info');
},
isAdmin() {
if (this.isLoggedIn()) {
return Cookies.getJSON('user').role === 'administrator';
}
return false;
},
getAuthData() {
if (this.isLoggedIn()) {
return Cookies.getJSON('auth_data');
if (this.hasAuthCookie()) {
return Cookies.getJSON('cw_d_session_info');
}
return false;
},
getPubSubToken() {
if (this.isLoggedIn()) {
const user = Cookies.getJSON('user') || {};
const { pubsub_token: pubsubToken } = user;
return pubsubToken;
}
return null;
},
getCurrentUser() {
if (this.isLoggedIn()) {
return Cookies.getJSON('user');
}
return null;
},
verifyPasswordToken({ confirmationToken }) {
return new Promise((resolve, reject) => {
axios

View file

@ -53,7 +53,7 @@ export default {
computed: {
...mapGetters({
getCurrentUserAvailability: 'getCurrentUserAvailability',
getCurrentAccountId: 'getCurrentAccountId',
currentAccountId: 'getCurrentAccountId',
}),
availabilityDisplayLabel() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
@ -63,9 +63,6 @@ export default {
availabilityIndex
];
},
currentAccountId() {
return this.getCurrentAccountId;
},
currentUserAvailability() {
return this.getCurrentUserAvailability;
},

View file

@ -1,4 +1,3 @@
/* eslint no-console: 0 */
import Auth from '../api/auth';
const parseErrorCode = error => Promise.reject(error);
@ -7,7 +6,7 @@ export default axios => {
const { apiHost = '' } = window.chatwootConfig || {};
const wootApi = axios.create({ baseURL: `${apiHost}/` });
// Add Auth Headers to requests if logged in
if (Auth.isLoggedIn()) {
if (Auth.hasAuthCookie()) {
const {
'access-token': accessToken,
'token-type': tokenType,

View file

@ -14,6 +14,9 @@ export const getLoginRedirectURL = (ssoAccountId, user) => {
if (ssoAccount) {
return frontendURL(`accounts/${ssoAccountId}/dashboard`);
}
if (accounts.length) {
return frontendURL(`accounts/${accounts[0].id}/dashboard`);
}
return DEFAULT_REDIRECT_URL;
};
@ -41,15 +44,6 @@ export const conversationUrl = ({
return url;
};
export const accountIdFromPathname = pathname => {
const isInsideAccountScopedURLs = pathname.includes('/app/accounts');
const urlParam = pathname.split('/')[3];
// eslint-disable-next-line no-restricted-globals
const isScoped = isInsideAccountScopedURLs && !isNaN(urlParam);
const accountId = isScoped ? Number(urlParam) : '';
return accountId;
};
export const isValidURL = value => {
/* eslint-disable no-useless-escape */
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;

View file

@ -142,14 +142,7 @@ class ActionCableConnector extends BaseActionCableConnector {
}
export default {
init() {
if (AuthAPI.isLoggedIn()) {
const actionCable = new ActionCableConnector(
window.WOOT,
AuthAPI.getPubSubToken()
);
return actionCable;
}
return null;
init(pubsubToken) {
return new ActionCableConnector(window.WOOT, pubsubToken);
},
};

View file

@ -44,7 +44,7 @@ export const getPushSubscriptionPayload = subscription => ({
});
export const sendRegistrationToServer = subscription => {
if (auth.isLoggedIn()) {
if (auth.hasAuthCookie()) {
return NotificationSubscriptions.create(
getPushSubscriptionPayload(subscription)
);

View file

@ -1,7 +1,6 @@
import {
frontendURL,
conversationUrl,
accountIdFromPathname,
isValidURL,
getLoginRedirectURL,
} from '../URLHelper';
@ -39,18 +38,6 @@ describe('#URL Helpers', () => {
});
});
describe('accountIdFromPathname', () => {
it('should return account id if accont scoped url is passed', () => {
expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1);
});
it('should return empty string if accont scoped url not is passed', () => {
expect(accountIdFromPathname('/app/accounts/settings/general')).toBe('');
});
it('should return empty string if empty string is passed', () => {
expect(accountIdFromPathname('')).toBe('');
});
});
describe('isValidURL', () => {
it('should return true if valid url is passed', () => {
expect(isValidURL('https://chatwoot.com')).toBe(true);
@ -75,7 +62,7 @@ describe('#URL Helpers', () => {
getLoginRedirectURL('7500', {
accounts: [{ id: '7501', name: 'Test Account 7501' }],
})
).toBe('/app/');
).toBe('/app/accounts/7501/dashboard');
expect(getLoginRedirectURL('7500', null)).toBe('/app/');
});
});

View file

@ -92,7 +92,6 @@ export default {
},
},
mounted() {
this.$store.dispatch('setCurrentAccountId', this.$route.params.accountId);
window.addEventListener('resize', this.handleResize);
this.handleResize();
bus.$on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);

View file

@ -1,21 +1,13 @@
import VueRouter from 'vue-router';
import auth from '../api/auth';
import login from './login/login.routes';
import dashboard from './dashboard/dashboard.routes';
import authRoute from './auth/auth.routes';
import { frontendURL } from '../helper/URLHelper';
import { clearBrowserSessionCookies } from '../store/utils/api';
import authRoute from './auth/auth.routes';
import dashboard from './dashboard/dashboard.routes';
import login from './login/login.routes';
import store from '../store';
const routes = [
...login.routes,
...dashboard.routes,
...authRoute.routes,
{
path: '/',
redirect: '/app',
},
];
const routes = [...login.routes, ...dashboard.routes, ...authRoute.routes];
window.roleWiseRoutes = {
agent: [],
@ -63,8 +55,8 @@ const routeValidators = [
{
protected: false,
loggedIn: true,
handler: () => {
const user = auth.getCurrentUser();
handler: (_, getters) => {
const user = getters.getCurrentUser;
return `accounts/${user.account_id}/dashboard`;
},
},
@ -76,8 +68,8 @@ const routeValidators = [
{
protected: true,
loggedIn: true,
handler: to => {
const user = auth.getCurrentUser();
handler: (to, getters) => {
const user = getters.getCurrentUser;
const userRole = getUserRole(user, Number(to.params.accountId));
const isAccessible = routeIsAccessibleFor(to.name, userRole);
return isAccessible ? null : `accounts/${to.params.accountId}/dashboard`;
@ -90,15 +82,20 @@ const routeValidators = [
},
];
export const validateAuthenticateRoutePermission = (to, from, next) => {
const isLoggedIn = auth.isLoggedIn();
export const validateAuthenticateRoutePermission = (
to,
from,
next,
{ getters }
) => {
const isLoggedIn = getters.isLoggedIn;
const isProtectedRoute = !unProtectedRoutes.includes(to.name);
const strategy = routeValidators.find(
validator =>
validator.protected === isProtectedRoute &&
validator.loggedIn === isLoggedIn
);
const nextRoute = strategy.handler(to);
const nextRoute = strategy.handler(to, getters);
return nextRoute ? next(frontendURL(nextRoute)) : next();
};
@ -109,38 +106,47 @@ const validateSSOLoginParams = to => {
return isLoginRoute && hasValidSSOParams;
};
const validateRouteAccess = (to, from, next) => {
export const validateRouteAccess = (to, from, next, { getters }) => {
// Disable navigation to signup page if signups are disabled
// Signup route has an attribute (requireSignupEnabled)
// defined in it's route definition
if (
window.chatwootConfig.signupEnabled !== 'true' &&
to.meta &&
to.meta.requireSignupEnabled
) {
const user = auth.getCurrentUser();
next(frontendURL(`accounts/${user.account_id}/dashboard`));
}
if (validateSSOLoginParams(to)) {
clearBrowserSessionCookies();
return next();
return next(frontendURL('login'));
}
// For routes which doesn't care about authentication, skip validation
if (authIgnoreRoutes.includes(to.name)) {
return next();
}
return validateAuthenticateRoutePermission(to, from, next);
return validateAuthenticateRoutePermission(to, from, next, { getters });
};
// protecting routes
router.beforeEach((to, from, next) => {
if (!to.name) {
const user = auth.getCurrentUser();
if (user) {
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
export const initalizeRouter = () => {
const userAuthentication = store.dispatch('setUser');
router.beforeEach((to, from, next) => {
if (validateSSOLoginParams(to)) {
clearBrowserSessionCookies();
next();
return;
}
return next('/app/login');
}
return validateRouteAccess(to, from, next);
});
userAuthentication.then(() => {
if (!to.name) {
const { isLoggedIn, getCurrentUser: user } = store.getters;
if (isLoggedIn) {
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
}
return next('/app/login');
}
return validateRouteAccess(to, from, next, store);
});
});
};
export default router;

View file

@ -1,6 +1,8 @@
import 'expect-more-jest';
import { validateAuthenticateRoutePermission } from './index';
import auth from '../api/auth';
import {
validateAuthenticateRoutePermission,
validateRouteAccess,
} from './index';
jest.mock('./dashboard/dashboard.routes', () => ({
routes: [],
@ -11,94 +13,119 @@ jest.mock('./auth/auth.routes', () => ({
jest.mock('./login/login.routes', () => ({
routes: [],
}));
jest.mock('../constants', () => {
return {
GRAVATAR_URL: 'https://www.gravatar.com/avatar',
CHANNELS: {
FACEBOOK: 'facebook',
},
ASSIGNEE_TYPE: {
ME: 'me',
UNASSIGNED: 'unassigned',
ALL: 'all',
},
STATUS_TYPE: {
OPEN: 'open',
RESOLVED: 'resolved',
},
};
});
window.roleWiseRoutes = {};
describe(`behavior`, () => {
describe('#validateAuthenticateRoutePermission', () => {
describe(`when route is not protected`, () => {
it(`should go to the dashboard when user is logged in`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(true);
spyOn(auth, 'getCurrentUser').and.returnValue({
account_id: 1,
accounts: [{ id: 1, role: 'agent' }],
});
const to = { name: 'login' };
const to = { name: 'login', params: { accountId: 1 } };
const from = { name: '', params: { accountId: 1 } };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
const getters = {
isLoggedIn: true,
getCurrentUser: {
account_id: 1,
id: 1,
accounts: [{ id: 1, role: 'admin' }],
},
};
validateAuthenticateRoutePermission(to, from, next, { getters });
expect(next).toHaveBeenCalledWith('/app/accounts/1/dashboard');
});
it(`should go there when user is not logged in`, () => {
const to = { name: 'login', params: {} };
const from = { name: '', params: {} };
const next = jest.fn();
const getters = { isLoggedIn: false };
validateAuthenticateRoutePermission(to, from, next, { getters });
expect(next).toHaveBeenCalledWith();
});
});
describe(`when route is protected`, () => {
describe(`when user not logged in`, () => {
it(`should redirect to login`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(false);
spyOn(auth, 'getCurrentUser').and.returnValue(null);
const to = { name: 'some-protected-route', params: { accountId: 1 } };
const from = { name: '' };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
const getters = {
isLoggedIn: false,
getCurrentUser: {
account_id: null,
id: null,
accounts: [],
},
};
validateAuthenticateRoutePermission(to, from, next, { getters });
expect(next).toHaveBeenCalledWith('/app/login');
});
});
describe(`when user is logged in`, () => {
describe(`when route is not accessible to current user`, () => {
it(`should redirect to dashboard`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(true);
spyOn(auth, 'getCurrentUser').and.returnValue({
accounts: [{ id: 1, role: 'agent' }],
});
window.roleWiseRoutes.agent = ['dashboard'];
const to = { name: 'admin', params: { accountId: 1 } };
const from = { name: '' };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
const getters = {
isLoggedIn: true,
getCurrentUser: {
account_id: 1,
id: 1,
accounts: [{ id: 1, role: 'agent' }],
},
};
validateAuthenticateRoutePermission(to, from, next, { getters });
expect(next).toHaveBeenCalledWith('/app/accounts/1/dashboard');
});
});
describe(`when route is accessible to current user`, () => {
it(`should go there`, () => {
// Arrange
spyOn(auth, 'isLoggedIn').and.returnValue(true);
spyOn(auth, 'getCurrentUser').and.returnValue({
accounts: [{ id: 1, role: 'agent' }],
});
window.roleWiseRoutes.agent = ['dashboard', 'admin'];
const to = { name: 'admin', params: { accountId: 1 } };
const from = { name: '' };
const next = jest.fn();
// Act
validateAuthenticateRoutePermission(to, from, next);
// Assert
const getters = {
isLoggedIn: true,
getCurrentUser: {
account_id: 1,
id: 1,
accounts: [{ id: 1, role: 'agent' }],
},
};
validateAuthenticateRoutePermission(to, from, next, { getters });
expect(next).toHaveBeenCalledWith();
});
});
});
});
});
describe('#validateRouteAccess', () => {
it('returns to login if signup is disabled', () => {
window.chatwootConfig = { signupEnabled: 'false' };
const to = { name: 'auth_signup', meta: { requireSignupEnabled: true } };
const from = { name: '' };
const next = jest.fn();
validateRouteAccess(to, from, next, {});
expect(next).toHaveBeenCalledWith('/app/login');
});
it('returns next for an auth ignore route ', () => {
const to = { name: 'auth_confirmation' };
const from = { name: '' };
const next = jest.fn();
validateRouteAccess(to, from, next, {});
expect(next).toHaveBeenCalledWith();
});
it('returns route validation for everything else ', () => {
const to = { name: 'login' };
const from = { name: '' };
const next = jest.fn();
validateRouteAccess(to, from, next, { getters: { isLoggedIn: false } });
expect(next).toHaveBeenCalledWith();
});
});

View file

@ -1,28 +1,20 @@
/* eslint no-param-reassign: 0 */
import axios from 'axios';
import Vue from 'vue';
import * as types from '../mutation-types';
import types from '../mutation-types';
import authAPI from '../../api/auth';
import createAxios from '../../helper/APIHelper';
import actionCable from '../../helper/actionCable';
import { setUser, getHeaderExpiry, clearCookiesOnLogout } from '../utils/api';
import { setUser, clearCookiesOnLogout } from '../utils/api';
import { getLoginRedirectURL } from '../../helper/URLHelper';
const state = {
const initialState = {
currentUser: {
id: null,
account_id: null,
channel: null,
accounts: [],
email: null,
name: null,
provider: null,
uid: null,
subscription: {
state: null,
expiry: null,
},
},
currentAccountId: null,
uiFlags: {
isFetching: true,
},
};
// getters
@ -31,18 +23,22 @@ export const getters = {
return !!$state.currentUser.id;
},
getCurrentUserID(_state) {
return _state.currentUser.id;
getCurrentUserID($state) {
return $state.currentUser.id;
},
getUISettings(_state) {
return _state.currentUser.ui_settings || {};
getUISettings($state) {
return $state.currentUser.ui_settings || {};
},
getCurrentUserAvailability(_state) {
const { accounts = [] } = _state.currentUser;
getAuthUIFlags($state) {
return $state.uiFlags;
},
getCurrentUserAvailability($state, $getters) {
const { accounts = [] } = $state.currentUser;
const [currentAccount = {}] = accounts.filter(
account => account.id === _state.currentAccountId
account => account.id === $getters.getCurrentAccountId
);
return currentAccount.availability;
},
@ -54,49 +50,45 @@ export const getters = {
return null;
},
getCurrentRole(_state) {
const { accounts = [] } = _state.currentUser;
getCurrentRole($state, $getters) {
const { accounts = [] } = $state.currentUser;
const [currentAccount = {}] = accounts.filter(
account => account.id === _state.currentAccountId
account => account.id === $getters.getCurrentAccountId
);
return currentAccount.role;
},
getCurrentUser(_state) {
return _state.currentUser;
getCurrentUser($state) {
return $state.currentUser;
},
getMessageSignature(_state) {
const { message_signature: messageSignature } = _state.currentUser;
getMessageSignature($state) {
const { message_signature: messageSignature } = $state.currentUser;
return messageSignature || '';
},
getCurrentAccount(_state) {
const { accounts = [] } = _state.currentUser;
getCurrentAccount($state, $getters) {
const { accounts = [] } = $state.currentUser;
const [currentAccount = {}] = accounts.filter(
account => account.id === _state.currentAccountId
account => account.id === $getters.getCurrentAccountId
);
return currentAccount || {};
},
getUserAccounts(_state) {
const { accounts = [] } = _state.currentUser;
getUserAccounts($state) {
const { accounts = [] } = $state.currentUser;
return accounts;
},
};
// actions
export const actions = {
login({ commit }, { ssoAccountId, ...credentials }) {
login(_, { ssoAccountId, ...credentials }) {
return new Promise((resolve, reject) => {
authAPI
.login(credentials)
.then(response => {
commit(types.default.SET_CURRENT_USER);
window.axios = createAxios(axios);
actionCable.init(Vue);
window.location = getLoginRedirectURL(ssoAccountId, response.data);
resolve();
})
@ -108,43 +100,40 @@ export const actions = {
async validityCheck(context) {
try {
const response = await authAPI.validityCheck();
setUser(response.data.payload.data, getHeaderExpiry(response), {
setUserInSDK: true,
});
context.commit(types.default.SET_CURRENT_USER);
const currentUser = response.data.payload.data;
setUser(currentUser);
context.commit(types.SET_CURRENT_USER, currentUser);
} catch (error) {
if (error?.response?.status === 401) {
clearCookiesOnLogout();
}
}
},
setUser({ commit, dispatch }) {
if (authAPI.isLoggedIn()) {
commit(types.default.SET_CURRENT_USER);
dispatch('validityCheck');
async setUser({ commit, dispatch }) {
if (authAPI.hasAuthCookie()) {
await dispatch('validityCheck');
} else {
commit(types.default.CLEAR_USER);
commit(types.CLEAR_USER);
}
commit(types.SET_CURRENT_USER_UI_FLAGS, { isFetching: false });
},
logout({ commit }) {
commit(types.default.CLEAR_USER);
commit(types.CLEAR_USER);
},
updateProfile: async ({ commit }, params) => {
// eslint-disable-next-line no-useless-catch
try {
const response = await authAPI.profileUpdate(params);
setUser(response.data, getHeaderExpiry(response));
commit(types.default.SET_CURRENT_USER);
commit(types.SET_CURRENT_USER, response.data);
} catch (error) {
throw error;
}
},
deleteAvatar: async ({ commit }) => {
deleteAvatar: async () => {
try {
await authAPI.deleteAvatar();
commit(types.default.SET_CURRENT_USER);
} catch (error) {
// Ignore error
}
@ -152,10 +141,9 @@ export const actions = {
updateUISettings: async ({ commit }, params) => {
try {
commit(types.default.SET_CURRENT_USER_UI_SETTINGS, params);
commit(types.SET_CURRENT_USER_UI_SETTINGS, params);
const response = await authAPI.updateUISettings(params);
setUser(response.data, getHeaderExpiry(response));
commit(types.default.SET_CURRENT_USER);
commit(types.SET_CURRENT_USER, response.data);
} catch (error) {
// Ignore error
}
@ -166,45 +154,32 @@ export const actions = {
const response = await authAPI.updateAvailability(params);
const userData = response.data;
const { id } = userData;
setUser(userData, getHeaderExpiry(response));
commit(types.default.SET_CURRENT_USER);
commit(types.SET_CURRENT_USER, response.data);
dispatch('agents/updatePresence', { [id]: params.availability });
} catch (error) {
// Ignore error
}
},
setCurrentAccountId({ commit }, accountId) {
commit(types.default.SET_CURRENT_ACCOUNT_ID, accountId);
},
setCurrentUserAvailability({ commit, state: $state }, data) {
if (data[$state.currentUser.id]) {
commit(
types.default.SET_CURRENT_USER_AVAILABILITY,
data[$state.currentUser.id]
);
commit(types.SET_CURRENT_USER_AVAILABILITY, data[$state.currentUser.id]);
}
},
};
// mutations
export const mutations = {
[types.default.SET_CURRENT_USER_AVAILABILITY](_state, availability) {
[types.SET_CURRENT_USER_AVAILABILITY](_state, availability) {
Vue.set(_state.currentUser, 'availability', availability);
},
[types.default.CLEAR_USER](_state) {
_state.currentUser.id = null;
[types.CLEAR_USER](_state) {
_state.currentUser = initialState.currentUser;
},
[types.default.SET_CURRENT_USER](_state) {
const currentUser = {
...authAPI.getAuthData(),
...authAPI.getCurrentUser(),
};
[types.SET_CURRENT_USER](_state, currentUser) {
Vue.set(_state, 'currentUser', currentUser);
},
[types.default.SET_CURRENT_USER_UI_SETTINGS](_state, { uiSettings }) {
[types.SET_CURRENT_USER_UI_SETTINGS](_state, { uiSettings }) {
Vue.set(_state, 'currentUser', {
..._state.currentUser,
ui_settings: {
@ -213,13 +188,14 @@ export const mutations = {
},
});
},
[types.default.SET_CURRENT_ACCOUNT_ID](_state, accountId) {
Vue.set(_state, 'currentAccountId', Number(accountId));
[types.SET_CURRENT_USER_UI_FLAGS](_state, { isFetching }) {
Vue.set(_state, 'uiFlags', { isFetching });
},
};
export default {
state,
state: initialState,
getters,
actions,
mutations,

View file

@ -1,4 +1,3 @@
import authAPI from '../../../api/auth';
import { MESSAGE_TYPE } from 'shared/constants/messages';
import { applyPageFilters } from './helpers';
@ -40,8 +39,8 @@ const getters = {
return lastEmail;
},
getMineChats: _state => activeFilters => {
const currentUserID = authAPI.getCurrentUser().id;
getMineChats: (_state, _, __, rootGetters) => activeFilters => {
const currentUserID = rootGetters.getCurrentUser?.id;
return _state.allConversations.filter(conversation => {
const { assignee } = conversation.meta;

View file

@ -29,7 +29,9 @@ describe('#actions', () => {
});
await actions.validityCheck({ commit });
expect(setUser).toHaveBeenCalledTimes(1);
expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]);
expect(commit.mock.calls).toEqual([
[types.default.SET_CURRENT_USER, { id: 1, name: 'John' }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({
@ -47,8 +49,9 @@ describe('#actions', () => {
headers: { expiry: 581842904 },
});
await actions.updateProfile({ commit }, { name: 'Pranav' });
expect(setUser).toHaveBeenCalledTimes(1);
expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]);
expect(commit.mock.calls).toEqual([
[types.default.SET_CURRENT_USER, { id: 1, name: 'John' }],
]);
});
});
@ -57,7 +60,8 @@ describe('#actions', () => {
axios.post.mockResolvedValue({
data: {
id: 1,
account_users: [{ account_id: 1, availability_status: 'offline' }],
name: 'John',
accounts: [{ account_id: 1, availability_status: 'offline' }],
},
headers: { expiry: 581842904 },
});
@ -65,8 +69,16 @@ describe('#actions', () => {
{ commit, dispatch },
{ availability: 'offline', account_id: 1 }
);
expect(setUser).toHaveBeenCalledTimes(1);
expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]);
expect(commit.mock.calls).toEqual([
[
types.default.SET_CURRENT_USER,
{
id: 1,
name: 'John',
accounts: [{ account_id: 1, availability_status: 'offline' }],
},
],
]);
expect(dispatch.mock.calls).toEqual([
['agents/updatePresence', { 1: 'offline' }],
]);
@ -88,13 +100,20 @@ describe('#actions', () => {
{ commit, dispatch },
{ uiSettings: { is_contact_sidebar_open: false } }
);
expect(setUser).toHaveBeenCalledTimes(1);
expect(commit.mock.calls).toEqual([
[
types.default.SET_CURRENT_USER_UI_SETTINGS,
{ uiSettings: { is_contact_sidebar_open: false } },
],
[types.default.SET_CURRENT_USER],
[
types.default.SET_CURRENT_USER,
{
id: 1,
name: 'John',
availability_status: 'offline',
ui_settings: { is_contact_sidebar_open: true },
},
],
]);
});
});
@ -103,14 +122,17 @@ describe('#actions', () => {
it('sends correct actions if user is logged in', async () => {
Cookies.getJSON.mockImplementation(() => true);
actions.setUser({ commit, dispatch });
expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]);
expect(commit.mock.calls).toEqual([]);
expect(dispatch.mock.calls).toEqual([['validityCheck']]);
});
it('sends correct actions if user is not logged in', async () => {
Cookies.getJSON.mockImplementation(() => false);
actions.setUser({ commit, dispatch });
expect(commit.mock.calls).toEqual([[types.default.CLEAR_USER]]);
expect(commit.mock.calls).toEqual([
[types.default.CLEAR_USER],
[types.default.SET_CURRENT_USER_UI_FLAGS, { isFetching: false }],
]);
expect(dispatch).toHaveBeenCalledTimes(0);
});
});

View file

@ -4,39 +4,74 @@ import '../../../../routes';
jest.mock('../../../../routes', () => {});
describe('#getters', () => {
it('isLoggedIn', () => {
expect(getters.isLoggedIn({ currentUser: { id: null } })).toEqual(false);
expect(getters.isLoggedIn({ currentUser: { id: 1 } })).toEqual(true);
describe('#isLoggedIn', () => {
it('return correct value if user data is available', () => {
expect(getters.isLoggedIn({ currentUser: { id: null } })).toEqual(false);
expect(getters.isLoggedIn({ currentUser: { id: 1 } })).toEqual(true);
});
});
it('getCurrentUserID', () => {
expect(getters.getCurrentUserID({ currentUser: { id: 1 } })).toEqual(1);
});
it('getCurrentUser', () => {
expect(
getters.getCurrentUser({ currentUser: { id: 1, name: 'Pranav' } })
).toEqual({ id: 1, name: 'Pranav' });
describe('#getCurrentUser', () => {
it('returns current user id', () => {
expect(getters.getCurrentUserID({ currentUser: { id: 1 } })).toEqual(1);
});
});
it('get', () => {
expect(
getters.getCurrentUserAvailability({
currentAccountId: 1,
currentUser: {
id: 1,
accounts: [{ id: 1, availability: 'busy' }],
},
})
).toEqual('busy');
describe('#getCurrentUser', () => {
it('returns current user object', () => {
expect(
getters.getCurrentUser({ currentUser: { id: 1, name: 'Pranav' } })
).toEqual({ id: 1, name: 'Pranav' });
});
});
it('getUISettings', () => {
expect(
getters.getUISettings({
currentUser: { ui_settings: { is_contact_sidebar_open: true } },
})
).toEqual({ is_contact_sidebar_open: true });
describe('#getCurrentRole', () => {
it('returns current role if account is available', () => {
expect(
getters.getCurrentRole(
{ currentUser: { accounts: [{ id: 1, role: 'admin' }] } },
{ getCurrentAccountId: 1 }
)
).toEqual('admin');
});
it('returns undefined if account is not available', () => {
expect(
getters.getCurrentRole(
{ currentUser: { accounts: [{ id: 1, role: 'admin' }] } },
{ getCurrentAccountId: 2 }
)
).toEqual(undefined);
});
});
describe('#getCurrentUserAvailability', () => {
it('returns correct availability status', () => {
expect(
getters.getCurrentUserAvailability(
{
currentAccountId: 1,
currentUser: {
id: 1,
accounts: [{ id: 1, availability: 'busy' }],
},
},
{ getCurrentAccountId: 1 }
)
).toEqual('busy');
});
});
describe('#getUISettings', () => {
it('return correct UI Settings', () => {
expect(
getters.getUISettings({
currentUser: { ui_settings: { is_contact_sidebar_open: true } },
})
).toEqual({ is_contact_sidebar_open: true });
});
});
describe('#getMessageSignature', () => {
it('Return signature when signature is present', () => {
expect(
@ -46,11 +81,7 @@ describe('#getters', () => {
).toEqual('Thanks');
});
it('Return empty string when signature is not present', () => {
expect(
getters.getMessageSignature({
currentUser: {},
})
).toEqual('');
expect(getters.getMessageSignature({ currentUser: {} })).toEqual('');
});
});
@ -59,22 +90,23 @@ describe('#getters', () => {
expect(
getters.getCurrentAccount({
currentUser: {},
currentAccountId: 1,
})
).toEqual({});
expect(
getters.getCurrentAccount({
currentUser: {
accounts: [
{
name: 'Chatwoot',
id: 1,
},
],
getters.getCurrentAccount(
{
currentUser: {
accounts: [
{
name: 'Chatwoot',
id: 1,
},
],
},
currentAccountId: 1,
},
currentAccountId: 1,
})
{ getCurrentAccountId: 1 }
)
).toEqual({
name: 'Chatwoot',
id: 1,
@ -89,7 +121,6 @@ describe('#getters', () => {
currentUser: {},
})
).toEqual([]);
expect(
getters.getUserAccounts({
currentUser: {

View file

@ -18,4 +18,28 @@ describe('#mutations', () => {
});
});
});
describe('#SET_CURRENT_USER_UI_FLAGS', () => {
it('set auth ui flags', () => {
const state = {
uiFlags: { isFetching: false },
};
mutations[types.SET_CURRENT_USER_UI_FLAGS](state, { isFetching: true });
expect(state.uiFlags.isFetching).toEqual(true);
});
});
describe('#CLEAR_USER', () => {
it('set auth ui flags', () => {
const state = {
currentUser: { id: 1 },
};
mutations[types.CLEAR_USER](state);
expect(state.currentUser).toEqual({
id: null,
account_id: null,
accounts: [],
email: null,
name: null,
});
});
});
});

View file

@ -2,9 +2,9 @@ export default {
AUTHENTICATE: 'AUTHENTICATE',
CLEAR_USER: 'LOGOUT',
SET_CURRENT_USER: 'SET_CURRENT_USER',
SET_CURRENT_ACCOUNT_ID: 'SET_CURRENT_ACCOUNT_ID',
SET_CURRENT_USER_AVAILABILITY: 'SET_CURRENT_USER_AVAILABILITY',
SET_CURRENT_USER_UI_SETTINGS: 'SET_CURRENT_USER_UI_SETTINGS',
SET_CURRENT_USER_UI_FLAGS: 'SET_CURRENT_USER_UI_FLAGS',
// Chat List
RECEIVE_CHAT_LIST: 'RECEIVE_CHAT_LIST',

View file

@ -1,8 +1,6 @@
/* eslint no-param-reassign: 0 */
import fromUnixTime from 'date-fns/fromUnixTime';
import differenceInDays from 'date-fns/differenceInDays';
import Cookies from 'js-cookie';
import { frontendURL } from '../../helper/URLHelper';
import {
ANALYTICS_IDENTITY,
ANALYTICS_RESET,
@ -17,14 +15,9 @@ export const setLoadingStatus = (state, status) => {
state.fetchAPIloadingStatus = status;
};
export const setUser = (user, expiryDate, options = {}) => {
if (options && options.setUserInSDK) {
window.bus.$emit(CHATWOOT_SET_USER, { user });
window.bus.$emit(ANALYTICS_IDENTITY, { user });
}
Cookies.set('user', user, {
expires: differenceInDays(expiryDate, new Date()),
});
export const setUser = user => {
window.bus.$emit(CHATWOOT_SET_USER, { user });
window.bus.$emit(ANALYTICS_IDENTITY, { user });
};
export const getHeaderExpiry = response =>
@ -32,13 +25,14 @@ export const getHeaderExpiry = response =>
export const setAuthCredentials = response => {
const expiryDate = getHeaderExpiry(response);
Cookies.set('auth_data', response.headers, {
Cookies.set('cw_d_session_info', response.headers, {
expires: differenceInDays(expiryDate, new Date()),
});
setUser(response.data.data, expiryDate);
};
export const clearBrowserSessionCookies = () => {
Cookies.remove('cw_d_session_info');
Cookies.remove('auth_data');
Cookies.remove('user');
};
@ -48,7 +42,6 @@ export const clearCookiesOnLogout = () => {
window.bus.$emit(ANALYTICS_RESET);
clearBrowserSessionCookies();
const globalConfig = window.globalConfig || {};
const logoutRedirectLink =
globalConfig.LOGOUT_REDIRECT_LINK || frontendURL('login');
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
window.location = logoutRedirectLink;
};

View file

@ -26,14 +26,9 @@ import {
initOnEvents,
} from '../shared/helpers/AudioNotificationHelper';
import { initFaviconSwitcher } from '../shared/helpers/faviconHelper';
import router from '../dashboard/routes';
import router, { initalizeRouter } from '../dashboard/routes';
import store from '../dashboard/store';
import vueActionCable from '../dashboard/helper/actionCable';
import constants from '../dashboard/constants';
import {
verifyServiceWorkerExistence,
registerSubscription,
} from '../dashboard/helper/pushHelper';
import * as Sentry from '@sentry/vue';
import 'vue-easytable/libs/theme-default/index.css';
import { Integrations } from '@sentry/tracing';
@ -93,6 +88,7 @@ window.axios = createAxios(axios);
window.bus = new Vue();
initializeChatwootEvents();
initializeAnalyticsEvents();
initalizeRouter();
window.onload = () => {
window.WOOT = new Vue({
@ -102,7 +98,6 @@ window.onload = () => {
components: { App },
template: '<App/>',
}).$mount('#app');
vueActionCable.init();
};
const setupAudioListeners = () => {
@ -113,13 +108,6 @@ const setupAudioListeners = () => {
});
};
window.addEventListener('load', () => {
verifyServiceWorkerExistence(registration =>
registration.pushManager.getSubscription().then(subscription => {
if (subscription) {
registerSubscription();
}
})
);
window.playAudioAlert = () => {};
initOnEvents.forEach(e => {
document.addEventListener(e, setupAudioListeners, false);