From b16b2d8e84bb540f99f2d1a436739c9acd9aa048 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 21 Jul 2026 13:46:54 +0200 Subject: [PATCH] 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 --- packages/frontend/@n8n/stores/package.json | 3 + .../stores/src}/roles.store.test.ts | 5 +- .../frontend/@n8n/stores/src/roles.store.ts | 143 +++++ .../stores/src}/settings.store.test.ts | 9 +- .../@n8n/stores/src/settings.store.ts | 515 ++++++++++++++++++ packages/frontend/editor-ui/src/Interface.ts | 7 - .../frontend/editor-ui/src/__tests__/utils.ts | 4 +- .../editor-ui/src/app/constants/notice.ts | 16 - .../frontend/editor-ui/src/app/init.test.ts | 10 +- packages/frontend/editor-ui/src/app/init.ts | 7 +- .../editor-ui/src/app/stores/roles.store.ts | 148 +---- .../src/app/stores/settings.store.ts | 505 +---------------- .../auth/views/SettingsPersonalView.test.ts | 5 +- .../src/features/settings/sso/sso.store.ts | 11 +- .../src/features/settings/sso/sso.test.ts | 19 +- pnpm-lock.yaml | 9 + 16 files changed, 713 insertions(+), 703 deletions(-) rename packages/frontend/{editor-ui/src/app/stores => @n8n/stores/src}/roles.store.test.ts (98%) create mode 100644 packages/frontend/@n8n/stores/src/roles.store.ts rename packages/frontend/{editor-ui/src/app/stores => @n8n/stores/src}/settings.store.test.ts (98%) create mode 100644 packages/frontend/@n8n/stores/src/settings.store.ts diff --git a/packages/frontend/@n8n/stores/package.json b/packages/frontend/@n8n/stores/package.json index 9bc5adbeadd..45810e600c6 100644 --- a/packages/frontend/@n8n/stores/package.json +++ b/packages/frontend/@n8n/stores/package.json @@ -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" diff --git a/packages/frontend/editor-ui/src/app/stores/roles.store.test.ts b/packages/frontend/@n8n/stores/src/roles.store.test.ts similarity index 98% rename from packages/frontend/editor-ui/src/app/stores/roles.store.test.ts rename to packages/frontend/@n8n/stores/src/roles.store.test.ts index 4bf910fc12f..6063057e9ee 100644 --- a/packages/frontend/editor-ui/src/app/stores/roles.store.test.ts +++ b/packages/frontend/@n8n/stores/src/roles.store.test.ts @@ -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; let settingsStore: ReturnType; diff --git a/packages/frontend/@n8n/stores/src/roles.store.ts b/packages/frontend/@n8n/stores/src/roles.store.ts new file mode 100644 index 00000000000..256e6b7cd43 --- /dev/null +++ b/packages/frontend/@n8n/stores/src/roles.store.ts @@ -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) { + 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({ + global: [], + project: [], + credential: [], + workflow: [], + secretsProviderConnection: [], + }); + const projectRoleOrder = ref([ + 'project:viewer', + 'project:chatUser', + 'project:editor', + 'project:admin', + ]); + const projectRoleOrderMap = computed>( + () => new Map(projectRoleOrder.value.map((role, idx) => [role, idx])), + ); + + const globalRoleOrder = ref(['global:admin', 'global:member']); + const globalRoleOrderMap = computed>( + () => new Map(globalRoleOrder.value.map((role, idx) => [role, idx])), + ); + + const processedInstanceRoles = computed(() => + 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(() => + processedInstanceRoles.value.filter((role) => !role.systemRole), + ); + + const processedProjectRoles = computed(() => + 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(() => + roles.value.credential.filter((role) => role.slug !== 'credential:owner'), + ); + + const processedWorkflowRoles = computed(() => + 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 => { + return await rolesApi.createRole(rootStore.restApiContext, body); + }; + + const fetchRoleBySlug = async (payload: { slug: string }): Promise => { + return await rolesApi.getRoleBySlug(rootStore.restApiContext, payload); + }; + + const deleteRole = async (slug: string): Promise => { + return await rolesApi.deleteRole(rootStore.restApiContext, slug); + }; + + const updateRole = async (slug: string, body: UpdateRoleDto): Promise => { + return await rolesApi.updateRole(rootStore.restApiContext, slug, body); + }; + + const fetchRoleAssignments = async (slug: string): Promise => { + return await rolesApi.getRoleAssignments(rootStore.restApiContext, slug); + }; + + const fetchRoleProjectMembers = async ( + slug: string, + projectId: string, + ): Promise => { + return await rolesApi.getRoleProjectMembers(rootStore.restApiContext, slug, projectId); + }; + + const fetchRoleMembers = async (slug: string): Promise => { + return await rolesApi.getRoleMembers(rootStore.restApiContext, slug); + }; + + return { + roles, + processedProjectRoles, + processedInstanceRoles, + customInstanceRoles, + processedCredentialRoles, + processedWorkflowRoles, + fetchRoles, + createRole, + fetchRoleBySlug, + updateRole, + deleteRole, + fetchRoleAssignments, + fetchRoleProjectMembers, + fetchRoleMembers, + }; +}); diff --git a/packages/frontend/editor-ui/src/app/stores/settings.store.test.ts b/packages/frontend/@n8n/stores/src/settings.store.test.ts similarity index 98% rename from packages/frontend/editor-ui/src/app/stores/settings.store.test.ts rename to packages/frontend/@n8n/stores/src/settings.store.test.ts index 3952838dccc..4619fb97784 100644 --- a/packages/frontend/editor-ui/src/app/stores/settings.store.test.ts +++ b/packages/frontend/@n8n/stores/src/settings.store.test.ts @@ -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'); diff --git a/packages/frontend/@n8n/stores/src/settings.store.ts b/packages/frontend/@n8n/stores/src/settings.store.ts new file mode 100644 index 00000000000..9377671b3eb --- /dev/null +++ b/packages/frontend/@n8n/stores/src/settings.store.ts @@ -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 = ` + +

🚫

+

Your n8n server is configured to use a secure cookie,
however you are either visiting this via an insecure URL, or using Safari. +

+
+
+ To fix this, please consider the following options: +
    +
  • Setup TLS/HTTPS (recommended), or
  • +
  • If you are running this locally, and not using Safari, try using localhost instead
  • +
  • If you prefer to disable this security feature (not recommended), set the environment variable N8N_SECURE_COOKIE to false
  • +
+
+`; + +export const useSettingsStore = defineStore(STORES.SETTINGS, () => { + const initialized = ref(false); + const settings = ref({} as FrontendSettings); + const moduleSettings = ref({}); + const userManagement = ref({ + 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('all'); + const saveDataSuccessExecution = ref('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 => { + 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, + }; +}); diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts index 2131b55d328..7a7009bffc6 100644 --- a/packages/frontend/editor-ui/src/Interface.ts +++ b/packages/frontend/editor-ui/src/Interface.ts @@ -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[]; diff --git a/packages/frontend/editor-ui/src/__tests__/utils.ts b/packages/frontend/editor-ui/src/__tests__/utils.ts index 36c8fd35c82..abf14c91d02 100644 --- a/packages/frontend/editor-ui/src/__tests__/utils.ts +++ b/packages/frontend/editor-ui/src/__tests__/utils.ts @@ -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, }, diff --git a/packages/frontend/editor-ui/src/app/constants/notice.ts b/packages/frontend/editor-ui/src/app/constants/notice.ts index a1d60219578..f5d29030abc 100644 --- a/packages/frontend/editor-ui/src/app/constants/notice.ts +++ b/packages/frontend/editor-ui/src/app/constants/notice.ts @@ -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 = ` - -

🚫

-

Your n8n server is configured to use a secure cookie,
however you are either visiting this via an insecure URL, or using Safari. -

-
-
- To fix this, please consider the following options: -
    -
  • Setup TLS/HTTPS (recommended), or
  • -
  • If you are running this locally, and not using Safari, try using localhost instead
  • -
  • If you prefer to disable this security feature (not recommended), set the environment variable N8N_SECURE_COOKIE to false
  • -
-
-`; diff --git a/packages/frontend/editor-ui/src/app/init.test.ts b/packages/frontend/editor-ui/src/app/init.test.ts index 71ff3399f5e..c36fc8ea56a 100644 --- a/packages/frontend/editor-ui/src/app/init.test.ts +++ b/packages/frontend/editor-ui/src/app/init.test.ts @@ -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: { diff --git a/packages/frontend/editor-ui/src/app/init.ts b/packages/frontend/editor-ui/src/app/init.ts index 282079284a0..d6056fa7da8 100644 --- a/packages/frontend/editor-ui/src/app/init.ts +++ b/packages/frontend/editor-ui/src/app/init.ts @@ -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: { diff --git a/packages/frontend/editor-ui/src/app/stores/roles.store.ts b/packages/frontend/editor-ui/src/app/stores/roles.store.ts index d9d41a5fc20..cba2b79852e 100644 --- a/packages/frontend/editor-ui/src/app/stores/roles.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/roles.store.ts @@ -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) { - 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({ - global: [], - project: [], - credential: [], - workflow: [], - secretsProviderConnection: [], - }); - const projectRoleOrder = ref([ - 'project:viewer', - 'project:chatUser', - 'project:editor', - 'project:admin', - ]); - const projectRoleOrderMap = computed>( - () => new Map(projectRoleOrder.value.map((role, idx) => [role, idx])), - ); - - const globalRoleOrder = ref(['global:admin', 'global:member']); - const globalRoleOrderMap = computed>( - () => new Map(globalRoleOrder.value.map((role, idx) => [role, idx])), - ); - - const processedInstanceRoles = computed(() => - 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(() => - processedInstanceRoles.value.filter((role) => !role.systemRole), - ); - - const processedProjectRoles = computed(() => - 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(() => - roles.value.credential.filter((role) => role.slug !== 'credential:owner'), - ); - - const processedWorkflowRoles = computed(() => - 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 => { - return await rolesApi.createRole(rootStore.restApiContext, body); - }; - - const fetchRoleBySlug = async (payload: { slug: string }): Promise => { - return await rolesApi.getRoleBySlug(rootStore.restApiContext, payload); - }; - - const deleteRole = async (slug: string): Promise => { - return await rolesApi.deleteRole(rootStore.restApiContext, slug); - }; - - const updateRole = async (slug: string, body: UpdateRoleDto): Promise => { - return await rolesApi.updateRole(rootStore.restApiContext, slug, body); - }; - - const fetchRoleAssignments = async (slug: string): Promise => { - return await rolesApi.getRoleAssignments(rootStore.restApiContext, slug); - }; - - const fetchRoleProjectMembers = async ( - slug: string, - projectId: string, - ): Promise => { - return await rolesApi.getRoleProjectMembers(rootStore.restApiContext, slug, projectId); - }; - - const fetchRoleMembers = async (slug: string): Promise => { - 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'; diff --git a/packages/frontend/editor-ui/src/app/stores/settings.store.ts b/packages/frontend/editor-ui/src/app/stores/settings.store.ts index fedcf318ef8..f2117bdbf05 100644 --- a/packages/frontend/editor-ui/src/app/stores/settings.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/settings.store.ts @@ -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({} as FrontendSettings); - const moduleSettings = ref({}); - const userManagement = ref({ - 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('all'); - const saveDataSuccessExecution = ref('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 => { - 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'; diff --git a/packages/frontend/editor-ui/src/features/core/auth/views/SettingsPersonalView.test.ts b/packages/frontend/editor-ui/src/features/core/auth/views/SettingsPersonalView.test.ts index 15bf4a8ad8e..1338096cf6d 100644 --- a/packages/frontend/editor-ui/src/features/core/auth/views/SettingsPersonalView.test.ts +++ b/packages/frontend/editor-ui/src/features/core/auth/views/SettingsPersonalView.test.ts @@ -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; let settingsStore: ReturnType; @@ -54,7 +53,7 @@ describe('SettingsPersonalView', () => { await settingsStore.getSettings(); ssoStore.initialize({ - authenticationMethod: UserManagementAuthenticationMethod.Email, + authenticationMethod: AuthenticationMethod.Email, config: settingsStore.settings.sso, features: { saml: true, diff --git a/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts b/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts index 77c2376b96c..8e357968a04 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts +++ b/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts @@ -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(undefined); + const authenticationMethod = ref(undefined); const selectedAuthProtocol = ref(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; @@ -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, ); /** diff --git a/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts b/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts index d31b18404ce..f6ea2422687 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts +++ b/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts @@ -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 }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 878c434a9fb..0e30a007791 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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)