mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
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:
@@ -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"
|
||||
|
||||
+3
-2
@@ -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,
|
||||
};
|
||||
});
|
||||
+2
-7
@@ -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">🚫</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">🚫</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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
+2
-3
@@ -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 },
|
||||
});
|
||||
|
||||
Generated
+9
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user