feat: Prevent saving preferences and status when impersonating (#11164)
This PR will prevent saving user preferences and online status when impersonating. Previously, these settings could be updated during impersonation, causing the user to see a different view or UI settings. Fixes https://linear.app/chatwoot/issue/CW-4163/impersonation-improvements
This commit is contained in:
@@ -4,6 +4,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store';
|
|||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownContainer,
|
DropdownContainer,
|
||||||
@@ -20,6 +21,8 @@ const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
|
|||||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||||
const currentUserAutoOffline = useMapGetter('getCurrentUserAutoOffline');
|
const currentUserAutoOffline = useMapGetter('getCurrentUserAutoOffline');
|
||||||
|
|
||||||
|
const { isImpersonating } = useImpersonation();
|
||||||
|
|
||||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||||
const statusList = computed(() => {
|
const statusList = computed(() => {
|
||||||
return [
|
return [
|
||||||
@@ -46,6 +49,10 @@ const activeStatus = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function changeAvailabilityStatus(availability) {
|
function changeAvailabilityStatus(availability) {
|
||||||
|
if (isImpersonating.value) {
|
||||||
|
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
store.dispatch('updateAvailability', {
|
store.dispatch('updateAvailability', {
|
||||||
availability,
|
availability,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||||
@@ -20,6 +21,10 @@ export default {
|
|||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
NextButton,
|
NextButton,
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { isImpersonating } = useImpersonation();
|
||||||
|
return { isImpersonating };
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isStatusMenuOpened: false,
|
isStatusMenuOpened: false,
|
||||||
@@ -73,6 +78,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
changeAvailabilityStatus(availability) {
|
changeAvailabilityStatus(availability) {
|
||||||
|
if (this.isImpersonating) {
|
||||||
|
useAlert(
|
||||||
|
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isUpdating) {
|
if (this.isUpdating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { useImpersonation } from '../useImpersonation';
|
||||||
|
|
||||||
|
vi.mock('shared/helpers/sessionStorage', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
get: vi.fn(),
|
||||||
|
set: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
|
|
||||||
|
describe('useImpersonation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if impersonation flag is set in session storage', () => {
|
||||||
|
SessionStorage.get.mockReturnValue(true);
|
||||||
|
const { isImpersonating } = useImpersonation();
|
||||||
|
expect(isImpersonating.value).toBe(true);
|
||||||
|
expect(SessionStorage.get).toHaveBeenCalledWith(
|
||||||
|
SESSION_STORAGE_KEYS.IMPERSONATION_USER
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if impersonation flag is not set in session storage', () => {
|
||||||
|
SessionStorage.get.mockReturnValue(false);
|
||||||
|
const { isImpersonating } = useImpersonation();
|
||||||
|
expect(isImpersonating.value).toBe(false);
|
||||||
|
expect(SessionStorage.get).toHaveBeenCalledWith(
|
||||||
|
SESSION_STORAGE_KEYS.IMPERSONATION_USER
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
10
app/javascript/dashboard/composables/useImpersonation.js
Normal file
10
app/javascript/dashboard/composables/useImpersonation.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
|
|
||||||
|
export function useImpersonation() {
|
||||||
|
const isImpersonating = computed(() => {
|
||||||
|
return SessionStorage.get(SESSION_STORAGE_KEYS.IMPERSONATION_USER);
|
||||||
|
});
|
||||||
|
return { isImpersonating };
|
||||||
|
}
|
||||||
3
app/javascript/dashboard/constants/sessionStorage.js
Normal file
3
app/javascript/dashboard/constants/sessionStorage.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const SESSION_STORAGE_KEYS = {
|
||||||
|
IMPERSONATION_USER: 'impersonationUser',
|
||||||
|
};
|
||||||
@@ -185,7 +185,8 @@
|
|||||||
"OFFLINE": "Offline"
|
"OFFLINE": "Offline"
|
||||||
},
|
},
|
||||||
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||||
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again",
|
||||||
|
"IMPERSONATING_ERROR": "Cannot change availability while impersonating a user"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "Your email address",
|
"LABEL": "Your email address",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import types from '../mutation-types';
|
|||||||
import authAPI from '../../api/auth';
|
import authAPI from '../../api/auth';
|
||||||
|
|
||||||
import { setUser, clearCookiesOnLogout } from '../utils/api';
|
import { setUser, clearCookiesOnLogout } from '../utils/api';
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
currentUser: {
|
currentUser: {
|
||||||
@@ -145,8 +147,15 @@ export const actions = {
|
|||||||
updateUISettings: async ({ commit }, params) => {
|
updateUISettings: async ({ commit }, params) => {
|
||||||
try {
|
try {
|
||||||
commit(types.SET_CURRENT_USER_UI_SETTINGS, params);
|
commit(types.SET_CURRENT_USER_UI_SETTINGS, params);
|
||||||
|
|
||||||
|
const isImpersonating = SessionStorage.get(
|
||||||
|
SESSION_STORAGE_KEYS.IMPERSONATION_USER
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isImpersonating) {
|
||||||
const response = await authAPI.updateUISettings(params);
|
const response = await authAPI.updateUISettings(params);
|
||||||
commit(types.SET_CURRENT_USER, response.data);
|
commit(types.SET_CURRENT_USER, response.data);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
|||||||
import differenceInDays from 'date-fns/differenceInDays';
|
import differenceInDays from 'date-fns/differenceInDays';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
import {
|
import {
|
||||||
ANALYTICS_IDENTITY,
|
ANALYTICS_IDENTITY,
|
||||||
@@ -44,6 +46,10 @@ export const clearLocalStorageOnLogout = () => {
|
|||||||
LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES);
|
LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const clearSessionStorageOnLogout = () => {
|
||||||
|
SessionStorage.remove(SESSION_STORAGE_KEYS.IMPERSONATION_USER);
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteIndexedDBOnLogout = async () => {
|
export const deleteIndexedDBOnLogout = async () => {
|
||||||
let dbs = [];
|
let dbs = [];
|
||||||
try {
|
try {
|
||||||
@@ -75,6 +81,7 @@ export const clearCookiesOnLogout = () => {
|
|||||||
emitter.emit(ANALYTICS_RESET);
|
emitter.emit(ANALYTICS_RESET);
|
||||||
clearBrowserSessionCookies();
|
clearBrowserSessionCookies();
|
||||||
clearLocalStorageOnLogout();
|
clearLocalStorageOnLogout();
|
||||||
|
clearSessionStorageOnLogout();
|
||||||
const globalConfig = window.globalConfig || {};
|
const globalConfig = window.globalConfig || {};
|
||||||
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
|
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
|
||||||
window.location = logoutRedirectLink;
|
window.location = logoutRedirectLink;
|
||||||
|
|||||||
26
app/javascript/shared/helpers/sessionStorage.js
Normal file
26
app/javascript/shared/helpers/sessionStorage.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export default {
|
||||||
|
clearAll() {
|
||||||
|
window.sessionStorage.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
try {
|
||||||
|
const value = window.sessionStorage.getItem(key);
|
||||||
|
return value ? JSON.parse(value) : null;
|
||||||
|
} catch (error) {
|
||||||
|
return window.sessionStorage.getItem(key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set(key, value) {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
window.sessionStorage.setItem(key, JSON.stringify(value));
|
||||||
|
} else {
|
||||||
|
window.sessionStorage.setItem(key, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(key) {
|
||||||
|
window.sessionStorage.removeItem(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
137
app/javascript/shared/helpers/specs/sessionStorage.spec.js
Normal file
137
app/javascript/shared/helpers/specs/sessionStorage.spec.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import SessionStorage from '../sessionStorage';
|
||||||
|
|
||||||
|
// Mocking sessionStorage
|
||||||
|
const sessionStorageMock = (() => {
|
||||||
|
let store = {};
|
||||||
|
return {
|
||||||
|
getItem: key => store[key] || null,
|
||||||
|
setItem: (key, value) => {
|
||||||
|
store[key] = String(value);
|
||||||
|
},
|
||||||
|
removeItem: key => delete store[key],
|
||||||
|
clear: () => {
|
||||||
|
store = {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'sessionStorage', {
|
||||||
|
value: sessionStorageMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SessionStorage utility', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearAll method', () => {
|
||||||
|
it('should clear all items from sessionStorage', () => {
|
||||||
|
sessionStorage.setItem('testKey1', 'testValue1');
|
||||||
|
sessionStorage.setItem('testKey2', 'testValue2');
|
||||||
|
|
||||||
|
SessionStorage.clearAll();
|
||||||
|
|
||||||
|
expect(sessionStorage.getItem('testKey1')).toBeNull();
|
||||||
|
expect(sessionStorage.getItem('testKey2')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get method', () => {
|
||||||
|
it('should retrieve and parse JSON values correctly', () => {
|
||||||
|
const testObject = { a: 1, b: 'test' };
|
||||||
|
sessionStorage.setItem('testKey', JSON.stringify(testObject));
|
||||||
|
|
||||||
|
expect(SessionStorage.get('testKey')).toEqual(testObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for non-existent keys', () => {
|
||||||
|
expect(SessionStorage.get('nonExistentKey')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-JSON values by returning the raw value', () => {
|
||||||
|
sessionStorage.setItem('testKey', 'plain string value');
|
||||||
|
|
||||||
|
expect(SessionStorage.get('testKey')).toBe('plain string value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle malformed JSON gracefully', () => {
|
||||||
|
sessionStorage.setItem('testKey', '{malformed:json}');
|
||||||
|
|
||||||
|
expect(SessionStorage.get('testKey')).toBe('{malformed:json}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('set method', () => {
|
||||||
|
it('should store object values as JSON strings', () => {
|
||||||
|
const testObject = { a: 1, b: 'test' };
|
||||||
|
SessionStorage.set('testKey', testObject);
|
||||||
|
|
||||||
|
expect(sessionStorage.getItem('testKey')).toBe(
|
||||||
|
JSON.stringify(testObject)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store primitive values directly', () => {
|
||||||
|
SessionStorage.set('stringKey', 'test string');
|
||||||
|
expect(sessionStorage.getItem('stringKey')).toBe('test string');
|
||||||
|
|
||||||
|
SessionStorage.set('numberKey', 42);
|
||||||
|
expect(sessionStorage.getItem('numberKey')).toBe('42');
|
||||||
|
|
||||||
|
SessionStorage.set('booleanKey', true);
|
||||||
|
expect(sessionStorage.getItem('booleanKey')).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null values', () => {
|
||||||
|
SessionStorage.set('nullKey', null);
|
||||||
|
|
||||||
|
expect(sessionStorage.getItem('nullKey')).toBe('null');
|
||||||
|
expect(SessionStorage.get('nullKey')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined values', () => {
|
||||||
|
SessionStorage.set('undefinedKey', undefined);
|
||||||
|
|
||||||
|
expect(sessionStorage.getItem('undefinedKey')).toBe('undefined');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove method', () => {
|
||||||
|
it('should remove an item from sessionStorage', () => {
|
||||||
|
SessionStorage.set('testKey', 'testValue');
|
||||||
|
expect(SessionStorage.get('testKey')).toBe('testValue');
|
||||||
|
|
||||||
|
SessionStorage.remove('testKey');
|
||||||
|
|
||||||
|
expect(SessionStorage.get('testKey')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when removing a non-existent key', () => {
|
||||||
|
expect(() => {
|
||||||
|
SessionStorage.remove('nonExistentKey');
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration of methods', () => {
|
||||||
|
it('should set, get, and remove values correctly', () => {
|
||||||
|
SessionStorage.set('testKey', { value: 'test' });
|
||||||
|
|
||||||
|
expect(SessionStorage.get('testKey')).toEqual({ value: 'test' });
|
||||||
|
|
||||||
|
SessionStorage.remove('testKey');
|
||||||
|
expect(SessionStorage.get('testKey')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle impersonation flag (common use case)', () => {
|
||||||
|
SessionStorage.set('impersonationUser', true);
|
||||||
|
|
||||||
|
expect(SessionStorage.get('impersonationUser')).toBe(true);
|
||||||
|
|
||||||
|
expect(sessionStorage.getItem('impersonationUser')).toBe('true');
|
||||||
|
|
||||||
|
SessionStorage.remove('impersonationUser');
|
||||||
|
expect(SessionStorage.get('impersonationUser')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,8 @@ import { parseBoolean } from '@chatwoot/utils';
|
|||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { required, email } from '@vuelidate/validators';
|
import { required, email } from '@vuelidate/validators';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
// mixins
|
// mixins
|
||||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ const ERROR_MESSAGES = {
|
|||||||
'business-account-only': 'LOGIN.OAUTH.BUSINESS_ACCOUNTS_ONLY',
|
'business-account-only': 'LOGIN.OAUTH.BUSINESS_ACCOUNTS_ONLY',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IMPERSONATION_URL_SEARCH_KEY = 'impersonation';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FormInput,
|
FormInput,
|
||||||
@@ -112,6 +115,14 @@ export default {
|
|||||||
this.loginApi.message = message;
|
this.loginApi.message = message;
|
||||||
useAlert(this.loginApi.message);
|
useAlert(this.loginApi.message);
|
||||||
},
|
},
|
||||||
|
handleImpersonation() {
|
||||||
|
// Detects impersonation mode via URL and sets a session flag to prevent user settings changes during impersonation.
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const impersonation = urlParams.get(IMPERSONATION_URL_SEARCH_KEY);
|
||||||
|
if (impersonation) {
|
||||||
|
SessionStorage.set(SESSION_STORAGE_KEYS.IMPERSONATION_USER, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
submitLogin() {
|
submitLogin() {
|
||||||
this.loginApi.hasErrored = false;
|
this.loginApi.hasErrored = false;
|
||||||
this.loginApi.showLoading = true;
|
this.loginApi.showLoading = true;
|
||||||
@@ -128,6 +139,7 @@ export default {
|
|||||||
|
|
||||||
login(credentials)
|
login(credentials)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
this.handleImpersonation();
|
||||||
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
|
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
|
||||||
})
|
})
|
||||||
.catch(response => {
|
.catch(response => {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ module SsoAuthenticatable
|
|||||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{generate_sso_auth_token}"
|
"#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{generate_sso_auth_token}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def generate_sso_link_with_impersonation
|
||||||
|
"#{generate_sso_link}&impersonation=true"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sso_token_key(token)
|
def sso_token_key(token)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<p class="text-color-red">Caution: Any actions executed after impersonate will appear as if performed by the impersonated user - [<%= page.resource.name %> ]</p>
|
<p class="text-color-red">Caution: Any actions executed after impersonate will appear as if performed by the impersonated user - [<%= page.resource.name %> ]</p>
|
||||||
<a class='button' target='_blank' href='<%= page.resource.generate_sso_link %>'>Impersonate user </a>
|
<a class='button' target='_blank' href='<%= page.resource.generate_sso_link_with_impersonation %>'>Impersonate user </a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user