refactor(editor): Move user login and logout side effects into hooks (no-changelog) (#16663)

This commit is contained in:
Alex Grozav
2025-06-25 13:00:52 +03:00
committed by GitHub
parent 6fa4a3eaa1
commit de0293595c
5 changed files with 91 additions and 45 deletions

View File

@@ -24,7 +24,11 @@ vi.mock('@/composables/useToast', () => ({
}));
vi.mock('@/stores/users.store', () => ({
useUsersStore: vi.fn().mockReturnValue({ initialize: vi.fn() }),
useUsersStore: vi.fn().mockReturnValue({
initialize: vi.fn(),
registerLoginHook: vi.fn(),
registerLogoutHook: vi.fn(),
}),
}));
vi.mock('@n8n/stores/useRootStore', () => ({
@@ -86,6 +90,16 @@ describe('Init', () => {
expect(settingsStoreSpy).toHaveBeenCalledTimes(1);
});
it('should initialize authentication hooks', async () => {
const registerLoginHookSpy = vi.spyOn(usersStore, 'registerLoginHook');
const registerLogoutHookSpy = vi.spyOn(usersStore, 'registerLogoutHook');
await initializeCore();
expect(registerLoginHookSpy).toHaveBeenCalled();
expect(registerLogoutHookSpy).toHaveBeenCalled();
});
it('should initialize ssoStore with settings SSO configuration', async () => {
const saml = { loginEnabled: true, loginLabel: '' };
const ldap = { loginEnabled: false, loginLabel: '' };

View File

@@ -18,6 +18,10 @@ import { EnterpriseEditionFeature } from '@/constants';
import type { UserManagementAuthenticationMethod } from '@/Interface';
import { useUIStore } from '@/stores/ui.store';
import type { BannerName } from '@n8n/api-types';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { usePostHog } from '@/stores/posthog.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useRBACStore } from '@/stores/rbac.store';
export const state = {
initialized: false,
@@ -39,6 +43,12 @@ export async function initializeCore() {
const ssoStore = useSSOStore();
const uiStore = useUIStore();
registerAuthenticationHooks();
/**
* Initialize stores
*/
await settingsStore.initialize();
ssoStore.initialize({
@@ -146,3 +156,30 @@ export async function initializeAuthenticatedFeatures(
authenticatedFeaturesInitialized = true;
}
function registerAuthenticationHooks() {
const rootStore = useRootStore();
const usersStore = useUsersStore();
const cloudPlanStore = useCloudPlanStore();
const postHogStore = usePostHog();
const uiStore = useUIStore();
const npsSurveyStore = useNpsSurveyStore();
const telemetry = useTelemetry();
const RBACStore = useRBACStore();
usersStore.registerLoginHook((user) => {
RBACStore.setGlobalScopes(user.globalScopes ?? []);
telemetry.identify(rootStore.instanceId, user.id);
postHogStore.init(user.featureFlags);
npsSurveyStore.setupNpsSurveyOnLogin(user.id, user.settings);
});
usersStore.registerLogoutHook(() => {
uiStore.clearBannerStack();
npsSurveyStore.resetNpsSurveyOnLogOut();
postHogStore.reset();
cloudPlanStore.reset();
telemetry.reset();
RBACStore.setGlobalScopes([]);
});
}

View File

@@ -44,7 +44,12 @@ function setCurrentUser() {
function resetStores() {
useSettingsStore().reset();
useUsersStore().reset();
const usersStore = useUsersStore();
usersStore.initialized = false;
usersStore.currentUserId = null;
usersStore.usersById = {};
usersStore.currentUserCloudInfo = null;
}
function setup() {

View File

@@ -2,7 +2,7 @@ import type { CurrentUserResponse } from '@/Interface';
import { useUsersStore } from './users.store';
import { createPinia, setActivePinia } from 'pinia';
const { loginCurrentUser, identify, inviteUsers } = vi.hoisted(() => {
const { loginCurrentUser, inviteUsers } = vi.hoisted(() => {
return {
loginCurrentUser: vi.fn(),
identify: vi.fn(),
@@ -18,12 +18,6 @@ vi.mock('@/api/invitation', () => ({
inviteUsers,
}));
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: vi.fn(() => ({
identify,
})),
}));
vi.mock('@n8n/stores/useRootStore', () => ({
useRootStore: vi.fn(() => ({
instanceId: 'test-instance-id',
@@ -59,8 +53,6 @@ describe('users.store', () => {
isDefaultUser: false,
isPendingUser: false,
});
expect(identify).toHaveBeenCalledWith('test-instance-id', mockUser.id);
});
});

View File

@@ -22,17 +22,11 @@ import type { Cloud } from '@n8n/rest-api-client/api/cloudPlans';
import { getPersonalizedNodeTypes } from '@/utils/userUtils';
import { defineStore } from 'pinia';
import { useRootStore } from '@n8n/stores/useRootStore';
import { usePostHog } from './posthog.store';
import { useUIStore } from './ui.store';
import { useCloudPlanStore } from './cloudPlan.store';
import * as mfaApi from '@n8n/rest-api-client/api/mfa';
import * as cloudApi from '@n8n/rest-api-client/api/cloudPlans';
import { useRBACStore } from '@/stores/rbac.store';
import type { Scope } from '@n8n/permissions';
import * as invitationsApi from '@/api/invitation';
import { useNpsSurveyStore } from './npsSurvey.store';
import { computed, ref } from 'vue';
import { useTelemetry } from '@/composables/useTelemetry';
import { useSettingsStore } from '@/stores/settings.store';
import * as onboardingApi from '@/api/workflow-webhooks';
import * as promptsApi from '@n8n/rest-api-client/api/prompts';
@@ -42,6 +36,9 @@ const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Own
const _isDefaultUser = (user: IUserResponse | null) =>
_isInstanceOwner(user) && _isPendingUser(user);
type LoginHook = (user: CurrentUserResponse) => void;
type LogoutHook = () => void;
export const useUsersStore = defineStore(STORES.USERS, () => {
const initialized = ref(false);
const currentUserId = ref<string | null>(null);
@@ -49,20 +46,14 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
const currentUserCloudInfo = ref<Cloud.UserAccount | null>(null);
const userQuota = ref<number>(-1);
const loginHooks = ref<LoginHook[]>([]);
const logoutHooks = ref<LogoutHook[]>([]);
// Stores
const RBACStore = useRBACStore();
const npsSurveyStore = useNpsSurveyStore();
const uiStore = useUIStore();
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const cloudPlanStore = useCloudPlanStore();
const telemetry = useTelemetry();
// Composables
const postHogStore = usePostHog();
// Computed
@@ -153,11 +144,13 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
addUsers([user]);
currentUserId.value = user.id;
const defaultScopes: Scope[] = [];
RBACStore.setGlobalScopes(user.globalScopes || defaultScopes);
telemetry.identify(rootStore.instanceId, user.id);
postHogStore.init(user.featureFlags);
npsSurveyStore.setupNpsSurveyOnLogin(user.id, user.settings);
for (const hook of loginHooks.value) {
try {
hook(user);
} catch (error) {
console.error('Error executing login hook:', error);
}
}
};
const loginWithCookie = async () => {
@@ -187,8 +180,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
const unsetCurrentUser = () => {
currentUserId.value = null;
currentUserCloudInfo.value = null;
telemetry.reset();
RBACStore.setGlobalScopes([]);
};
const deleteUserById = (userId: string) => {
@@ -219,13 +210,26 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
setCurrentUser(user);
};
const registerLoginHook = (hook: LoginHook) => {
loginHooks.value.push(hook);
};
const registerLogoutHook = (hook: LogoutHook) => {
logoutHooks.value.push(hook);
};
const logout = async () => {
await usersApi.logout(rootStore.restApiContext);
unsetCurrentUser();
cloudPlanStore.reset();
postHogStore.reset();
uiStore.clearBannerStack();
npsSurveyStore.resetNpsSurveyOnLogOut();
for (const hook of logoutHooks.value) {
try {
hook();
} catch (error) {
console.error('Error executing logout hook:', error);
}
}
localStorage.removeItem(BROWSER_ID_STORAGE_KEY);
};
@@ -406,13 +410,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
await fetchUsers();
};
const reset = () => {
initialized.value = false;
currentUserId.value = null;
usersById.value = {};
currentUserCloudInfo.value = null;
};
const submitContactEmail = async (email: string, agree: boolean) => {
if (currentUser.value) {
return await onboardingApi.submitEmailOnSignup(
@@ -459,6 +456,8 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
setPersonalizationAnswers,
loginWithCreds,
logout,
registerLoginHook,
registerLogoutHook,
createOwner,
validateSignupToken,
acceptInvitation,
@@ -485,7 +484,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
fetchUserCloudAccount,
sendConfirmationEmail,
updateGlobalRole,
reset,
setEasyAIWorkflowOnboardingDone,
isCalloutDismissed,
setCalloutDismissed,