diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 8e815c9d2a..d003c7312f 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -83,6 +83,7 @@ export class FrontendService { } this.settings = { + previewMode: process.env.N8N_PREVIEW_MODE === 'true', endpointForm: config.getEnv('endpoints.form'), endpointFormTest: config.getEnv('endpoints.formTest'), endpointFormWaiting: config.getEnv('endpoints.formWaiting'), diff --git a/packages/editor-ui/src/rbac/checks/__tests__/isAuthenticated.test.ts b/packages/editor-ui/src/rbac/checks/__tests__/isAuthenticated.test.ts index 96fd17aaab..b38cb1d35b 100644 --- a/packages/editor-ui/src/rbac/checks/__tests__/isAuthenticated.test.ts +++ b/packages/editor-ui/src/rbac/checks/__tests__/isAuthenticated.test.ts @@ -7,8 +7,9 @@ vi.mock('@/stores/users.store', () => ({ describe('Checks', () => { describe('isAuthenticated()', () => { + const mockUser = { id: 'user123', name: 'Test User' }; + it('should return true if there is a current user', () => { - const mockUser = { id: 'user123', name: 'Test User' }; vi.mocked(useUsersStore).mockReturnValue({ currentUser: mockUser } as unknown as ReturnType< typeof useUsersStore >); @@ -23,5 +24,29 @@ describe('Checks', () => { expect(isAuthenticated()).toBe(false); }); + + it('should return true if there is a current user and bypass returns false', () => { + vi.mocked(useUsersStore).mockReturnValue({ currentUser: mockUser } as ReturnType< + typeof useUsersStore + >); + + expect(isAuthenticated({ bypass: () => false })).toBe(true); + }); + + it('should return true if there is no current user and bypass returns true', () => { + vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType< + typeof useUsersStore + >); + + expect(isAuthenticated({ bypass: () => true })).toBe(true); + }); + + it('should return false if there is no current user and bypass returns false', () => { + vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType< + typeof useUsersStore + >); + + expect(isAuthenticated({ bypass: () => false })).toBe(false); + }); }); }); diff --git a/packages/editor-ui/src/rbac/checks/isAuthenticated.ts b/packages/editor-ui/src/rbac/checks/isAuthenticated.ts index ed8654410f..4cbdd4cf18 100644 --- a/packages/editor-ui/src/rbac/checks/isAuthenticated.ts +++ b/packages/editor-ui/src/rbac/checks/isAuthenticated.ts @@ -1,7 +1,11 @@ import { useUsersStore } from '@/stores/users.store'; import type { RBACPermissionCheck, AuthenticatedPermissionOptions } from '@/types/rbac'; -export const isAuthenticated: RBACPermissionCheck = () => { +export const isAuthenticated: RBACPermissionCheck = (options) => { + if (options?.bypass?.()) { + return true; + } + const usersStore = useUsersStore(); return !!usersStore.currentUser; }; diff --git a/packages/editor-ui/src/rbac/middleware/authenticated.ts b/packages/editor-ui/src/rbac/middleware/authenticated.ts index 20100f78f0..0f74161600 100644 --- a/packages/editor-ui/src/rbac/middleware/authenticated.ts +++ b/packages/editor-ui/src/rbac/middleware/authenticated.ts @@ -7,8 +7,9 @@ export const authenticatedMiddleware: RouterMiddleware { - const valid = isAuthenticated(); + const valid = isAuthenticated(options); if (!valid) { const redirect = to.query.redirect ?? diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 078912bbd8..afd9f4498b 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -359,6 +359,14 @@ export const routes = [ }, meta: { middleware: ['authenticated'], + middlewareOptions: { + authenticated: { + bypass: () => { + const settingsStore = useSettingsStore(); + return settingsStore.isPreviewMode; + }, + }, + }, }, }, { diff --git a/packages/editor-ui/src/stores/settings.store.ts b/packages/editor-ui/src/stores/settings.store.ts index 6437dd4d58..42418900cc 100644 --- a/packages/editor-ui/src/stores/settings.store.ts +++ b/packages/editor-ui/src/stores/settings.store.ts @@ -85,6 +85,9 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { isSwaggerUIEnabled(): boolean { return this.api.swaggerUi.enabled; }, + isPreviewMode(): boolean { + return this.settings.previewMode; + }, publicApiLatestVersion(): number { return this.api.latestVersion; }, diff --git a/packages/editor-ui/src/types/rbac.ts b/packages/editor-ui/src/types/rbac.ts index 4fbff27728..97c5f56bfb 100644 --- a/packages/editor-ui/src/types/rbac.ts +++ b/packages/editor-ui/src/types/rbac.ts @@ -2,7 +2,9 @@ import type { EnterpriseEditionFeature } from '@/constants'; import type { Resource, ScopeOptions, Scope } from '@n8n/permissions'; import type { IRole } from '@/Interface'; -export type AuthenticatedPermissionOptions = {}; +export type AuthenticatedPermissionOptions = { + bypass?: () => boolean; +}; export type CustomPermissionOptions = RBACPermissionCheck; export type DefaultUserMiddlewareOptions = {}; export type InstanceOwnerMiddlewareOptions = {}; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 9afd6cec17..24bc312eb5 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -805,13 +805,16 @@ export default defineComponent({ this.clipboard.onPaste.value = this.onClipboardPasteEvent; this.canvasStore.startLoading(); - const loadPromises = [ - this.loadActiveWorkflows(), - this.loadCredentials(), - this.loadCredentialTypes(), - this.loadVariables(), - this.loadSecrets(), - ]; + const loadPromises = + this.settingsStore.isPreviewMode && this.isDemo + ? [] + : [ + this.loadActiveWorkflows(), + this.loadCredentials(), + this.loadCredentialTypes(), + this.loadVariables(), + this.loadSecrets(), + ]; if (this.nodeTypesStore.allNodeTypes.length === 0) { loadPromises.push(this.loadNodeTypes()); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 709aaafd9d..fbe78b49ee 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2505,6 +2505,7 @@ export interface IN8nUISettings { workflowTagsDisabled: boolean; logLevel: LogLevel; hiringBannerEnabled: boolean; + previewMode: boolean; templates: { enabled: boolean; host: string;