refactor(editor): Extract SAML, OIDC, and LDAP from Settings Store into SSO Store (no-changelog) (#16276)

This commit is contained in:
Alex Grozav
2025-06-16 13:54:31 +02:00
committed by GitHub
parent e93fd1a689
commit c22ca2cb4a
17 changed files with 330 additions and 247 deletions

View File

@@ -2,19 +2,21 @@
import { useSSOStore } from '@/stores/sso.store'; import { useSSOStore } from '@/stores/sso.store';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useSettingsStore } from '@/stores/settings.store'; import { useRoute } from 'vue-router';
const i18n = useI18n(); const i18n = useI18n();
const ssoStore = useSSOStore(); const ssoStore = useSSOStore();
const toast = useToast(); const toast = useToast();
const settingsStore = useSettingsStore(); const route = useRoute();
const onSSOLogin = async () => { const onSSOLogin = async () => {
try { try {
const redirectUrl = ssoStore.isDefaultAuthenticationSaml const redirectUrl = ssoStore.isDefaultAuthenticationSaml
? await ssoStore.getSSORedirectUrl() ? await ssoStore.getSSORedirectUrl(
: settingsStore.settings.sso.oidc.loginUrl; typeof route.query?.redirect === 'string' ? route.query.redirect : '',
window.location.href = redirectUrl; )
: ssoStore.oidc.loginUrl;
window.location.href = redirectUrl ?? '';
} catch (error) { } catch (error) {
toast.showError(error, 'Error', error.message); toast.showError(error, 'Error', error.message);
} }

View File

@@ -3,12 +3,18 @@ import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@n8n/stores/useRootStore'; import { useRootStore } from '@n8n/stores/useRootStore';
import { initializeAuthenticatedFeatures, initializeCore } from '@/init'; import { state, initializeAuthenticatedFeatures, initializeCore } from '@/init';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia'; import { setActivePinia } from 'pinia';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useVersionsStore } from '@/stores/versions.store'; import { useVersionsStore } from '@/stores/versions.store';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import merge from 'lodash/merge';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import { STORES } from '@n8n/stores';
import { useSSOStore } from '@/stores/sso.store';
import { UserManagementAuthenticationMethod } from '@/Interface';
import { EnterpriseEditionFeature } from '@/constants';
const showMessage = vi.fn(); const showMessage = vi.fn();
@@ -31,9 +37,17 @@ describe('Init', () => {
let usersStore: ReturnType<typeof useUsersStore>; let usersStore: ReturnType<typeof useUsersStore>;
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>; let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
let versionsStore: ReturnType<typeof useVersionsStore>; let versionsStore: ReturnType<typeof useVersionsStore>;
let ssoStore: ReturnType<typeof useSSOStore>;
beforeEach(() => { beforeEach(() => {
setActivePinia(createTestingPinia()); setActivePinia(
createTestingPinia({
initialState: {
[STORES.SETTINGS]: merge({}, SETTINGS_STORE_DEFAULT_STATE),
},
}),
);
settingsStore = useSettingsStore(); settingsStore = useSettingsStore();
cloudPlanStore = useCloudPlanStore(); cloudPlanStore = useCloudPlanStore();
sourceControlStore = useSourceControlStore(); sourceControlStore = useSourceControlStore();
@@ -41,9 +55,14 @@ describe('Init', () => {
usersStore = useUsersStore(); usersStore = useUsersStore();
versionsStore = useVersionsStore(); versionsStore = useVersionsStore();
versionsStore = useVersionsStore(); versionsStore = useVersionsStore();
ssoStore = useSSOStore();
}); });
describe('initializeCore()', () => { describe('initializeCore()', () => {
beforeEach(() => {
state.initialized = false;
});
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@@ -63,6 +82,28 @@ describe('Init', () => {
expect(settingsStoreSpy).toHaveBeenCalledTimes(1); expect(settingsStoreSpy).toHaveBeenCalledTimes(1);
}); });
it('should initialize ssoStore with settings SSO configuration', async () => {
const saml = { loginEnabled: true, loginLabel: '' };
const ldap = { loginEnabled: false, loginLabel: '' };
const oidc = { loginEnabled: false, loginUrl: '', callbackUrl: '' };
settingsStore.userManagement.authenticationMethod = UserManagementAuthenticationMethod.Saml;
settingsStore.settings.sso = { saml, ldap, oidc };
settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Saml] = true;
await initializeCore();
expect(ssoStore.initialize).toHaveBeenCalledWith({
authenticationMethod: UserManagementAuthenticationMethod.Saml,
config: { saml, ldap, oidc },
features: {
saml: true,
ldap: false,
oidc: false,
},
});
});
}); });
describe('initializeAuthenticatedFeatures()', () => { describe('initializeAuthenticatedFeatures()', () => {

View File

@@ -13,8 +13,13 @@ import { useInsightsStore } from '@/features/insights/insights.store';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import SourceControlInitializationErrorMessage from '@/components/SourceControlInitializationErrorMessage.vue'; import SourceControlInitializationErrorMessage from '@/components/SourceControlInitializationErrorMessage.vue';
import { useSSOStore } from '@/stores/sso.store';
import { EnterpriseEditionFeature } from '@/constants';
import type { UserManagementAuthenticationMethod } from '@/Interface';
let coreInitialized = false; export const state = {
initialized: false,
};
let authenticatedFeaturesInitialized = false; let authenticatedFeaturesInitialized = false;
/** /**
@@ -22,16 +27,28 @@ let authenticatedFeaturesInitialized = false;
* This is called once, when the first route is loaded. * This is called once, when the first route is loaded.
*/ */
export async function initializeCore() { export async function initializeCore() {
if (coreInitialized) { if (state.initialized) {
return; return;
} }
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const usersStore = useUsersStore(); const usersStore = useUsersStore();
const versionsStore = useVersionsStore(); const versionsStore = useVersionsStore();
const ssoStore = useSSOStore();
await settingsStore.initialize(); await settingsStore.initialize();
ssoStore.initialize({
authenticationMethod: settingsStore.userManagement
.authenticationMethod as UserManagementAuthenticationMethod,
config: settingsStore.settings.sso,
features: {
saml: settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Saml],
ldap: settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Ldap],
oidc: settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Oidc],
},
});
void useExternalHooks().run('app.mount'); void useExternalHooks().run('app.mount');
if (!settingsStore.isPreviewMode) { if (!settingsStore.isPreviewMode) {
@@ -40,7 +57,7 @@ export async function initializeCore() {
void versionsStore.checkForNewVersions(); void versionsStore.checkForNewVersions();
} }
coreInitialized = true; state.initialized = true;
} }
/** /**

View File

@@ -3,11 +3,9 @@ import Bowser from 'bowser';
import type { IUserManagementSettings, FrontendSettings } from '@n8n/api-types'; import type { IUserManagementSettings, FrontendSettings } from '@n8n/api-types';
import * as eventsApi from '@n8n/rest-api-client/api/events'; import * as eventsApi from '@n8n/rest-api-client/api/events';
import * as ldapApi from '@n8n/rest-api-client/api/ldap';
import * as settingsApi from '@n8n/rest-api-client/api/settings'; import * as settingsApi from '@n8n/rest-api-client/api/settings';
import * as promptsApi from '@n8n/rest-api-client/api/prompts'; import * as promptsApi from '@n8n/rest-api-client/api/prompts';
import { testHealthEndpoint } from '@/api/templates'; import { testHealthEndpoint } from '@/api/templates';
import type { LdapConfig } from '@n8n/rest-api-client/api/ldap';
import { import {
INSECURE_CONNECTION_WARNING, INSECURE_CONNECTION_WARNING,
LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS, LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
@@ -44,9 +42,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
enabled: false, enabled: false,
}, },
}); });
const ldap = ref({ loginLabel: '', loginEnabled: false });
const saml = ref({ loginLabel: '', loginEnabled: false });
const oidc = ref({ loginEnabled: false, loginUrl: '', callbackUrl: '' });
const mfa = ref({ enabled: false }); const mfa = ref({ enabled: false });
const folders = ref({ enabled: false }); const folders = ref({ enabled: false });
@@ -90,16 +85,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const publicApiPath = computed(() => api.value.path); const publicApiPath = computed(() => api.value.path);
const isLdapLoginEnabled = computed(() => ldap.value.loginEnabled);
const ldapLoginLabel = computed(() => ldap.value.loginLabel);
const isSamlLoginEnabled = computed(() => saml.value.loginEnabled);
const isOidcLoginEnabled = computed(() => oidc.value.loginEnabled);
const oidcCallBackUrl = computed(() => oidc.value.callbackUrl);
const isAiAssistantEnabled = computed(() => settings.value.aiAssistant?.enabled); const isAiAssistantEnabled = computed(() => settings.value.aiAssistant?.enabled);
const isAskAiEnabled = computed(() => settings.value.askAi?.enabled); const isAskAiEnabled = computed(() => settings.value.askAi?.enabled);
@@ -182,14 +167,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
() => settings.value.workflowCallerPolicyDefaultOption, () => settings.value.workflowCallerPolicyDefaultOption,
); );
const isDefaultAuthenticationSaml = computed(
() => userManagement.value.authenticationMethod === UserManagementAuthenticationMethod.Saml,
);
const isDefaultAuthenticationOidc = computed(
() => userManagement.value.authenticationMethod === UserManagementAuthenticationMethod.Oidc,
);
const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []); const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []);
const isBelowUserQuota = computed( const isBelowUserQuota = computed(
@@ -210,21 +187,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
!!settings.value.userManagement.showSetupOnFirstLoad; !!settings.value.userManagement.showSetupOnFirstLoad;
} }
api.value = settings.value.publicApi; api.value = settings.value.publicApi;
if (settings.value.sso?.ldap) {
ldap.value.loginEnabled = settings.value.sso.ldap.loginEnabled;
ldap.value.loginLabel = settings.value.sso.ldap.loginLabel;
}
if (settings.value.sso?.saml) {
saml.value.loginEnabled = settings.value.sso.saml.loginEnabled;
saml.value.loginLabel = settings.value.sso.saml.loginLabel;
}
if (settings.value.sso?.oidc) {
oidc.value.loginEnabled = settings.value.sso.oidc.loginEnabled;
oidc.value.loginUrl = settings.value.sso.oidc.loginUrl || '';
oidc.value.callbackUrl = settings.value.sso.oidc.callbackUrl || '';
}
mfa.value.enabled = settings.value.mfa?.enabled; mfa.value.enabled = settings.value.mfa?.enabled;
folders.value.enabled = settings.value.folders?.enabled; folders.value.enabled = settings.value.folders?.enabled;
@@ -364,31 +326,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
templatesEndpointHealthy.value = true; templatesEndpointHealthy.value = true;
}; };
const getLdapConfig = async () => {
const rootStore = useRootStore();
return await ldapApi.getLdapConfig(rootStore.restApiContext);
};
const getLdapSynchronizations = async (pagination: { page: number }) => {
const rootStore = useRootStore();
return await ldapApi.getLdapSynchronizations(rootStore.restApiContext, pagination);
};
const testLdapConnection = async () => {
const rootStore = useRootStore();
return await ldapApi.testLdapConnection(rootStore.restApiContext);
};
const updateLdapConfig = async (ldapConfig: LdapConfig) => {
const rootStore = useRootStore();
return await ldapApi.updateLdapConfig(rootStore.restApiContext, ldapConfig);
};
const runLdapSync = async (data: IDataObject) => {
const rootStore = useRootStore();
return await ldapApi.runLdapSync(rootStore.restApiContext, data);
};
const getTimezones = async (): Promise<IDataObject> => { const getTimezones = async (): Promise<IDataObject> => {
const rootStore = useRootStore(); const rootStore = useRootStore();
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones'); return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones');
@@ -412,8 +349,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
userManagement, userManagement,
templatesEndpointHealthy, templatesEndpointHealthy,
api, api,
ldap,
saml,
mfa, mfa,
isDocker, isDocker,
isDevRelease, isDevRelease,
@@ -432,10 +367,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
isPreviewMode, isPreviewMode,
publicApiLatestVersion, publicApiLatestVersion,
publicApiPath, publicApiPath,
isLdapLoginEnabled,
ldapLoginLabel,
isSamlLoginEnabled,
isOidcLoginEnabled,
showSetupPage, showSetupPage,
deploymentType, deploymentType,
isCloudDeployment, isCloudDeployment,
@@ -459,8 +390,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
isQueueModeEnabled, isQueueModeEnabled,
isMultiMain, isMultiMain,
isWorkerViewAvailable, isWorkerViewAvailable,
isDefaultAuthenticationSaml,
isDefaultAuthenticationOidc,
workflowCallerPolicyDefaultOption, workflowCallerPolicyDefaultOption,
permanentlyDismissedBanners, permanentlyDismissedBanners,
isBelowUserQuota, isBelowUserQuota,
@@ -474,13 +403,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
aiCreditsQuota, aiCreditsQuota,
experimental__minZoomNodeSettingsInCanvas, experimental__minZoomNodeSettingsInCanvas,
partialExecutionVersion, partialExecutionVersion,
oidcCallBackUrl,
reset, reset,
testLdapConnection,
getLdapConfig,
getLdapSynchronizations,
updateLdapConfig,
runLdapSync,
getTimezones, getTimezones,
testTemplatesEndpoint, testTemplatesEndpoint,
submitContactInfo, submitContactInfo,

View File

@@ -1,75 +1,19 @@
import type { OidcConfigDto } from '@n8n/api-types'; import type { OidcConfigDto } from '@n8n/api-types';
import { type SamlPreferences } from '@n8n/api-types'; import { type SamlPreferences } from '@n8n/api-types';
import { computed, reactive } from 'vue'; import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { EnterpriseEditionFeature } from '@/constants';
import { useRootStore } from '@n8n/stores/useRootStore'; import { useRootStore } from '@n8n/stores/useRootStore';
import { useSettingsStore } from '@/stores/settings.store';
import * as ssoApi from '@n8n/rest-api-client/api/sso'; import * as ssoApi from '@n8n/rest-api-client/api/sso';
import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso'; import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso';
import { updateCurrentUser } from '@/api/users'; import * as ldapApi from '@n8n/rest-api-client/api/ldap';
import { useUsersStore } from '@/stores/users.store'; import type { LdapConfig } from '@n8n/rest-api-client/api/ldap';
import type { IDataObject } from 'n8n-workflow';
import { UserManagementAuthenticationMethod } from '@/Interface';
export const useSSOStore = defineStore('sso', () => { export const useSSOStore = defineStore('sso', () => {
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const usersStore = useUsersStore();
const route = useRoute();
const state = reactive({ const authenticationMethod = ref<UserManagementAuthenticationMethod | undefined>(undefined);
samlConfig: undefined as (SamlPreferences & SamlPreferencesExtractedData) | undefined,
oidcConfig: undefined as OidcConfigDto | undefined,
});
const samlConfig = computed(() => state.samlConfig);
const oidcConfig = computed(() => state.oidcConfig);
const isSamlLoginEnabled = computed({
get: () => settingsStore.isSamlLoginEnabled,
set: (value: boolean) => {
settingsStore.setSettings({
...settingsStore.settings,
sso: {
...settingsStore.settings.sso,
saml: {
...settingsStore.settings.sso.saml,
loginEnabled: value,
},
},
});
void toggleLoginEnabled(value);
},
});
const isOidcLoginEnabled = computed({
get: () => settingsStore.isOidcLoginEnabled,
set: (value: boolean) => {
settingsStore.setSettings({
...settingsStore.settings,
sso: {
...settingsStore.settings.sso,
oidc: {
...settingsStore.settings.sso.oidc,
loginEnabled: value,
},
},
});
},
});
const isEnterpriseSamlEnabled = computed(
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Saml],
);
const isEnterpriseOidcEnabled = computed(
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Oidc],
);
const isDefaultAuthenticationSaml = computed(() => settingsStore.isDefaultAuthenticationSaml);
const isDefaultAuthenticationOidc = computed(() => settingsStore.isDefaultAuthenticationOidc);
const showSsoLoginButton = computed( const showSsoLoginButton = computed(
() => () =>
@@ -81,10 +25,70 @@ export const useSSOStore = defineStore('sso', () => {
isDefaultAuthenticationOidc.value), isDefaultAuthenticationOidc.value),
); );
const getSSORedirectUrl = async () => const getSSORedirectUrl = async (existingRedirect?: string) =>
await ssoApi.initSSO( await ssoApi.initSSO(rootStore.restApiContext, existingRedirect);
rootStore.restApiContext,
typeof route.query?.redirect === 'string' ? route.query.redirect : '', const initialize = (options: {
authenticationMethod: UserManagementAuthenticationMethod;
config: {
ldap?: Pick<LdapConfig, 'loginLabel' | 'loginEnabled'>;
saml?: Pick<SamlPreferences, 'loginLabel' | 'loginEnabled'>;
oidc?: Pick<OidcConfigDto, 'loginEnabled'> & {
loginUrl?: string;
callbackUrl?: string;
};
};
features: {
saml: boolean;
ldap: boolean;
oidc: boolean;
};
}) => {
authenticationMethod.value = options.authenticationMethod;
isEnterpriseLdapEnabled.value = options.features.ldap;
if (options.config.ldap) {
ldap.value.loginEnabled = options.config.ldap.loginEnabled;
ldap.value.loginLabel = options.config.ldap.loginLabel;
}
isEnterpriseSamlEnabled.value = options.features.saml;
if (options.config.saml) {
saml.value.loginEnabled = options.config.saml.loginEnabled;
saml.value.loginLabel = options.config.saml.loginLabel;
}
isEnterpriseOidcEnabled.value = options.features.oidc;
if (options.config.oidc) {
oidc.value.loginEnabled = options.config.oidc.loginEnabled;
oidc.value.loginUrl = options.config.oidc.loginUrl || '';
oidc.value.callbackUrl = options.config.oidc.callbackUrl || '';
}
};
/**
* SAML
*/
const saml = ref<Pick<SamlPreferences, 'loginLabel' | 'loginEnabled'>>({
loginLabel: '',
loginEnabled: false,
});
const samlConfig = ref<SamlPreferences & SamlPreferencesExtractedData>();
const isSamlLoginEnabled = computed({
get: () => saml.value.loginEnabled,
set: (value: boolean) => {
saml.value.loginEnabled = value;
void toggleLoginEnabled(value);
},
});
const isEnterpriseSamlEnabled = ref(false);
const isDefaultAuthenticationSaml = computed(
() => authenticationMethod.value === UserManagementAuthenticationMethod.Saml,
); );
const toggleLoginEnabled = async (enabled: boolean) => const toggleLoginEnabled = async (enabled: boolean) =>
@@ -92,55 +96,130 @@ export const useSSOStore = defineStore('sso', () => {
const getSamlMetadata = async () => await ssoApi.getSamlMetadata(rootStore.restApiContext); const getSamlMetadata = async () => await ssoApi.getSamlMetadata(rootStore.restApiContext);
// const getOidcRedirectLUrl = async () => await ssoApi)
const getSamlConfig = async () => { const getSamlConfig = async () => {
const samlConfig = await ssoApi.getSamlConfig(rootStore.restApiContext); const config = await ssoApi.getSamlConfig(rootStore.restApiContext);
state.samlConfig = samlConfig; samlConfig.value = config;
return samlConfig; return config;
}; };
const saveSamlConfig = async (config: Partial<SamlPreferences>) => const saveSamlConfig = async (config: Partial<SamlPreferences>) =>
await ssoApi.saveSamlConfig(rootStore.restApiContext, config); await ssoApi.saveSamlConfig(rootStore.restApiContext, config);
const testSamlConfig = async () => await ssoApi.testSamlConfig(rootStore.restApiContext); const testSamlConfig = async () => await ssoApi.testSamlConfig(rootStore.restApiContext);
const updateUser = async (params: { firstName: string; lastName: string }) => /**
await updateCurrentUser(rootStore.restApiContext, { * OIDC
email: usersStore.currentUser!.email!, */
...params,
const oidc = ref<
Pick<OidcConfigDto, 'loginEnabled'> & {
loginUrl?: string;
callbackUrl?: string;
}
>({
loginUrl: '',
loginEnabled: false,
callbackUrl: '',
}); });
const userData = computed(() => usersStore.currentUser); const oidcConfig = ref<OidcConfigDto | undefined>();
const isEnterpriseOidcEnabled = ref(false);
const getOidcConfig = async () => { const getOidcConfig = async () => {
const oidcConfig = await ssoApi.getOidcConfig(rootStore.restApiContext); const config = await ssoApi.getOidcConfig(rootStore.restApiContext);
state.oidcConfig = oidcConfig; oidcConfig.value = config;
return oidcConfig; return config;
}; };
const saveOidcConfig = async (config: OidcConfigDto) => { const saveOidcConfig = async (config: OidcConfigDto) => {
const savedConfig = await ssoApi.saveOidcConfig(rootStore.restApiContext, config); const savedConfig = await ssoApi.saveOidcConfig(rootStore.restApiContext, config);
state.oidcConfig = savedConfig; oidcConfig.value = savedConfig;
return savedConfig; return savedConfig;
}; };
const isOidcLoginEnabled = computed({
get: () => oidc.value.loginEnabled,
set: (value: boolean) => {
oidc.value.loginEnabled = value;
},
});
const isDefaultAuthenticationOidc = computed(
() => authenticationMethod.value === UserManagementAuthenticationMethod.Oidc,
);
/**
* LDAP Configuration
*/
const ldap = ref<Pick<LdapConfig, 'loginLabel' | 'loginEnabled'>>({
loginLabel: '',
loginEnabled: false,
});
const isEnterpriseLdapEnabled = ref(false);
const isLdapLoginEnabled = computed(() => ldap.value.loginEnabled);
const ldapLoginLabel = computed(() => ldap.value.loginLabel);
const getLdapConfig = async () => {
const rootStore = useRootStore();
return await ldapApi.getLdapConfig(rootStore.restApiContext);
};
const getLdapSynchronizations = async (pagination: { page: number }) => {
const rootStore = useRootStore();
return await ldapApi.getLdapSynchronizations(rootStore.restApiContext, pagination);
};
const testLdapConnection = async () => {
const rootStore = useRootStore();
return await ldapApi.testLdapConnection(rootStore.restApiContext);
};
const updateLdapConfig = async (ldapConfig: LdapConfig) => {
const rootStore = useRootStore();
return await ldapApi.updateLdapConfig(rootStore.restApiContext, ldapConfig);
};
const runLdapSync = async (data: IDataObject) => {
const rootStore = useRootStore();
return await ldapApi.runLdapSync(rootStore.restApiContext, data);
};
return { return {
isEnterpriseOidcEnabled, showSsoLoginButton,
getSSORedirectUrl,
initialize,
saml,
samlConfig,
isSamlLoginEnabled, isSamlLoginEnabled,
isOidcLoginEnabled,
isEnterpriseSamlEnabled, isEnterpriseSamlEnabled,
isDefaultAuthenticationSaml, isDefaultAuthenticationSaml,
isDefaultAuthenticationOidc,
showSsoLoginButton,
samlConfig,
oidcConfig,
userData,
getSSORedirectUrl,
getSamlMetadata, getSamlMetadata,
getSamlConfig, getSamlConfig,
saveSamlConfig, saveSamlConfig,
testSamlConfig, testSamlConfig,
updateUser,
oidc,
oidcConfig,
isOidcLoginEnabled,
isEnterpriseOidcEnabled,
isDefaultAuthenticationOidc,
getOidcConfig, getOidcConfig,
saveOidcConfig, saveOidcConfig,
ldap,
isLdapLoginEnabled,
isEnterpriseLdapEnabled,
ldapLoginLabel,
getLdapConfig,
getLdapSynchronizations,
testLdapConnection,
updateLdapConfig,
runLdapSync,
}; };
}); });

View File

@@ -1,20 +1,13 @@
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { useSettingsStore } from '@/stores/settings.store';
import { useSSOStore } from '@/stores/sso.store'; import { useSSOStore } from '@/stores/sso.store';
import merge from 'lodash/merge'; import type { UserManagementAuthenticationMethod } from '@/Interface';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import type { FrontendSettings } from '@n8n/api-types';
let ssoStore: ReturnType<typeof useSSOStore>; let ssoStore: ReturnType<typeof useSSOStore>;
let settingsStore: ReturnType<typeof useSettingsStore>;
const DEFAULT_SETTINGS: FrontendSettings = SETTINGS_STORE_DEFAULT_STATE.settings;
describe('SSO store', () => { describe('SSO store', () => {
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()); setActivePinia(createPinia());
ssoStore = useSSOStore(); ssoStore = useSSOStore();
settingsStore = useSettingsStore();
}); });
test.each([ test.each([
@@ -26,21 +19,19 @@ describe('SSO store', () => {
])( ])(
'should check SSO login button availability when authenticationMethod is %s and enterprise feature is %s and sso login is set to %s', 'should check SSO login button availability when authenticationMethod is %s and enterprise feature is %s and sso login is set to %s',
(authenticationMethod, saml, loginEnabled, expectation) => { (authenticationMethod, saml, loginEnabled, expectation) => {
settingsStore.setSettings( ssoStore.initialize({
merge({}, DEFAULT_SETTINGS, { authenticationMethod: authenticationMethod as UserManagementAuthenticationMethod,
userManagement: { config: {
authenticationMethod,
},
enterprise: {
saml,
},
sso: {
saml: { saml: {
loginEnabled, loginEnabled,
}, },
}, },
}), features: {
); saml,
ldap: false,
oidc: false,
},
});
expect(ssoStore.showSsoLoginButton).toBe(expectation); expect(ssoStore.showSsoLoginButton).toBe(expectation);
}, },

View File

@@ -264,6 +264,18 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
const updateUser = async (params: UserUpdateRequestDto) => { const updateUser = async (params: UserUpdateRequestDto) => {
const user = await usersApi.updateCurrentUser(rootStore.restApiContext, params); const user = await usersApi.updateCurrentUser(rootStore.restApiContext, params);
addUsers([user]); addUsers([user]);
return user;
};
const updateUserName = async (params: { firstName: string; lastName: string }) => {
if (!currentUser.value) {
return;
}
return await updateUser({
email: currentUser.value.email as string,
...params,
});
}; };
const updateUserSettings = async (settings: SettingsUpdateRequestDto) => { const updateUserSettings = async (settings: SettingsUpdateRequestDto) => {
@@ -430,6 +442,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
validatePasswordToken, validatePasswordToken,
changePassword, changePassword,
updateUser, updateUser,
updateUserName,
updateUserSettings, updateUserSettings,
updateOtherUserSettings, updateOtherUserSettings,
updateCurrentUserPassword, updateCurrentUserPassword,

View File

@@ -3,10 +3,10 @@ import { useRouter } from 'vue-router';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import SamlOnboarding from '@/views/SamlOnboarding.vue'; import SamlOnboarding from '@/views/SamlOnboarding.vue';
import { useSSOStore } from '@/stores/sso.store';
import { STORES } from '@n8n/stores'; import { STORES } from '@n8n/stores';
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils'; import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { useUsersStore } from '@/stores/users.store';
vi.mock('vue-router', () => { vi.mock('vue-router', () => {
const push = vi.fn(); const push = vi.fn();
@@ -20,7 +20,7 @@ vi.mock('vue-router', () => {
}); });
let pinia: ReturnType<typeof createTestingPinia>; let pinia: ReturnType<typeof createTestingPinia>;
let ssoStore: ReturnType<typeof useSSOStore>; let usersStore: ReturnType<typeof useUsersStore>;
let router: ReturnType<typeof useRouter>; let router: ReturnType<typeof useRouter>;
const renderComponent = createComponentRenderer(SamlOnboarding); const renderComponent = createComponentRenderer(SamlOnboarding);
@@ -34,7 +34,7 @@ describe('SamlOnboarding', () => {
}, },
}, },
}); });
ssoStore = useSSOStore(pinia); usersStore = useUsersStore(pinia);
router = useRouter(); router = useRouter();
}); });
@@ -43,7 +43,7 @@ describe('SamlOnboarding', () => {
}); });
it('should submit filled in form only and redirect', async () => { it('should submit filled in form only and redirect', async () => {
vi.spyOn(ssoStore, 'updateUser').mockResolvedValue({ vi.spyOn(usersStore, 'updateUserName').mockResolvedValue({
id: '1', id: '1',
isPending: false, isPending: false,
}); });
@@ -56,14 +56,14 @@ describe('SamlOnboarding', () => {
await userEvent.click(submit); await userEvent.click(submit);
await waitAllPromises(); await waitAllPromises();
expect(ssoStore.updateUser).not.toHaveBeenCalled(); expect(usersStore.updateUserName).not.toHaveBeenCalled();
expect(router.push).not.toHaveBeenCalled(); expect(router.push).not.toHaveBeenCalled();
await userEvent.type(inputs[0], 'test'); await userEvent.type(inputs[0], 'test');
await userEvent.type(inputs[1], 'test'); await userEvent.type(inputs[1], 'test');
await userEvent.click(submit); await userEvent.click(submit);
expect(ssoStore.updateUser).toHaveBeenCalled(); expect(usersStore.updateUserName).toHaveBeenCalled();
expect(router.push).toHaveBeenCalled(); expect(router.push).toHaveBeenCalled();
}); });
}); });

View File

@@ -6,13 +6,13 @@ import AuthView from '@/views/AuthView.vue';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useSSOStore } from '@/stores/sso.store'; import { useUsersStore } from '@/stores/users.store';
const router = useRouter(); const router = useRouter();
const locale = useI18n(); const locale = useI18n();
const toast = useToast(); const toast = useToast();
const ssoStore = useSSOStore(); const usersStore = useUsersStore();
const loading = ref(false); const loading = ref(false);
const FORM_CONFIG: IFormBoxConfig = reactive({ const FORM_CONFIG: IFormBoxConfig = reactive({
@@ -21,7 +21,7 @@ const FORM_CONFIG: IFormBoxConfig = reactive({
inputs: [ inputs: [
{ {
name: 'firstName', name: 'firstName',
initialValue: ssoStore.userData?.firstName, initialValue: usersStore.currentUser?.firstName,
properties: { properties: {
label: locale.baseText('auth.firstName'), label: locale.baseText('auth.firstName'),
maxlength: 32, maxlength: 32,
@@ -32,7 +32,7 @@ const FORM_CONFIG: IFormBoxConfig = reactive({
}, },
{ {
name: 'lastName', name: 'lastName',
initialValue: ssoStore.userData?.lastName, initialValue: usersStore.currentUser?.lastName,
properties: { properties: {
label: locale.baseText('auth.lastName'), label: locale.baseText('auth.lastName'),
maxlength: 32, maxlength: 32,
@@ -54,7 +54,7 @@ const onSubmit = async (values: { [key: string]: string }) => {
if (!isFormWithFirstAndLastName(values)) return; if (!isFormWithFirstAndLastName(values)) return;
try { try {
loading.value = true; loading.value = true;
await ssoStore.updateUser(values); await usersStore.updateUserName(values);
await router.push({ name: VIEWS.HOMEPAGE }); await router.push({ name: VIEWS.HOMEPAGE });
} catch (error) { } catch (error) {
loading.value = false; loading.value = false;

View File

@@ -19,6 +19,7 @@ import { createFormEventBus } from '@n8n/design-system/utils';
import type { TableColumnCtx } from 'element-plus'; import type { TableColumnCtx } from 'element-plus';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useSSOStore } from '@/stores/sso.store';
type TableRow = { type TableRow = {
status: string; status: string;
@@ -65,6 +66,7 @@ const documentTitle = useDocumentTitle();
const pageRedirectionHelper = usePageRedirectionHelper(); const pageRedirectionHelper = usePageRedirectionHelper();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const ssoStore = useSSOStore();
const dataTable = ref<LdapSyncTable[]>([]); const dataTable = ref<LdapSyncTable[]>([]);
const tableKey = ref(0); const tableKey = ref(0);
@@ -192,7 +194,7 @@ const onSubmit = async () => {
hasAnyChanges.value = true; hasAnyChanges.value = true;
} }
adConfig.value = await settingsStore.updateLdapConfig(newConfiguration); adConfig.value = await ssoStore.updateLdapConfig(newConfiguration);
toast.showToast({ toast.showToast({
title: i18n.baseText('settings.ldap.updateConfiguration'), title: i18n.baseText('settings.ldap.updateConfiguration'),
message: '', message: '',
@@ -214,7 +216,7 @@ const onSaveClick = () => {
const onTestConnectionClick = async () => { const onTestConnectionClick = async () => {
loadingTestConnection.value = true; loadingTestConnection.value = true;
try { try {
await settingsStore.testLdapConnection(); await ssoStore.testLdapConnection();
toast.showToast({ toast.showToast({
title: i18n.baseText('settings.ldap.connectionTest'), title: i18n.baseText('settings.ldap.connectionTest'),
message: i18n.baseText('settings.ldap.toast.connection.success'), message: i18n.baseText('settings.ldap.toast.connection.success'),
@@ -234,7 +236,7 @@ const onTestConnectionClick = async () => {
const onDryRunClick = async () => { const onDryRunClick = async () => {
loadingDryRun.value = true; loadingDryRun.value = true;
try { try {
await settingsStore.runLdapSync({ type: 'dry' }); await ssoStore.runLdapSync({ type: 'dry' });
toast.showToast({ toast.showToast({
title: i18n.baseText('settings.ldap.runSync.title'), title: i18n.baseText('settings.ldap.runSync.title'),
message: i18n.baseText('settings.ldap.toast.sync.success'), message: i18n.baseText('settings.ldap.toast.sync.success'),
@@ -251,7 +253,7 @@ const onDryRunClick = async () => {
const onLiveRunClick = async () => { const onLiveRunClick = async () => {
loadingLiveRun.value = true; loadingLiveRun.value = true;
try { try {
await settingsStore.runLdapSync({ type: 'live' }); await ssoStore.runLdapSync({ type: 'live' });
toast.showToast({ toast.showToast({
title: i18n.baseText('settings.ldap.runSync.title'), title: i18n.baseText('settings.ldap.runSync.title'),
message: i18n.baseText('settings.ldap.toast.sync.success'), message: i18n.baseText('settings.ldap.toast.sync.success'),
@@ -267,7 +269,7 @@ const onLiveRunClick = async () => {
const getLdapConfig = async () => { const getLdapConfig = async () => {
try { try {
adConfig.value = await settingsStore.getLdapConfig(); adConfig.value = await ssoStore.getLdapConfig();
loginEnabled.value = adConfig.value.loginEnabled; loginEnabled.value = adConfig.value.loginEnabled;
syncEnabled.value = adConfig.value.synchronizationEnabled; syncEnabled.value = adConfig.value.synchronizationEnabled;
const whenLoginEnabled: IFormInput['shouldDisplay'] = (values) => values.loginEnabled === true; const whenLoginEnabled: IFormInput['shouldDisplay'] = (values) => values.loginEnabled === true;
@@ -554,7 +556,7 @@ const getLdapConfig = async () => {
const getLdapSynchronizations = async (state: Parameters<Events['infinite']>[0]) => { const getLdapSynchronizations = async (state: Parameters<Events['infinite']>[0]) => {
try { try {
loadingTable.value = true; loadingTable.value = true;
const data = await settingsStore.getLdapSynchronizations({ const data = await ssoStore.getLdapSynchronizations({
page: page.value, page: page.value,
}); });

View File

@@ -9,9 +9,12 @@ import { setupServer } from '@/__tests__/server';
import { ROLE } from '@n8n/api-types'; import { ROLE } from '@n8n/api-types';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useSSOStore } from '@/stores/sso.store';
import { UserManagementAuthenticationMethod } from '@/Interface';
let pinia: ReturnType<typeof createPinia>; let pinia: ReturnType<typeof createPinia>;
let settingsStore: ReturnType<typeof useSettingsStore>; let settingsStore: ReturnType<typeof useSettingsStore>;
let ssoStore: ReturnType<typeof useSSOStore>;
let usersStore: ReturnType<typeof useUsersStore>; let usersStore: ReturnType<typeof useUsersStore>;
let uiStore: ReturnType<typeof useUIStore>; let uiStore: ReturnType<typeof useUIStore>;
let cloudPlanStore: ReturnType<typeof useCloudPlanStore>; let cloudPlanStore: ReturnType<typeof useCloudPlanStore>;
@@ -41,6 +44,7 @@ describe('SettingsPersonalView', () => {
pinia = createPinia(); pinia = createPinia();
settingsStore = useSettingsStore(pinia); settingsStore = useSettingsStore(pinia);
ssoStore = useSSOStore(pinia);
usersStore = useUsersStore(pinia); usersStore = useUsersStore(pinia);
uiStore = useUIStore(pinia); uiStore = useUIStore(pinia);
cloudPlanStore = useCloudPlanStore(pinia); cloudPlanStore = useCloudPlanStore(pinia);
@@ -49,6 +53,15 @@ describe('SettingsPersonalView', () => {
usersStore.currentUserId = currentUser.id; usersStore.currentUserId = currentUser.id;
await settingsStore.getSettings(); await settingsStore.getSettings();
ssoStore.initialize({
authenticationMethod: UserManagementAuthenticationMethod.Email,
config: settingsStore.settings.sso,
features: {
saml: true,
ldap: true,
oidc: true,
},
});
}); });
afterAll(() => { afterAll(() => {
@@ -96,7 +109,9 @@ describe('SettingsPersonalView', () => {
}); });
it('should commit the theme change after clicking save', async () => { it('should commit the theme change after clicking save', async () => {
vi.spyOn(usersStore, 'updateUser').mockReturnValue(Promise.resolve()); vi.spyOn(usersStore, 'updateUser').mockReturnValue(
Promise.resolve({ id: '123', isPending: false }),
);
const { getByPlaceholderText, findByText, getByTestId } = renderComponent({ pinia }); const { getByPlaceholderText, findByText, getByTestId } = renderComponent({ pinia });
await waitAllPromises(); await waitAllPromises();
@@ -116,8 +131,8 @@ describe('SettingsPersonalView', () => {
describe('when external auth is enabled, email and password change', () => { describe('when external auth is enabled, email and password change', () => {
beforeEach(() => { beforeEach(() => {
vi.spyOn(settingsStore, 'isSamlLoginEnabled', 'get').mockReturnValue(true); vi.spyOn(ssoStore, 'isSamlLoginEnabled', 'get').mockReturnValue(true);
vi.spyOn(settingsStore, 'isDefaultAuthenticationSaml', 'get').mockReturnValue(true); vi.spyOn(ssoStore, 'isDefaultAuthenticationSaml', 'get').mockReturnValue(true);
vi.spyOn(settingsStore, 'isMfaFeatureEnabled', 'get').mockReturnValue(true); vi.spyOn(settingsStore, 'isMfaFeatureEnabled', 'get').mockReturnValue(true);
}); });

View File

@@ -19,6 +19,7 @@ import { createFormEventBus } from '@n8n/design-system/utils';
import type { MfaModalEvents } from '@/event-bus/mfa'; import type { MfaModalEvents } from '@/event-bus/mfa';
import { promptMfaCodeBus } from '@/event-bus/mfa'; import { promptMfaCodeBus } from '@/event-bus/mfa';
import type { BaseTextKey } from '@n8n/i18n'; import type { BaseTextKey } from '@n8n/i18n';
import { useSSOStore } from '@/stores/sso.store';
type UserBasicDetailsForm = { type UserBasicDetailsForm = {
firstName: string; firstName: string;
@@ -62,32 +63,38 @@ const themeOptions = ref<Array<{ name: ThemeOption; label: BaseTextKey }>>([
const uiStore = useUIStore(); const uiStore = useUIStore();
const usersStore = useUsersStore(); const usersStore = useUsersStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const ssoStore = useSSOStore();
const cloudPlanStore = useCloudPlanStore(); const cloudPlanStore = useCloudPlanStore();
const currentUser = computed((): IUser | null => { const currentUser = computed((): IUser | null => {
return usersStore.currentUser; return usersStore.currentUser;
}); });
const isExternalAuthEnabled = computed((): boolean => { const isExternalAuthEnabled = computed((): boolean => {
const isLdapEnabled = const isLdapEnabled =
settingsStore.settings.enterprise.ldap && currentUser.value?.signInType === 'ldap'; ssoStore.isEnterpriseLdapEnabled && currentUser.value?.signInType === 'ldap';
const isSamlEnabled = const isSamlEnabled = ssoStore.isSamlLoginEnabled && ssoStore.isDefaultAuthenticationSaml;
settingsStore.isSamlLoginEnabled && settingsStore.isDefaultAuthenticationSaml;
const isOidcEnabled = const isOidcEnabled =
settingsStore.settings.enterprise.oidc && currentUser.value?.signInType === 'oidc'; ssoStore.isEnterpriseOidcEnabled && currentUser.value?.signInType === 'oidc';
return isLdapEnabled || isSamlEnabled || isOidcEnabled; return isLdapEnabled || isSamlEnabled || isOidcEnabled;
}); });
const isPersonalSecurityEnabled = computed((): boolean => { const isPersonalSecurityEnabled = computed((): boolean => {
return usersStore.isInstanceOwner || !isExternalAuthEnabled.value; return usersStore.isInstanceOwner || !isExternalAuthEnabled.value;
}); });
const mfaDisabled = computed((): boolean => { const mfaDisabled = computed((): boolean => {
return !usersStore.mfaEnabled; return !usersStore.mfaEnabled;
}); });
const isMfaFeatureEnabled = computed((): boolean => { const isMfaFeatureEnabled = computed((): boolean => {
return settingsStore.isMfaFeatureEnabled; return settingsStore.isMfaFeatureEnabled;
}); });
const hasAnyPersonalisationChanges = computed((): boolean => { const hasAnyPersonalisationChanges = computed((): boolean => {
return currentSelectedTheme.value !== uiStore.theme; return currentSelectedTheme.value !== uiStore.theme;
}); });
const hasAnyChanges = computed(() => { const hasAnyChanges = computed(() => {
return hasAnyBasicInfoChanges.value || hasAnyPersonalisationChanges.value; return hasAnyBasicInfoChanges.value || hasAnyPersonalisationChanges.value;
}); });

View File

@@ -9,7 +9,6 @@ import { useSettingsStore } from '@/stores/settings.store';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { useSSOStore } from '@/stores/sso.store'; import { useSSOStore } from '@/stores/sso.store';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { EnterpriseEditionFeature } from '@/constants';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso'; import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso';
@@ -328,7 +327,7 @@ describe('SettingsSso', () => {
}); });
it('should render licensed content', async () => { it('should render licensed content', async () => {
settingsStore.settings.enterprise[EnterpriseEditionFeature.Saml] = true; ssoStore.isEnterpriseSamlEnabled = true;
await nextTick(); await nextTick();
const { getByTestId, queryByTestId, getByRole } = renderComponent({ const { getByTestId, queryByTestId, getByRole } = renderComponent({
@@ -342,7 +341,7 @@ describe('SettingsSso', () => {
it('should enable activation checkbox and test button if data is already saved', async () => { it('should enable activation checkbox and test button if data is already saved', async () => {
await ssoStore.getSamlConfig(); await ssoStore.getSamlConfig();
settingsStore.settings.enterprise[EnterpriseEditionFeature.Saml] = true; ssoStore.isEnterpriseSamlEnabled = true;
await nextTick(); await nextTick();
const { container, getByTestId, getByRole } = renderComponent({ const { container, getByTestId, getByRole } = renderComponent({

View File

@@ -10,7 +10,6 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useRootStore } from '@n8n/stores/useRootStore'; import { useRootStore } from '@n8n/stores/useRootStore';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { MODAL_CONFIRM } from '@/constants'; import { MODAL_CONFIRM } from '@/constants';
import { useSettingsStore } from '@/stores/settings.store';
type SupportedProtocolType = (typeof SupportedProtocols)[keyof typeof SupportedProtocols]; type SupportedProtocolType = (typeof SupportedProtocols)[keyof typeof SupportedProtocols];
@@ -29,7 +28,6 @@ const telemetry = useTelemetry();
const rootStore = useRootStore(); const rootStore = useRootStore();
const ssoStore = useSSOStore(); const ssoStore = useSSOStore();
const message = useMessage(); const message = useMessage();
const settingsStore = useSettingsStore();
const toast = useToast(); const toast = useToast();
const documentTitle = useDocumentTitle(); const documentTitle = useDocumentTitle();
const pageRedirectionHelper = usePageRedirectionHelper(); const pageRedirectionHelper = usePageRedirectionHelper();
@@ -398,7 +396,7 @@ async function onOidcSettingsSave() {
<div :class="$style.group"> <div :class="$style.group">
<label>Redirect URL</label> <label>Redirect URL</label>
<CopyInput <CopyInput
:value="settingsStore.oidcCallBackUrl" :value="ssoStore.oidc.callbackUrl"
:copy-button-text="i18n.baseText('generic.clickToCopy')" :copy-button-text="i18n.baseText('generic.clickToCopy')"
toast-title="Redirect URL copied to clipboard" toast-title="Redirect URL copied to clipboard"
/> />

View File

@@ -91,8 +91,7 @@ describe('SettingsUsersView', () => {
it('hides invite button visibility based on user permissions', async () => { it('hides invite button visibility based on user permissions', async () => {
const pinia = createTestingPinia({ initialState: getInitialState() }); const pinia = createTestingPinia({ initialState: getInitialState() });
const userStore = useUsersStore(pinia); const userStore = mockedStore(useUsersStore);
// @ts-expect-error: mocked getter
userStore.currentUser = createUser({ isDefaultUser: true }); userStore.currentUser = createUser({ isDefaultUser: true });
const { queryByTestId } = renderView({ pinia }); const { queryByTestId } = renderView({ pinia });
@@ -103,8 +102,7 @@ describe('SettingsUsersView', () => {
describe('Below quota', () => { describe('Below quota', () => {
const pinia = createTestingPinia({ initialState: getInitialState() }); const pinia = createTestingPinia({ initialState: getInitialState() });
const settingsStore = useSettingsStore(pinia); const settingsStore = mockedStore(useSettingsStore);
// @ts-expect-error: mocked getter
settingsStore.isBelowUserQuota = false; settingsStore.isBelowUserQuota = false;
it('disables the invite button', async () => { it('disables the invite button', async () => {
@@ -180,8 +178,7 @@ describe('SettingsUsersView', () => {
const pinia = createTestingPinia({ initialState: getInitialState() }); const pinia = createTestingPinia({ initialState: getInitialState() });
const settingsStore = useSettingsStore(pinia); const settingsStore = mockedStore(useSettingsStore);
// @ts-expect-error: mocked getter
settingsStore.isSmtpSetup = true; settingsStore.isSmtpSetup = true;
const userStore = useUsersStore(); const userStore = useUsersStore();
@@ -236,9 +233,8 @@ describe('SettingsUsersView', () => {
const pinia = createTestingPinia({ initialState: getInitialState() }); const pinia = createTestingPinia({ initialState: getInitialState() });
const settingsStore = useSettingsStore(pinia); const ssoStore = useSSOStore(pinia);
// @ts-expect-error: mocked getter ssoStore.isSamlLoginEnabled = true;
settingsStore.isSamlLoginEnabled = true;
const userStore = useUsersStore(); const userStore = useUsersStore();
@@ -256,9 +252,8 @@ describe('SettingsUsersView', () => {
const pinia = createTestingPinia({ initialState: getInitialState() }); const pinia = createTestingPinia({ initialState: getInitialState() });
const settingsStore = useSettingsStore(pinia); const ssoStore = useSSOStore(pinia);
// @ts-expect-error: mocked getter ssoStore.isSamlLoginEnabled = true;
settingsStore.isSamlLoginEnabled = true;
const userStore = useUsersStore(); const userStore = useUsersStore();

View File

@@ -71,13 +71,12 @@ const usersListActions = computed((): IUserListAction[] => {
{ {
label: i18n.baseText('settings.users.actions.allowSSOManualLogin'), label: i18n.baseText('settings.users.actions.allowSSOManualLogin'),
value: 'allowSSOManualLogin', value: 'allowSSOManualLogin',
guard: (user) => settingsStore.isSamlLoginEnabled && !user.settings?.allowSSOManualLogin, guard: (user) => !!ssoStore.isSamlLoginEnabled && !user.settings?.allowSSOManualLogin,
}, },
{ {
label: i18n.baseText('settings.users.actions.disallowSSOManualLogin'), label: i18n.baseText('settings.users.actions.disallowSSOManualLogin'),
value: 'disallowSSOManualLogin', value: 'disallowSSOManualLogin',
guard: (user) => guard: (user) => !!ssoStore.isSamlLoginEnabled && user.settings?.allowSSOManualLogin === true,
settingsStore.isSamlLoginEnabled && user.settings?.allowSSOManualLogin === true,
}, },
]; ];
}); });

View File

@@ -12,6 +12,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useSSOStore } from '@/stores/sso.store';
import type { IFormBoxConfig } from '@/Interface'; import type { IFormBoxConfig } from '@/Interface';
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants'; import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants';
@@ -27,6 +28,7 @@ export type MfaCodeOrMfaRecoveryCode = Pick<LoginRequestDto, 'mfaCode' | 'mfaRec
const usersStore = useUsersStore(); const usersStore = useUsersStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const cloudPlanStore = useCloudPlanStore(); const cloudPlanStore = useCloudPlanStore();
const ssoStore = useSSOStore();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -41,8 +43,8 @@ const emailOrLdapLoginId = ref('');
const password = ref(''); const password = ref('');
const reportError = ref(false); const reportError = ref(false);
const ldapLoginLabel = computed(() => settingsStore.ldapLoginLabel); const ldapLoginLabel = computed(() => ssoStore.ldapLoginLabel);
const isLdapLoginEnabled = computed(() => settingsStore.isLdapLoginEnabled); const isLdapLoginEnabled = computed(() => ssoStore.isLdapLoginEnabled);
const emailLabel = computed(() => { const emailLabel = computed(() => {
let label = locale.baseText('auth.email'); let label = locale.baseText('auth.email');
if (isLdapLoginEnabled.value && ldapLoginLabel.value) { if (isLdapLoginEnabled.value && ldapLoginLabel.value) {