refactor(editor): Move settings.store and roles.store into @n8n/stores behind re-export shims (no-changelog) (#34574)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Alex Grozav
2026-07-21 13:46:54 +02:00
committed by GitHub
parent 6dc2079399
commit b16b2d8e84
16 changed files with 713 additions and 703 deletions
@@ -38,8 +38,10 @@
"dependencies": {
"@n8n/api-types": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n/rest-api-client": "workspace:*",
"n8n-workflow": "workspace:*",
"@vueuse/core": "catalog:frontend",
"bowser": "2.11.0",
"pinia": "catalog:frontend",
"vue": "catalog:frontend"
},
@@ -58,6 +60,7 @@
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"vue-tsc": "catalog:frontend"
},
"license": "LicenseRef-n8n-sustainable-use"
@@ -1,8 +1,9 @@
import { useRolesStore } from '@/app/stores/roles.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import * as rolesApi from '@n8n/rest-api-client/api/roles';
import { createPinia, setActivePinia } from 'pinia';
import { useRolesStore } from './roles.store';
import { useSettingsStore } from './settings.store';
let rolesStore: ReturnType<typeof useRolesStore>;
let settingsStore: ReturnType<typeof useSettingsStore>;
@@ -0,0 +1,143 @@
import type {
CreateRoleDto,
RoleAssignmentsResponse,
RoleMembersResponse,
RoleProjectMembersResponse,
UpdateRoleDto,
} from '@n8n/api-types';
import {
type AllRolesMap,
type Role,
GLOBAL_OWNER_ROLE_SLUG,
GLOBAL_CHAT_USER_ROLE_SLUG,
PROJECT_OWNER_ROLE_SLUG,
PROJECT_CHAT_USER_ROLE_SLUG,
} from '@n8n/permissions';
import * as rolesApi from '@n8n/rest-api-client/api/roles';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useSettingsStore } from './settings.store';
import { useRootStore } from './useRootStore';
function sortByOrderThenName(orderMap: Map<string, number>) {
return (a: { slug: string; displayName: string }, b: { slug: string; displayName: string }) => {
const orderA = orderMap.get(a.slug);
const orderB = orderMap.get(b.slug);
// Roles with an explicit order come first; the rest sort alphabetically by name.
if (orderA !== undefined || orderB !== undefined) {
return (orderA ?? Number.MAX_SAFE_INTEGER) - (orderB ?? Number.MAX_SAFE_INTEGER);
}
return a.displayName.localeCompare(b.displayName);
};
}
export const useRolesStore = defineStore('roles', () => {
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const roles = ref<AllRolesMap>({
global: [],
project: [],
credential: [],
workflow: [],
secretsProviderConnection: [],
});
const projectRoleOrder = ref<string[]>([
'project:viewer',
'project:chatUser',
'project:editor',
'project:admin',
]);
const projectRoleOrderMap = computed<Map<string, number>>(
() => new Map(projectRoleOrder.value.map((role, idx) => [role, idx])),
);
const globalRoleOrder = ref<string[]>(['global:admin', 'global:member']);
const globalRoleOrderMap = computed<Map<string, number>>(
() => new Map(globalRoleOrder.value.map((role, idx) => [role, idx])),
);
const processedInstanceRoles = computed<AllRolesMap['global']>(() =>
roles.value.global
.filter(
(role) =>
role.slug !== GLOBAL_OWNER_ROLE_SLUG &&
(settingsStore.isChatFeatureEnabled || role.slug !== GLOBAL_CHAT_USER_ROLE_SLUG),
)
.sort(sortByOrderThenName(globalRoleOrderMap.value)),
);
const customInstanceRoles = computed<AllRolesMap['global']>(() =>
processedInstanceRoles.value.filter((role) => !role.systemRole),
);
const processedProjectRoles = computed<AllRolesMap['project']>(() =>
roles.value.project
.filter(
(role) =>
role.slug !== PROJECT_OWNER_ROLE_SLUG &&
(settingsStore.isChatFeatureEnabled || role.slug !== PROJECT_CHAT_USER_ROLE_SLUG),
)
.sort(sortByOrderThenName(projectRoleOrderMap.value)),
);
const processedCredentialRoles = computed<AllRolesMap['credential']>(() =>
roles.value.credential.filter((role) => role.slug !== 'credential:owner'),
);
const processedWorkflowRoles = computed<AllRolesMap['workflow']>(() =>
roles.value.workflow.filter((role) => role.slug !== 'workflow:owner'),
);
const fetchRoles = async () => {
roles.value = await rolesApi.getRoles(rootStore.restApiContext);
};
const createRole = async (body: CreateRoleDto): Promise<Role> => {
return await rolesApi.createRole(rootStore.restApiContext, body);
};
const fetchRoleBySlug = async (payload: { slug: string }): Promise<Role> => {
return await rolesApi.getRoleBySlug(rootStore.restApiContext, payload);
};
const deleteRole = async (slug: string): Promise<Role> => {
return await rolesApi.deleteRole(rootStore.restApiContext, slug);
};
const updateRole = async (slug: string, body: UpdateRoleDto): Promise<Role> => {
return await rolesApi.updateRole(rootStore.restApiContext, slug, body);
};
const fetchRoleAssignments = async (slug: string): Promise<RoleAssignmentsResponse> => {
return await rolesApi.getRoleAssignments(rootStore.restApiContext, slug);
};
const fetchRoleProjectMembers = async (
slug: string,
projectId: string,
): Promise<RoleProjectMembersResponse> => {
return await rolesApi.getRoleProjectMembers(rootStore.restApiContext, slug, projectId);
};
const fetchRoleMembers = async (slug: string): Promise<RoleMembersResponse> => {
return await rolesApi.getRoleMembers(rootStore.restApiContext, slug);
};
return {
roles,
processedProjectRoles,
processedInstanceRoles,
customInstanceRoles,
processedCredentialRoles,
processedWorkflowRoles,
fetchRoles,
createRole,
fetchRoleBySlug,
updateRole,
deleteRole,
fetchRoleAssignments,
fetchRoleProjectMembers,
fetchRoleMembers,
};
});
@@ -1,6 +1,7 @@
import type { FrontendSettings } from '@n8n/api-types';
import { createPinia, setActivePinia } from 'pinia';
import { mock } from 'vitest-mock-extended';
import { useSettingsStore } from './settings.store';
const mockRootStore = {
@@ -45,16 +46,10 @@ vi.mock('@n8n/rest-api-client/api/events', () => ({
sessionStarted,
}));
vi.mock('@n8n/stores/useRootStore', () => ({
vi.mock('./useRootStore', () => ({
useRootStore,
}));
vi.mock('@/app/stores/versions.store', () => ({
useVersionsStore: vi.fn(() => ({
initialize: vi.fn(),
})),
}));
vi.mock('@vueuse/core', async () => {
const originalModule = await vi.importActual('@vueuse/core');
@@ -0,0 +1,515 @@
import {
AuthenticationMethod,
type IUserManagementSettings,
type FrontendSettings,
type FrontendModuleSettings,
} from '@n8n/api-types';
import { makeRestApiRequest } from '@n8n/rest-api-client';
import * as aiUsageApi from '@n8n/rest-api-client/api/ai-usage';
import * as eventsApi from '@n8n/rest-api-client/api/events';
import * as moduleSettingsApi from '@n8n/rest-api-client/api/module-settings';
import * as settingsApi from '@n8n/rest-api-client/api/settings';
import { testHealthEndpoint } from '@n8n/rest-api-client/api/templates';
import Bowser from 'bowser';
import type { IDataObject, WorkflowSettings } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { STORES } from './constants';
import { useRootStore } from './useRootStore';
/**
* Full-page warning rendered when the instance requires a secure cookie but the
* page is served over an insecure origin (or via Safari, which drops the cookie).
*/
const INSECURE_CONNECTION_WARNING = `
<body style="margin-top: 20px; font-family: 'Open Sans', sans-serif; text-align: center;">
<h1 style="font-size: 40px">&#x1F6AB;</h1>
<h2>Your n8n server is configured to use a secure cookie, <br/>however you are either visiting this via an insecure URL, or using Safari.
</h2>
<br/>
<div style="font-size: 18px; max-width: 640px; text-align: left; margin: 10px auto">
To fix this, please consider the following options:
<ul>
<li>Setup TLS/HTTPS (<strong>recommended</strong>), or</li>
<li>If you are running this locally, and not using Safari, try using <a href="http://localhost:5678">localhost</a> instead</li>
<li>If you prefer to disable this security feature (<strong>not recommended</strong>), set the environment variable <code>N8N_SECURE_COOKIE</code> to <code>false</code></li>
</ul>
</div>
</body>`;
export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const initialized = ref(false);
const settings = ref<FrontendSettings>({} as FrontendSettings);
const moduleSettings = ref<FrontendModuleSettings>({});
const userManagement = ref<IUserManagementSettings>({
quota: -1,
showSetupOnFirstLoad: false,
smtpSetup: false,
authenticationMethod: AuthenticationMethod.Email,
passwordMinLength: 8,
});
const templatesEndpointHealthy = ref(false);
const api = ref({
enabled: false,
latestVersion: 0,
path: '/',
swaggerUi: {
enabled: false,
},
});
const mfa = ref({ enabled: false });
const folders = ref({ enabled: false });
const saveDataErrorExecution = ref<WorkflowSettings.SaveDataExecution>('all');
const saveDataSuccessExecution = ref<WorkflowSettings.SaveDataExecution>('all');
const saveManualExecutions = ref(false);
const saveDataProgressExecution = ref(false);
const isMFAEnforced = ref(false);
const isDocker = computed(() => settings.value?.isDocker ?? false);
const databaseType = computed(() => settings.value?.databaseType);
const planName = computed(() => settings.value?.license.planName ?? 'Community');
const consumerId = computed(() => settings.value?.license.consumerId);
const binaryDataMode = computed(() => settings.value?.binaryDataMode);
const pruning = computed(() => settings.value?.pruning);
const security = computed(() => ({
blockFileAccessToN8nFiles: settings.value.security.blockFileAccessToN8nFiles,
secureCookie: settings.value.authCookie.secure,
}));
const isEnterpriseFeatureEnabled = computed(() => settings.value.enterprise ?? {});
const nodeJsVersion = computed(() => settings.value.nodeJsVersion);
const nodeEnv = computed(() => settings.value.nodeEnv);
const concurrency = computed(() => settings.value.concurrency);
const isConcurrencyEnabled = computed(() => concurrency.value !== -1);
const isPublicApiEnabled = computed(() => api.value.enabled);
const isSwaggerUIEnabled = computed(() => api.value.swaggerUi.enabled);
const isPreviewMode = computed(() => settings.value.previewMode);
const isCanvasOnly = computed(() => settings.value.canvasOnly);
const isCrdtCollaborationEnabled = computed(
() => (settings.value.collaboration?.crdt ?? 'off') !== 'off',
);
const publicApiLatestVersion = computed(() => api.value.latestVersion);
const publicApiPath = computed(() => api.value.path);
const isAiAssistantEnabled = computed(
() => settings.value.aiAssistant?.enabled && settings.value.aiAssistant?.setup,
);
const isAskAiEnabled = computed(() => settings.value.askAi?.enabled);
const isAiBuilderEnabled = computed(
() => settings.value.aiBuilder?.enabled && settings.value.aiBuilder?.setup,
);
const isAiAssistantOrBuilderEnabled = computed(
() => isAiAssistantEnabled.value || isAiBuilderEnabled.value,
);
const showSetupPage = computed(() => userManagement.value.showSetupOnFirstLoad);
const deploymentType = computed(() => settings.value.deployment?.type || 'default');
const isCloudDeployment = computed(() => settings.value.deployment?.type === 'cloud');
const activeModules = computed(() => settings.value.activeModules);
const isModuleActive = (moduleName: string) => {
return activeModules.value?.includes(moduleName);
};
/**
* Checks whether an agents-module sub-feature token (listed in
* `N8N_AGENTS_MODULES` on the backend) is enabled. Returns `false`
* unless the top-level `agents` module is active AND the token is
* present in the module settings' `modules` array.
*
* Known tokens: see `AGENTS_MODULE_NAMES` in `agents.config.ts`.
*/
const isAgentModuleActive = (name: string): boolean => {
return (
isModuleActive('agents') && moduleSettings.value.agents?.modules?.includes(name) === true
);
};
const isAiCreditsEnabled = computed(
() => settings.value.aiCredits?.enabled && settings.value.aiCredits?.setup,
);
const aiCreditsQuota = computed(() => settings.value.aiCredits?.credits);
const isAiDataSharingEnabled = computed(
() => settings.value.ai?.allowSendingParameterValues ?? true,
);
const isAiGatewayEnabled = computed(() => settings.value.aiGateway?.enabled ?? false);
const aiGatewayBudget = computed(() => settings.value.aiGateway?.budget ?? 0);
const isSmtpSetup = computed(() => userManagement.value.smtpSetup);
const isPersonalizationSurveyEnabled = computed(
() => settings.value.telemetry?.enabled && settings.value.personalizationSurveyEnabled,
);
const telemetry = computed(() => settings.value.telemetry);
const logLevel = computed(() => settings.value.logLevel);
const isTelemetryEnabled = computed(() => settings.value.telemetry?.enabled);
const isMFAEnforcementLicensed = computed(() => {
return settings.value.enterprise?.mfaEnforcement ?? false;
});
const isMfaFeatureEnabled = computed(() => mfa.value.enabled);
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
const isDataTableFeatureEnabled = computed(() => isModuleActive('data-table'));
const isChatFeatureEnabled = computed(
() => isModuleActive('chat-hub') && moduleSettings.value['chat-hub']?.enabled === true,
);
const isOtelCustomSpanAttributesEnabled = computed(() => {
const isOtelCustomSpanAttributesLicensed =
settings.value.enterprise?.otelCustomSpanAttributes ?? false;
const isOtelModuleActive =
isModuleActive('otel') && moduleSettings.value.otel?.enabled === true;
return isOtelCustomSpanAttributesLicensed && isOtelModuleActive;
});
// Opt-in flag: enabled when the backend's Daytona sandbox env vars
// (`N8N_AGENTS_AI_SANDBOX_ENABLED=true` + `N8N_AGENTS_AI_SANDBOX_PROVIDER=daytona`)
// are set, OR the AI Assistant proxy is available.
const isAgentsKnowledgeBaseFeatureEnabled = computed(
() => isModuleActive('agents') && moduleSettings.value.agents?.knowledgeBaseEnabled === true,
);
const isPublicChatTriggerDisabled = computed(
() => settings.value.chatTrigger?.disablePublicChat ?? false,
);
const isCustomRolesFeatureEnabled = computed(
() => settings.value.enterprise?.customRoles ?? false,
);
const areTagsEnabled = computed(() =>
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
);
const isAutosaveEnabled = computed(() =>
settings.value.workflowsAutosaveDisabled !== undefined
? !settings.value.workflowsAutosaveDisabled
: true,
);
const isHiringBannerEnabled = computed(() => settings.value.hiringBannerEnabled);
const isTemplatesEnabled = computed(() => Boolean(settings.value.templates?.enabled));
const isTemplatesEndpointReachable = computed(() => templatesEndpointHealthy.value);
const templatesHost = computed(() => settings.value.templates?.host ?? '');
const pushBackend = computed(() => settings.value.pushBackend);
const isCommunityNodesFeatureEnabled = computed(() => settings.value.communityNodesEnabled);
const isUnverifiedPackagesEnabled = computed(
() => settings.value.unverifiedCommunityNodesEnabled,
);
const allowedModules = computed(() => settings.value.allowedModules);
const isQueueModeEnabled = computed(() => settings.value.executionMode === 'queue');
const isMultiMain = computed(() => settings.value.isMultiMain);
const isWorkerViewAvailable = computed(() => !!settings.value.enterprise?.workerView);
const workflowCallerPolicyDefaultOption = computed(
() => settings.value.workflowCallerPolicyDefaultOption,
);
const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []);
const isCommunityPlan = computed(() => planName.value.toLowerCase() === 'community');
const isDevRelease = computed(() => settings.value.releaseChannel === 'dev');
const endpointHealth = computed(() => settings.value.endpointHealth);
const isWorkflowPublicationServiceEnabled = computed(
() => settings.value.useWorkflowPublicationService ?? false,
);
const setSettings = (newSettings: FrontendSettings) => {
settings.value = newSettings;
userManagement.value = newSettings.userManagement;
if (userManagement.value) {
userManagement.value.showSetupOnFirstLoad =
!!settings.value.userManagement.showSetupOnFirstLoad;
}
if (settings.value.publicApi) {
api.value = settings.value.publicApi;
}
mfa.value.enabled = settings.value.mfa?.enabled;
folders.value.enabled = settings.value.folders?.enabled;
if (settings.value.versionCli) {
useRootStore().setVersionCli(settings.value.versionCli);
}
if (settings.value.authCookie.secure) {
const { browser } = Bowser.parse(navigator.userAgent);
if (
location.protocol === 'http:' &&
(!['localhost', '127.0.0.1'].includes(location.hostname) || browser.name === 'Safari')
) {
document.write(INSECURE_CONNECTION_WARNING);
return;
}
}
};
const setAllowedModules = (allowedModules: FrontendSettings['allowedModules']) => {
settings.value.allowedModules = allowedModules;
};
const setSaveDataErrorExecution = (newValue: WorkflowSettings.SaveDataExecution) => {
saveDataErrorExecution.value = newValue;
};
const setSaveDataSuccessExecution = (newValue: WorkflowSettings.SaveDataExecution) => {
saveDataSuccessExecution.value = newValue;
};
const setSaveManualExecutions = (newValue: boolean) => {
saveManualExecutions.value = newValue;
};
const setSaveDataProgressExecution = (newValue: boolean) => {
saveDataProgressExecution.value = newValue;
};
const getSettings = async () => {
const rootStore = useRootStore();
const fetchedSettings = await settingsApi.getSettings(rootStore.restApiContext);
setSettings(fetchedSettings);
rootStore.setDefaultLocale(fetchedSettings.defaultLocale);
// Set MFA enforced state even for public settings mode
// as it is needed to determine if the MFA setup page should be shown
isMFAEnforced.value = settings.value.mfa?.enforced ?? false;
if (fetchedSettings.settingsMode === 'public') {
// public settings mode is typically used for unauthenticated users
// when public settings are returned we can skip the rest of the setup
// that need the full set of authenticated settings
return;
}
settings.value.communityNodesEnabled = fetchedSettings.communityNodesEnabled;
settings.value.unverifiedCommunityNodesEnabled =
fetchedSettings.unverifiedCommunityNodesEnabled;
setAllowedModules(fetchedSettings.allowedModules);
setSaveDataErrorExecution(fetchedSettings.saveDataErrorExecution);
setSaveDataSuccessExecution(fetchedSettings.saveDataSuccessExecution);
setSaveDataProgressExecution(fetchedSettings.saveExecutionProgress);
setSaveManualExecutions(fetchedSettings.saveManualExecutions);
rootStore.setUrlBaseWebhook(fetchedSettings.urlBaseWebhook);
rootStore.setUrlBaseEditor(fetchedSettings.urlBaseEditor);
rootStore.setUrlBaseWebhookTest(fetchedSettings.urlBaseWebhookTest);
rootStore.setEndpointForm(fetchedSettings.endpointForm);
rootStore.setEndpointFormTest(fetchedSettings.endpointFormTest);
rootStore.setEndpointFormWaiting(fetchedSettings.endpointFormWaiting);
rootStore.setEndpointWebhook(fetchedSettings.endpointWebhook);
rootStore.setEndpointWebhookTest(fetchedSettings.endpointWebhookTest);
rootStore.setEndpointWebhookWaiting(fetchedSettings.endpointWebhookWaiting);
rootStore.setEndpointMcp(fetchedSettings.endpointMcp);
rootStore.setEndpointMcpTest(fetchedSettings.endpointMcpTest);
rootStore.setTimezone(fetchedSettings.timezone);
rootStore.setExecutionTimeout(fetchedSettings.executionTimeout);
rootStore.setMaxExecutionTimeout(fetchedSettings.maxExecutionTimeout);
rootStore.setInstanceId(fetchedSettings.instanceId);
rootStore.setOauthCallbackUrls(fetchedSettings.oauthCallbackUrls);
rootStore.setN8nMetadata(fetchedSettings.n8nMetadata ?? {});
rootStore.setBinaryDataMode(fetchedSettings.binaryDataMode);
if (fetchedSettings.telemetry.enabled) {
void eventsApi.sessionStarted(rootStore.restApiContext);
}
};
const initialize = async () => {
if (initialized.value) {
return;
}
await getSettings();
initialized.value = true;
};
const stopShowingSetupPage = () => {
userManagement.value.showSetupOnFirstLoad = false;
};
const disableTemplates = () => {
settings.value = {
...settings.value,
templates: {
...settings.value.templates,
enabled: false,
},
};
};
const testTemplatesEndpoint = async () => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Templates health check timed out')), 2000),
);
await Promise.race([testHealthEndpoint(templatesHost.value), timeout]);
templatesEndpointHealthy.value = true;
};
const getTimezones = async (): Promise<IDataObject> => {
const rootStore = useRootStore();
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones');
};
const reset = () => {
settings.value = {} as FrontendSettings;
};
const getModuleSettings = async () => {
const fetched = await moduleSettingsApi.getModuleSettings(useRootStore().restApiContext);
moduleSettings.value = fetched;
};
const updateAiDataSharingSettings = async (allowSendingParameterValues: boolean) => {
const rootStore = useRootStore();
await aiUsageApi.updateAiUsageSettings(rootStore.restApiContext, {
allowSendingParameterValues,
});
if (settings.value.ai) {
settings.value.ai.allowSendingParameterValues = allowSendingParameterValues;
}
};
return {
settings,
userManagement,
templatesEndpointHealthy,
api,
mfa,
isDocker,
isDevRelease,
endpointHealth,
isEnterpriseFeatureEnabled,
databaseType,
planName,
consumerId,
binaryDataMode,
pruning,
security,
nodeJsVersion,
nodeEnv,
concurrency,
isConcurrencyEnabled,
isPublicApiEnabled,
isSwaggerUIEnabled,
isPreviewMode,
isCanvasOnly,
isCrdtCollaborationEnabled,
publicApiLatestVersion,
publicApiPath,
showSetupPage,
deploymentType,
isCloudDeployment,
isSmtpSetup,
isPersonalizationSurveyEnabled,
telemetry,
logLevel,
isTelemetryEnabled,
isMfaFeatureEnabled,
isFoldersFeatureEnabled,
isAiAssistantEnabled,
isCustomRolesFeatureEnabled,
areTagsEnabled,
isAutosaveEnabled,
isHiringBannerEnabled,
isTemplatesEnabled,
isTemplatesEndpointReachable,
templatesHost,
pushBackend,
isCommunityNodesFeatureEnabled,
isUnverifiedPackagesEnabled,
allowedModules,
isQueueModeEnabled,
isMultiMain,
isWorkerViewAvailable,
workflowCallerPolicyDefaultOption,
permanentlyDismissedBanners,
saveDataErrorExecution,
saveDataSuccessExecution,
saveManualExecutions,
saveDataProgressExecution,
isCommunityPlan,
isAskAiEnabled,
isAiBuilderEnabled,
isAiAssistantOrBuilderEnabled,
isAiCreditsEnabled,
aiCreditsQuota,
isAiDataSharingEnabled,
isAiGatewayEnabled,
aiGatewayBudget,
reset,
getTimezones,
testTemplatesEndpoint,
disableTemplates,
stopShowingSetupPage,
getSettings,
setSettings,
initialize,
getModuleSettings,
moduleSettings,
updateAiDataSharingSettings,
isMFAEnforcementLicensed,
isMFAEnforced,
activeModules,
isModuleActive,
isAgentModuleActive,
isDataTableFeatureEnabled,
isChatFeatureEnabled,
isOtelCustomSpanAttributesEnabled,
isAgentsKnowledgeBaseFeatureEnabled,
isPublicChatTriggerDisabled,
isWorkflowPublicationServiceEnabled,
};
});
@@ -391,13 +391,6 @@ export interface IShareWorkflowsPayload {
shareWithIds: string[];
}
export const enum UserManagementAuthenticationMethod {
Email = 'email',
Ldap = 'ldap',
Saml = 'saml',
Oidc = 'oidc',
}
export interface IPermissionGroup {
loginStatus?: ILogInStatus[];
role?: Role[];
@@ -1,7 +1,7 @@
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import type { ISettingsState } from '@/Interface';
import { UserManagementAuthenticationMethod } from '@/Interface';
import { AuthenticationMethod } from '@n8n/api-types';
import { defaultSettings } from './defaults';
import type { Mock } from 'vitest';
import type { Store, StoreDefinition } from 'pinia';
@@ -45,7 +45,7 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
userManagement: {
showSetupOnFirstLoad: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
authenticationMethod: AuthenticationMethod.Email,
quota: defaultSettings.userManagement.quota,
passwordMinLength: 8,
},
@@ -19,19 +19,3 @@ export const HIRING_BANNER = `
Love n8n? Help us build the future of automation! https://n8n.io/careers?utm_source=n8n_user&utm_medium=console_output
`;
export const INSECURE_CONNECTION_WARNING = `
<body style="margin-top: 20px; font-family: 'Open Sans', sans-serif; text-align: center;">
<h1 style="font-size: 40px">&#x1F6AB;</h1>
<h2>Your n8n server is configured to use a secure cookie, <br/>however you are either visiting this via an insecure URL, or using Safari.
</h2>
<br/>
<div style="font-size: 18px; max-width: 640px; text-align: left; margin: 10px auto">
To fix this, please consider the following options:
<ul>
<li>Setup TLS/HTTPS (<strong>recommended</strong>), or</li>
<li>If you are running this locally, and not using Safari, try using <a href="http://localhost:5678">localhost</a> instead</li>
<li>If you prefer to disable this security feature (<strong>not recommended</strong>), set the environment variable <code>N8N_SECURE_COOKIE</code> to <code>false</code></li>
</ul>
</div>
</body>`;
@@ -1,7 +1,7 @@
import { mockedStore, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import { EnterpriseEditionFeature } from '@/app/constants';
import { initializeAuthenticatedFeatures, initializeCore, state } from '@/app/init';
import { UserManagementAuthenticationMethod } from '@/Interface';
import { AuthenticationMethod } from '@n8n/api-types';
import { useCloudPlanStore } from '@/app/stores/cloudPlan.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useSettingsStore } from '@/app/stores/settings.store';
@@ -144,7 +144,7 @@ describe('Init', () => {
callbackUrl: 'http://localhost:5678/rest/sso/oidc/callback',
};
settingsStore.userManagement.authenticationMethod = UserManagementAuthenticationMethod.Oidc;
settingsStore.userManagement.authenticationMethod = AuthenticationMethod.Oidc;
settingsStore.settings.sso = { managedByEnv: false, saml, ldap, oidc };
settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Oidc] = true;
@@ -158,7 +158,7 @@ describe('Init', () => {
// once during initializeCore and once during the login hook
expect(ssoStore.initialize).toHaveBeenCalledTimes(2);
expect(ssoStore.initialize).toHaveBeenLastCalledWith({
authenticationMethod: UserManagementAuthenticationMethod.Oidc,
authenticationMethod: AuthenticationMethod.Oidc,
managedByEnv: false,
config: { managedByEnv: false, saml, ldap, oidc },
features: {
@@ -174,14 +174,14 @@ describe('Init', () => {
const ldap = { loginEnabled: false, loginLabel: '' };
const oidc = { loginEnabled: false, loginUrl: '', callbackUrl: '' };
settingsStore.userManagement.authenticationMethod = UserManagementAuthenticationMethod.Saml;
settingsStore.userManagement.authenticationMethod = AuthenticationMethod.Saml;
settingsStore.settings.sso = { managedByEnv: false, saml, ldap, oidc };
settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Saml] = true;
await initializeCore();
expect(ssoStore.initialize).toHaveBeenCalledWith({
authenticationMethod: UserManagementAuthenticationMethod.Saml,
authenticationMethod: AuthenticationMethod.Saml,
managedByEnv: false,
config: { managedByEnv: false, saml, ldap, oidc },
features: {
+3 -4
View File
@@ -6,7 +6,7 @@ import { useToast } from '@/app/composables/useToast';
import { isDataWorkerEnabled } from '@/app/workers/isDataWorkerEnabled';
import { EnterpriseEditionFeature, VIEWS } from '@/app/constants';
import type { UserManagementAuthenticationMethod } from '@/Interface';
import type { AuthenticationMethod } from '@n8n/api-types';
import {
registerModuleModals,
registerModuleProjectTabs,
@@ -73,8 +73,7 @@ export async function initializeCore() {
}
ssoStore.initialize({
authenticationMethod: settingsStore.userManagement
.authenticationMethod as UserManagementAuthenticationMethod,
authenticationMethod: settingsStore.userManagement.authenticationMethod as AuthenticationMethod,
managedByEnv: settingsStore.settings.sso.managedByEnv,
config: settingsStore.settings.sso,
features: {
@@ -248,7 +247,7 @@ function registerAuthenticationHooks() {
// Without this, navigating to SSO settings after login shows an empty redirect URL.
ssoStore.initialize({
authenticationMethod: settingsStore.userManagement
.authenticationMethod as UserManagementAuthenticationMethod,
.authenticationMethod as AuthenticationMethod,
managedByEnv: settingsStore.settings.sso.managedByEnv,
config: settingsStore.settings.sso,
features: {
@@ -1,142 +1,6 @@
import {
type AllRolesMap,
type Role,
GLOBAL_OWNER_ROLE_SLUG,
GLOBAL_CHAT_USER_ROLE_SLUG,
PROJECT_OWNER_ROLE_SLUG,
PROJECT_CHAT_USER_ROLE_SLUG,
} from '@n8n/permissions';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import * as rolesApi from '@n8n/rest-api-client/api/roles';
import { useRootStore } from '@n8n/stores/useRootStore';
import type {
CreateRoleDto,
RoleAssignmentsResponse,
RoleMembersResponse,
RoleProjectMembersResponse,
UpdateRoleDto,
} from '@n8n/api-types';
import { useSettingsStore } from './settings.store';
function sortByOrderThenName(orderMap: Map<string, number>) {
return (a: { slug: string; displayName: string }, b: { slug: string; displayName: string }) => {
const orderA = orderMap.get(a.slug);
const orderB = orderMap.get(b.slug);
// Roles with an explicit order come first; the rest sort alphabetically by name.
if (orderA !== undefined || orderB !== undefined) {
return (orderA ?? Number.MAX_SAFE_INTEGER) - (orderB ?? Number.MAX_SAFE_INTEGER);
}
return a.displayName.localeCompare(b.displayName);
};
}
export const useRolesStore = defineStore('roles', () => {
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const roles = ref<AllRolesMap>({
global: [],
project: [],
credential: [],
workflow: [],
secretsProviderConnection: [],
});
const projectRoleOrder = ref<string[]>([
'project:viewer',
'project:chatUser',
'project:editor',
'project:admin',
]);
const projectRoleOrderMap = computed<Map<string, number>>(
() => new Map(projectRoleOrder.value.map((role, idx) => [role, idx])),
);
const globalRoleOrder = ref<string[]>(['global:admin', 'global:member']);
const globalRoleOrderMap = computed<Map<string, number>>(
() => new Map(globalRoleOrder.value.map((role, idx) => [role, idx])),
);
const processedInstanceRoles = computed<AllRolesMap['global']>(() =>
roles.value.global
.filter(
(role) =>
role.slug !== GLOBAL_OWNER_ROLE_SLUG &&
(settingsStore.isChatFeatureEnabled || role.slug !== GLOBAL_CHAT_USER_ROLE_SLUG),
)
.sort(sortByOrderThenName(globalRoleOrderMap.value)),
);
const customInstanceRoles = computed<AllRolesMap['global']>(() =>
processedInstanceRoles.value.filter((role) => !role.systemRole),
);
const processedProjectRoles = computed<AllRolesMap['project']>(() =>
roles.value.project
.filter(
(role) =>
role.slug !== PROJECT_OWNER_ROLE_SLUG &&
(settingsStore.isChatFeatureEnabled || role.slug !== PROJECT_CHAT_USER_ROLE_SLUG),
)
.sort(sortByOrderThenName(projectRoleOrderMap.value)),
);
const processedCredentialRoles = computed<AllRolesMap['credential']>(() =>
roles.value.credential.filter((role) => role.slug !== 'credential:owner'),
);
const processedWorkflowRoles = computed<AllRolesMap['workflow']>(() =>
roles.value.workflow.filter((role) => role.slug !== 'workflow:owner'),
);
const fetchRoles = async () => {
roles.value = await rolesApi.getRoles(rootStore.restApiContext);
};
const createRole = async (body: CreateRoleDto): Promise<Role> => {
return await rolesApi.createRole(rootStore.restApiContext, body);
};
const fetchRoleBySlug = async (payload: { slug: string }): Promise<Role> => {
return await rolesApi.getRoleBySlug(rootStore.restApiContext, payload);
};
const deleteRole = async (slug: string): Promise<Role> => {
return await rolesApi.deleteRole(rootStore.restApiContext, slug);
};
const updateRole = async (slug: string, body: UpdateRoleDto): Promise<Role> => {
return await rolesApi.updateRole(rootStore.restApiContext, slug, body);
};
const fetchRoleAssignments = async (slug: string): Promise<RoleAssignmentsResponse> => {
return await rolesApi.getRoleAssignments(rootStore.restApiContext, slug);
};
const fetchRoleProjectMembers = async (
slug: string,
projectId: string,
): Promise<RoleProjectMembersResponse> => {
return await rolesApi.getRoleProjectMembers(rootStore.restApiContext, slug, projectId);
};
const fetchRoleMembers = async (slug: string): Promise<RoleMembersResponse> => {
return await rolesApi.getRoleMembers(rootStore.restApiContext, slug);
};
return {
roles,
processedProjectRoles,
processedInstanceRoles,
customInstanceRoles,
processedCredentialRoles,
processedWorkflowRoles,
fetchRoles,
createRole,
fetchRoleBySlug,
updateRole,
deleteRole,
fetchRoleAssignments,
fetchRoleProjectMembers,
fetchRoleMembers,
};
});
/**
* @deprecated Import from `@n8n/stores/roles.store` instead. This store moved to
* `@n8n/stores` (CAT-3686 kernel slice); this re-export is a temporary shim kept
* so existing importers keep working and will be removed once call sites migrate.
*/
export * from '@n8n/stores/roles.store';
@@ -1,499 +1,6 @@
import { computed, ref } from 'vue';
import Bowser from 'bowser';
import type {
IUserManagementSettings,
FrontendSettings,
FrontendModuleSettings,
} from '@n8n/api-types';
import * as eventsApi from '@n8n/rest-api-client/api/events';
import * as settingsApi from '@n8n/rest-api-client/api/settings';
import * as moduleSettingsApi from '@n8n/rest-api-client/api/module-settings';
import * as aiUsageApi from '@n8n/rest-api-client/api/ai-usage';
import { testHealthEndpoint } from '@n8n/rest-api-client/api/templates';
import { INSECURE_CONNECTION_WARNING } from '@/app/constants';
import { STORES } from '@n8n/stores';
import { UserManagementAuthenticationMethod } from '@/Interface';
import type { IDataObject, WorkflowSettings } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { useRootStore } from '@n8n/stores/useRootStore';
import { makeRestApiRequest } from '@n8n/rest-api-client';
export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const initialized = ref(false);
const settings = ref<FrontendSettings>({} as FrontendSettings);
const moduleSettings = ref<FrontendModuleSettings>({});
const userManagement = ref<IUserManagementSettings>({
quota: -1,
showSetupOnFirstLoad: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
passwordMinLength: 8,
});
const templatesEndpointHealthy = ref(false);
const api = ref({
enabled: false,
latestVersion: 0,
path: '/',
swaggerUi: {
enabled: false,
},
});
const mfa = ref({ enabled: false });
const folders = ref({ enabled: false });
const saveDataErrorExecution = ref<WorkflowSettings.SaveDataExecution>('all');
const saveDataSuccessExecution = ref<WorkflowSettings.SaveDataExecution>('all');
const saveManualExecutions = ref(false);
const saveDataProgressExecution = ref(false);
const isMFAEnforced = ref(false);
const isDocker = computed(() => settings.value?.isDocker ?? false);
const databaseType = computed(() => settings.value?.databaseType);
const planName = computed(() => settings.value?.license.planName ?? 'Community');
const consumerId = computed(() => settings.value?.license.consumerId);
const binaryDataMode = computed(() => settings.value?.binaryDataMode);
const pruning = computed(() => settings.value?.pruning);
const security = computed(() => ({
blockFileAccessToN8nFiles: settings.value.security.blockFileAccessToN8nFiles,
secureCookie: settings.value.authCookie.secure,
}));
const isEnterpriseFeatureEnabled = computed(() => settings.value.enterprise ?? {});
const nodeJsVersion = computed(() => settings.value.nodeJsVersion);
const nodeEnv = computed(() => settings.value.nodeEnv);
const concurrency = computed(() => settings.value.concurrency);
const isConcurrencyEnabled = computed(() => concurrency.value !== -1);
const isPublicApiEnabled = computed(() => api.value.enabled);
const isSwaggerUIEnabled = computed(() => api.value.swaggerUi.enabled);
const isPreviewMode = computed(() => settings.value.previewMode);
const isCanvasOnly = computed(() => settings.value.canvasOnly);
const isCrdtCollaborationEnabled = computed(
() => (settings.value.collaboration?.crdt ?? 'off') !== 'off',
);
const publicApiLatestVersion = computed(() => api.value.latestVersion);
const publicApiPath = computed(() => api.value.path);
const isAiAssistantEnabled = computed(
() => settings.value.aiAssistant?.enabled && settings.value.aiAssistant?.setup,
);
const isAskAiEnabled = computed(() => settings.value.askAi?.enabled);
const isAiBuilderEnabled = computed(
() => settings.value.aiBuilder?.enabled && settings.value.aiBuilder?.setup,
);
const isAiAssistantOrBuilderEnabled = computed(
() => isAiAssistantEnabled.value || isAiBuilderEnabled.value,
);
const showSetupPage = computed(() => userManagement.value.showSetupOnFirstLoad);
const deploymentType = computed(() => settings.value.deployment?.type || 'default');
const isCloudDeployment = computed(() => settings.value.deployment?.type === 'cloud');
const activeModules = computed(() => settings.value.activeModules);
const isModuleActive = (moduleName: string) => {
return activeModules.value?.includes(moduleName);
};
/**
* Checks whether an agents-module sub-feature token (listed in
* `N8N_AGENTS_MODULES` on the backend) is enabled. Returns `false`
* unless the top-level `agents` module is active AND the token is
* present in the module settings' `modules` array.
*
* Known tokens: see `AGENTS_MODULE_NAMES` in `agents.config.ts`.
*/
const isAgentModuleActive = (name: string): boolean => {
return (
isModuleActive('agents') === true &&
moduleSettings.value.agents?.modules?.includes(name) === true
);
};
const isAiCreditsEnabled = computed(
() => settings.value.aiCredits?.enabled && settings.value.aiCredits?.setup,
);
const aiCreditsQuota = computed(() => settings.value.aiCredits?.credits);
const isAiDataSharingEnabled = computed(
() => settings.value.ai?.allowSendingParameterValues ?? true,
);
const isAiGatewayEnabled = computed(() => settings.value.aiGateway?.enabled ?? false);
const aiGatewayBudget = computed(() => settings.value.aiGateway?.budget ?? 0);
const isSmtpSetup = computed(() => userManagement.value.smtpSetup);
const isPersonalizationSurveyEnabled = computed(
() => settings.value.telemetry?.enabled && settings.value.personalizationSurveyEnabled,
);
const telemetry = computed(() => settings.value.telemetry);
const logLevel = computed(() => settings.value.logLevel);
const isTelemetryEnabled = computed(
() => settings.value.telemetry && settings.value.telemetry.enabled,
);
const isMFAEnforcementLicensed = computed(() => {
return settings.value.enterprise?.mfaEnforcement ?? false;
});
const isMfaFeatureEnabled = computed(() => mfa.value.enabled);
const isFoldersFeatureEnabled = computed(() => folders.value.enabled);
const isDataTableFeatureEnabled = computed(() => isModuleActive('data-table'));
const isChatFeatureEnabled = computed(
() => isModuleActive('chat-hub') && moduleSettings.value['chat-hub']?.enabled === true,
);
const isOtelCustomSpanAttributesEnabled = computed(() => {
const isOtelCustomSpanAttributesLicensed =
settings.value.enterprise?.otelCustomSpanAttributes === true;
const isOtelModuleActive =
isModuleActive('otel') === true && moduleSettings.value.otel?.enabled === true;
return isOtelCustomSpanAttributesLicensed && isOtelModuleActive;
});
// Opt-in flag: enabled when the backend's Daytona sandbox env vars
// (`N8N_AGENTS_AI_SANDBOX_ENABLED=true` + `N8N_AGENTS_AI_SANDBOX_PROVIDER=daytona`)
// are set, OR the AI Assistant proxy is available.
const isAgentsKnowledgeBaseFeatureEnabled = computed(
() =>
isModuleActive('agents') === true &&
moduleSettings.value.agents?.knowledgeBaseEnabled === true,
);
const isPublicChatTriggerDisabled = computed(
() => settings.value.chatTrigger?.disablePublicChat ?? false,
);
const isCustomRolesFeatureEnabled = computed(
() => settings.value.enterprise?.customRoles ?? false,
);
const areTagsEnabled = computed(() =>
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
);
const isAutosaveEnabled = computed(() =>
settings.value.workflowsAutosaveDisabled !== undefined
? !settings.value.workflowsAutosaveDisabled
: true,
);
const isHiringBannerEnabled = computed(() => settings.value.hiringBannerEnabled);
const isTemplatesEnabled = computed(() => Boolean(settings.value.templates?.enabled));
const isTemplatesEndpointReachable = computed(() => templatesEndpointHealthy.value);
const templatesHost = computed(() => settings.value.templates?.host ?? '');
const pushBackend = computed(() => settings.value.pushBackend);
const isCommunityNodesFeatureEnabled = computed(() => settings.value.communityNodesEnabled);
const isUnverifiedPackagesEnabled = computed(
() => settings.value.unverifiedCommunityNodesEnabled,
);
const allowedModules = computed(() => settings.value.allowedModules);
const isQueueModeEnabled = computed(() => settings.value.executionMode === 'queue');
const isMultiMain = computed(() => settings.value.isMultiMain);
const isWorkerViewAvailable = computed(() => !!settings.value.enterprise?.workerView);
const workflowCallerPolicyDefaultOption = computed(
() => settings.value.workflowCallerPolicyDefaultOption,
);
const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []);
const isCommunityPlan = computed(() => planName.value.toLowerCase() === 'community');
const isDevRelease = computed(() => settings.value.releaseChannel === 'dev');
const endpointHealth = computed(() => settings.value.endpointHealth);
const isWorkflowPublicationServiceEnabled = computed(
() => settings.value.useWorkflowPublicationService ?? false,
);
const setSettings = (newSettings: FrontendSettings) => {
settings.value = newSettings;
userManagement.value = newSettings.userManagement;
if (userManagement.value) {
userManagement.value.showSetupOnFirstLoad =
!!settings.value.userManagement.showSetupOnFirstLoad;
}
if (settings.value.publicApi) {
api.value = settings.value.publicApi;
}
mfa.value.enabled = settings.value.mfa?.enabled;
folders.value.enabled = settings.value.folders?.enabled;
if (settings.value.versionCli) {
useRootStore().setVersionCli(settings.value.versionCli);
}
if (settings.value.authCookie.secure) {
const { browser } = Bowser.parse(navigator.userAgent);
if (
location.protocol === 'http:' &&
(!['localhost', '127.0.0.1'].includes(location.hostname) || browser.name === 'Safari')
) {
document.write(INSECURE_CONNECTION_WARNING);
return;
}
}
};
const setAllowedModules = (allowedModules: FrontendSettings['allowedModules']) => {
settings.value.allowedModules = allowedModules;
};
const setSaveDataErrorExecution = (newValue: WorkflowSettings.SaveDataExecution) => {
saveDataErrorExecution.value = newValue;
};
const setSaveDataSuccessExecution = (newValue: WorkflowSettings.SaveDataExecution) => {
saveDataSuccessExecution.value = newValue;
};
const setSaveManualExecutions = (newValue: boolean) => {
saveManualExecutions.value = newValue;
};
const setSaveDataProgressExecution = (newValue: boolean) => {
saveDataProgressExecution.value = newValue;
};
const getSettings = async () => {
const rootStore = useRootStore();
const fetchedSettings = await settingsApi.getSettings(rootStore.restApiContext);
setSettings(fetchedSettings);
rootStore.setDefaultLocale(fetchedSettings.defaultLocale);
// Set MFA enforced state even for public settings mode
// as it is needed to determine if the MFA setup page should be shown
isMFAEnforced.value = settings.value.mfa?.enforced ?? false;
if (fetchedSettings.settingsMode === 'public') {
// public settings mode is typically used for unauthenticated users
// when public settings are returned we can skip the rest of the setup
// that need the full set of authenticated settings
return;
}
settings.value.communityNodesEnabled = fetchedSettings.communityNodesEnabled;
settings.value.unverifiedCommunityNodesEnabled =
fetchedSettings.unverifiedCommunityNodesEnabled;
setAllowedModules(fetchedSettings.allowedModules);
setSaveDataErrorExecution(fetchedSettings.saveDataErrorExecution);
setSaveDataSuccessExecution(fetchedSettings.saveDataSuccessExecution);
setSaveDataProgressExecution(fetchedSettings.saveExecutionProgress);
setSaveManualExecutions(fetchedSettings.saveManualExecutions);
rootStore.setUrlBaseWebhook(fetchedSettings.urlBaseWebhook);
rootStore.setUrlBaseEditor(fetchedSettings.urlBaseEditor);
rootStore.setUrlBaseWebhookTest(fetchedSettings.urlBaseWebhookTest);
rootStore.setEndpointForm(fetchedSettings.endpointForm);
rootStore.setEndpointFormTest(fetchedSettings.endpointFormTest);
rootStore.setEndpointFormWaiting(fetchedSettings.endpointFormWaiting);
rootStore.setEndpointWebhook(fetchedSettings.endpointWebhook);
rootStore.setEndpointWebhookTest(fetchedSettings.endpointWebhookTest);
rootStore.setEndpointWebhookWaiting(fetchedSettings.endpointWebhookWaiting);
rootStore.setEndpointMcp(fetchedSettings.endpointMcp);
rootStore.setEndpointMcpTest(fetchedSettings.endpointMcpTest);
rootStore.setTimezone(fetchedSettings.timezone);
rootStore.setExecutionTimeout(fetchedSettings.executionTimeout);
rootStore.setMaxExecutionTimeout(fetchedSettings.maxExecutionTimeout);
rootStore.setInstanceId(fetchedSettings.instanceId);
rootStore.setOauthCallbackUrls(fetchedSettings.oauthCallbackUrls);
rootStore.setN8nMetadata(fetchedSettings.n8nMetadata || {});
rootStore.setBinaryDataMode(fetchedSettings.binaryDataMode);
if (fetchedSettings.telemetry.enabled) {
void eventsApi.sessionStarted(rootStore.restApiContext);
}
};
const initialize = async () => {
if (initialized.value) {
return;
}
await getSettings();
initialized.value = true;
};
const stopShowingSetupPage = () => {
userManagement.value.showSetupOnFirstLoad = false;
};
const disableTemplates = () => {
settings.value = {
...settings.value,
templates: {
...settings.value.templates,
enabled: false,
},
};
};
const testTemplatesEndpoint = async () => {
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
await Promise.race([testHealthEndpoint(templatesHost.value), timeout]);
templatesEndpointHealthy.value = true;
};
const getTimezones = async (): Promise<IDataObject> => {
const rootStore = useRootStore();
return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/options/timezones');
};
const reset = () => {
settings.value = {} as FrontendSettings;
};
const getModuleSettings = async () => {
const fetched = await moduleSettingsApi.getModuleSettings(useRootStore().restApiContext);
moduleSettings.value = fetched;
};
const updateAiDataSharingSettings = async (allowSendingParameterValues: boolean) => {
const rootStore = useRootStore();
await aiUsageApi.updateAiUsageSettings(rootStore.restApiContext, {
allowSendingParameterValues,
});
if (settings.value.ai) {
settings.value.ai.allowSendingParameterValues = allowSendingParameterValues;
}
};
return {
settings,
userManagement,
templatesEndpointHealthy,
api,
mfa,
isDocker,
isDevRelease,
endpointHealth,
isEnterpriseFeatureEnabled,
databaseType,
planName,
consumerId,
binaryDataMode,
pruning,
security,
nodeJsVersion,
nodeEnv,
concurrency,
isConcurrencyEnabled,
isPublicApiEnabled,
isSwaggerUIEnabled,
isPreviewMode,
isCanvasOnly,
isCrdtCollaborationEnabled,
publicApiLatestVersion,
publicApiPath,
showSetupPage,
deploymentType,
isCloudDeployment,
isSmtpSetup,
isPersonalizationSurveyEnabled,
telemetry,
logLevel,
isTelemetryEnabled,
isMfaFeatureEnabled,
isFoldersFeatureEnabled,
isAiAssistantEnabled,
isCustomRolesFeatureEnabled,
areTagsEnabled,
isAutosaveEnabled,
isHiringBannerEnabled,
isTemplatesEnabled,
isTemplatesEndpointReachable,
templatesHost,
pushBackend,
isCommunityNodesFeatureEnabled,
isUnverifiedPackagesEnabled,
allowedModules,
isQueueModeEnabled,
isMultiMain,
isWorkerViewAvailable,
workflowCallerPolicyDefaultOption,
permanentlyDismissedBanners,
saveDataErrorExecution,
saveDataSuccessExecution,
saveManualExecutions,
saveDataProgressExecution,
isCommunityPlan,
isAskAiEnabled,
isAiBuilderEnabled,
isAiAssistantOrBuilderEnabled,
isAiCreditsEnabled,
aiCreditsQuota,
isAiDataSharingEnabled,
isAiGatewayEnabled,
aiGatewayBudget,
reset,
getTimezones,
testTemplatesEndpoint,
disableTemplates,
stopShowingSetupPage,
getSettings,
setSettings,
initialize,
getModuleSettings,
moduleSettings,
updateAiDataSharingSettings,
isMFAEnforcementLicensed,
isMFAEnforced,
activeModules,
isModuleActive,
isAgentModuleActive,
isDataTableFeatureEnabled,
isChatFeatureEnabled,
isOtelCustomSpanAttributesEnabled,
isAgentsKnowledgeBaseFeatureEnabled,
isPublicChatTriggerDisabled,
isWorkflowPublicationServiceEnabled,
};
});
/**
* @deprecated Import from `@n8n/stores/settings.store` instead. This store moved to
* `@n8n/stores` (CAT-3686 kernel slice); this re-export is a temporary shim kept
* so existing importers keep working and will be removed once call sites migrate.
*/
export * from '@n8n/stores/settings.store';
@@ -6,11 +6,10 @@ import { useSettingsStore } from '@/app/stores/settings.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { createComponentRenderer } from '@/__tests__/render';
import { setupServer } from '@/__tests__/server';
import { ROLE } from '@n8n/api-types';
import { AuthenticationMethod, ROLE } from '@n8n/api-types';
import { useUIStore } from '@/app/stores/ui.store';
import { useCloudPlanStore } from '@/app/stores/cloudPlan.store';
import { useSSOStore } from '@/features/settings/sso/sso.store';
import { UserManagementAuthenticationMethod } from '@/Interface';
let pinia: ReturnType<typeof createPinia>;
let settingsStore: ReturnType<typeof useSettingsStore>;
@@ -54,7 +53,7 @@ describe('SettingsPersonalView', () => {
await settingsStore.getSettings();
ssoStore.initialize({
authenticationMethod: UserManagementAuthenticationMethod.Email,
authenticationMethod: AuthenticationMethod.Email,
config: settingsStore.settings.sso,
features: {
saml: true,
@@ -1,4 +1,4 @@
import type { OidcConfigDto, SamlPreferences } from '@n8n/api-types';
import { AuthenticationMethod, type OidcConfigDto, type SamlPreferences } from '@n8n/api-types';
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { useRootStore } from '@n8n/stores/useRootStore';
@@ -7,7 +7,6 @@ import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso'
import * as ldapApi from '@n8n/rest-api-client/api/ldap';
import type { LdapConfig } from '@n8n/rest-api-client/api/ldap';
import type { IDataObject } from 'n8n-workflow';
import { UserManagementAuthenticationMethod } from '@/Interface';
export const SupportedProtocols = {
SAML: 'saml',
@@ -19,7 +18,7 @@ export type SupportedProtocolType = (typeof SupportedProtocols)[keyof typeof Sup
export const useSSOStore = defineStore('sso', () => {
const rootStore = useRootStore();
const authenticationMethod = ref<UserManagementAuthenticationMethod | undefined>(undefined);
const authenticationMethod = ref<AuthenticationMethod | undefined>(undefined);
const selectedAuthProtocol = ref<SupportedProtocolType | undefined>(undefined);
const ssoManagedByEnv = ref(false);
@@ -37,7 +36,7 @@ export const useSSOStore = defineStore('sso', () => {
await ssoApi.initSSO(rootStore.restApiContext, existingRedirect);
const initialize = (options: {
authenticationMethod: UserManagementAuthenticationMethod;
authenticationMethod: AuthenticationMethod;
managedByEnv?: boolean;
config: {
ldap?: Pick<LdapConfig, 'loginLabel' | 'loginEnabled'>;
@@ -97,7 +96,7 @@ export const useSSOStore = defineStore('sso', () => {
const isEnterpriseSamlEnabled = ref(false);
const isDefaultAuthenticationSaml = computed(
() => authenticationMethod.value === UserManagementAuthenticationMethod.Saml,
() => authenticationMethod.value === AuthenticationMethod.Saml,
);
const getSamlMetadata = async () => await ssoApi.getSamlMetadata(rootStore.restApiContext);
@@ -158,7 +157,7 @@ export const useSSOStore = defineStore('sso', () => {
});
const isDefaultAuthenticationOidc = computed(
() => authenticationMethod.value === UserManagementAuthenticationMethod.Oidc,
() => authenticationMethod.value === AuthenticationMethod.Oidc,
);
/**
@@ -1,7 +1,6 @@
import type { OidcConfigDto } from '@n8n/api-types';
import type { AuthenticationMethod, OidcConfigDto } from '@n8n/api-types';
import { createPinia, setActivePinia } from 'pinia';
import { useSSOStore, SupportedProtocols } from '@/features/settings/sso/sso.store';
import type { UserManagementAuthenticationMethod } from '@/Interface';
import * as ssoApi from '@n8n/rest-api-client/api/sso';
vi.mock('@n8n/rest-api-client/api/sso');
@@ -24,7 +23,7 @@ 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',
(authenticationMethod, saml, loginEnabled, expectation) => {
ssoStore.initialize({
authenticationMethod: authenticationMethod as UserManagementAuthenticationMethod,
authenticationMethod: authenticationMethod as AuthenticationMethod,
config: {
saml: {
loginEnabled,
@@ -45,7 +44,7 @@ describe('SSO store', () => {
it('should populate callbackUrl when re-initialized with authenticated settings', () => {
// Simulate public settings (before login) — no callbackUrl
ssoStore.initialize({
authenticationMethod: 'oidc' as UserManagementAuthenticationMethod,
authenticationMethod: 'oidc' as AuthenticationMethod,
config: {
oidc: { loginEnabled: false, loginUrl: 'http://localhost:5678/rest/sso/oidc/login' },
},
@@ -56,7 +55,7 @@ describe('SSO store', () => {
// Simulate authenticated settings (after login) — includes callbackUrl
ssoStore.initialize({
authenticationMethod: 'oidc' as UserManagementAuthenticationMethod,
authenticationMethod: 'oidc' as AuthenticationMethod,
config: {
oidc: {
loginEnabled: false,
@@ -80,7 +79,7 @@ describe('SSO store', () => {
it('should initialize selectedAuthProtocol to OIDC when default authentication is OIDC', () => {
// Initialize with OIDC as default authentication method
ssoStore.initialize({
authenticationMethod: 'oidc' as UserManagementAuthenticationMethod,
authenticationMethod: 'oidc' as AuthenticationMethod,
config: {
oidc: { loginEnabled: true },
},
@@ -104,7 +103,7 @@ describe('SSO store', () => {
it('should initialize selectedAuthProtocol to SAML when default authentication is SAML', () => {
// Initialize with SAML as default authentication method
ssoStore.initialize({
authenticationMethod: 'saml' as UserManagementAuthenticationMethod,
authenticationMethod: 'saml' as AuthenticationMethod,
config: {
saml: { loginEnabled: true },
},
@@ -128,7 +127,7 @@ describe('SSO store', () => {
it('should initialize selectedAuthProtocol to SAML when default authentication is email', () => {
// Initialize with email as default authentication method
ssoStore.initialize({
authenticationMethod: 'email' as UserManagementAuthenticationMethod,
authenticationMethod: 'email' as AuthenticationMethod,
config: {},
features: {
saml: true,
@@ -150,7 +149,7 @@ describe('SSO store', () => {
it('should not reinitialize selectedAuthProtocol if already set', () => {
// Initialize with SAML as default authentication method
ssoStore.initialize({
authenticationMethod: 'saml' as UserManagementAuthenticationMethod,
authenticationMethod: 'saml' as AuthenticationMethod,
config: {
saml: { loginEnabled: true },
},
@@ -212,7 +211,7 @@ describe('SSO store', () => {
it('should reset oidc.loginEnabled to false when server config has it disabled', async () => {
// Start with loginEnabled = true via initialize
ssoStore.initialize({
authenticationMethod: 'oidc' as UserManagementAuthenticationMethod,
authenticationMethod: 'oidc' as AuthenticationMethod,
config: { oidc: { loginEnabled: true } },
features: { saml: false, ldap: false, oidc: true },
});
+9
View File
@@ -5383,9 +5383,15 @@ importers:
'@n8n/permissions':
specifier: workspace:*
version: link:../../../@n8n/permissions
'@n8n/rest-api-client':
specifier: workspace:*
version: link:../rest-api-client
'@vueuse/core':
specifier: catalog:frontend
version: 14.3.0(vue@3.5.26(typescript@6.0.2))
bowser:
specifier: 2.11.0
version: 2.11.0
n8n-workflow:
specifier: workspace:*
version: link:../../../workflow
@@ -5435,6 +5441,9 @@ importers:
vitest:
specifier: 'catalog:'
version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
vitest-mock-extended:
specifier: 'catalog:'
version: 3.1.0(typescript@6.0.2)(vitest@4.1.9)
vue-tsc:
specifier: ^2.2.8
version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2)