diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 398180af18..545c6449b2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -313,6 +313,7 @@ export class Server extends AbstractServer { advancedExecutionFilters: false, variables: false, versionControl: false, + auditLogs: false, }, hideUsagePage: config.getEnv('hideUsagePage'), license: { diff --git a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts index ae891cfad8..07fbaf99dd 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts @@ -17,6 +17,7 @@ const defaultSettings: IN8nUISettings = { advancedExecutionFilters: false, variables: true, versionControl: false, + auditLogs: false, }, executionMode: 'regular', executionTimeout: 0, diff --git a/packages/editor-ui/src/__tests__/utils.ts b/packages/editor-ui/src/__tests__/utils.ts index 0d1e252ea5..932b955a22 100644 --- a/packages/editor-ui/src/__tests__/utils.ts +++ b/packages/editor-ui/src/__tests__/utils.ts @@ -44,6 +44,7 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = { logStreaming: false, variables: false, versionControl: false, + auditLogs: false, }, executionMode: 'regular', executionTimeout: 0, diff --git a/packages/editor-ui/src/components/SettingsSidebar.vue b/packages/editor-ui/src/components/SettingsSidebar.vue index bfae8129b8..08f45963b4 100644 --- a/packages/editor-ui/src/components/SettingsSidebar.vue +++ b/packages/editor-ui/src/components/SettingsSidebar.vue @@ -74,6 +74,14 @@ export default defineComponent({ available: this.canAccessApiSettings(), activateOnRouteNames: [VIEWS.API_SETTINGS], }, + { + id: 'settings-audit-logs', + icon: 'clipboard-list', + label: this.$locale.baseText('settings.auditLogs.title'), + position: 'top', + available: this.canAccessAuditLogs(), + activateOnRouteNames: [VIEWS.AUDIT_LOGS], + }, { id: 'settings-version-control', icon: 'code-branch', @@ -159,6 +167,9 @@ export default defineComponent({ canAccessVersionControl(): boolean { return this.canUserAccessRouteByName(VIEWS.VERSION_CONTROL); }, + canAccessAuditLogs(): boolean { + return this.canUserAccessRouteByName(VIEWS.AUDIT_LOGS); + }, canAccessSso(): boolean { return this.canUserAccessRouteByName(VIEWS.SSO_SETTINGS); }, @@ -220,6 +231,11 @@ export default defineComponent({ void this.$router.push({ name: VIEWS.VERSION_CONTROL }); } break; + case 'settings-audit-logs': + if (this.$router.currentRoute.name !== VIEWS.AUDIT_LOGS) { + void this.$router.push({ name: VIEWS.AUDIT_LOGS }); + } + break; default: break; } diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 713f8efeb6..a6902c6a84 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -376,6 +376,7 @@ export const enum VIEWS { SSO_SETTINGS = 'SSoSettings', SAML_ONBOARDING = 'SamlOnboarding', VERSION_CONTROL = 'VersionControl', + AUDIT_LOGS = 'AuditLogs', } export const enum FAKE_DOOR_FEATURES { @@ -443,6 +444,7 @@ export const enum EnterpriseEditionFeature { Variables = 'variables', Saml = 'saml', VersionControl = 'versionControl', + AuditLogs = 'auditLogs', } export const MAIN_NODE_PANEL_WIDTH = 360; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 25e75dd91c..808314759b 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1407,6 +1407,10 @@ "settings.versionControl.refreshBranches.success": "Branches successfully refreshed", "settings.versionControl.refreshBranches.error": "Error refreshing branches", "showMessage.cancel": "@:_reusableBaseText.cancel", + "settings.auditLogs.title": "Audit Logs", + "settings.auditLogs.actionBox.title": "Available on Enterprise plan", + "settings.auditLogs.actionBox.description": "Upgrade to see the audit logs of your n8n instance.", + "settings.auditLogs.actionBox.buttonText": "See plans", "showMessage.ok": "OK", "showMessage.showDetails": "Show Details", "startupError": "Error connecting to n8n", diff --git a/packages/editor-ui/src/plugins/icons/index.ts b/packages/editor-ui/src/plugins/icons/index.ts index ef898784af..fd89183048 100644 --- a/packages/editor-ui/src/plugins/icons/index.ts +++ b/packages/editor-ui/src/plugins/icons/index.ts @@ -32,6 +32,7 @@ import { faCodeBranch, faCog, faCogs, + faClipboardList, faClock, faClone, faCloud, @@ -171,6 +172,7 @@ addIcon(faCode); addIcon(faCodeBranch); addIcon(faCog); addIcon(faCogs); +addIcon(faClipboardList); addIcon(faClock); addIcon(faClone); addIcon(faCloud); diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index d9ac354ae9..e9ad0052a0 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -42,6 +42,7 @@ import SettingsSso from './views/SettingsSso.vue'; import SignoutView from '@/views/SignoutView.vue'; import SamlOnboarding from '@/views/SamlOnboarding.vue'; import SettingsVersionControl from './views/SettingsVersionControl.vue'; +import SettingsAuditLogs from './views/SettingsAuditLogs.vue'; import { usePostHog } from './stores/posthog.store'; Vue.use(Router); @@ -714,6 +715,31 @@ export const routes = [ }, }, }, + { + path: 'audit-logs', + name: VIEWS.AUDIT_LOGS, + components: { + settingsView: SettingsAuditLogs, + }, + meta: { + telemetry: { + pageCategory: 'settings', + getProperties(route: Route) { + return { + feature: 'audit-logs', + }; + }, + }, + permissions: { + allow: { + role: [ROLE.Owner], + }, + deny: { + shouldDeny: () => !window.localStorage.getItem('audit-logs'), + }, + }, + }, + }, ], }, { diff --git a/packages/editor-ui/src/stores/auditLogs.store.ts b/packages/editor-ui/src/stores/auditLogs.store.ts new file mode 100644 index 0000000000..7fec2b2db1 --- /dev/null +++ b/packages/editor-ui/src/stores/auditLogs.store.ts @@ -0,0 +1,16 @@ +import { computed } from 'vue'; +import { defineStore } from 'pinia'; +import { EnterpriseEditionFeature } from '@/constants'; +import { useSettingsStore } from '@/stores'; + +export const useAuditLogsStore = defineStore('auditLogs', () => { + const settingsStore = useSettingsStore(); + + const isEnterpriseAuditLogsFeatureEnabled = computed(() => + settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AuditLogs), + ); + + return { + isEnterpriseAuditLogsFeatureEnabled, + }; +}); diff --git a/packages/editor-ui/src/stores/index.ts b/packages/editor-ui/src/stores/index.ts index c5740d8075..bab882957c 100644 --- a/packages/editor-ui/src/stores/index.ts +++ b/packages/editor-ui/src/stores/index.ts @@ -24,3 +24,4 @@ export * from './workflows.store'; export * from './cloudPlan.store'; export * from './versionControl.store'; export * from './sso.store'; +export * from './auditLogs.store'; diff --git a/packages/editor-ui/src/views/SettingsAuditLogs.vue b/packages/editor-ui/src/views/SettingsAuditLogs.vue new file mode 100644 index 0000000000..e7d0caf90c --- /dev/null +++ b/packages/editor-ui/src/views/SettingsAuditLogs.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/packages/editor-ui/src/views/__tests__/SettingsAuditLogs.test.ts b/packages/editor-ui/src/views/__tests__/SettingsAuditLogs.test.ts new file mode 100644 index 0000000000..8b3b01115e --- /dev/null +++ b/packages/editor-ui/src/views/__tests__/SettingsAuditLogs.test.ts @@ -0,0 +1,57 @@ +import { vi } from 'vitest'; +import { render } from '@testing-library/vue'; +import { createPinia, setActivePinia, PiniaVuePlugin } from 'pinia'; +import { merge } from 'lodash-es'; +import { i18nInstance } from '@/plugins/i18n'; +import { useAuditLogsStore, useSettingsStore } from '@/stores'; +import SettingsAuditLogs from '@/views/SettingsAuditLogs.vue'; + +let pinia: ReturnType; +let settingsStore: ReturnType; +let auditLogsStore: ReturnType; + +const renderComponent = (renderOptions: Parameters[1] = {}) => + render( + SettingsAuditLogs, + merge( + { + pinia, + i18n: i18nInstance, + }, + renderOptions, + ), + (vue) => { + vue.use(PiniaVuePlugin); + }, + ); + +describe('SettingsAuditLogs', () => { + beforeEach(() => { + pinia = createPinia(); + setActivePinia(pinia); + settingsStore = useSettingsStore(); + auditLogsStore = useAuditLogsStore(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render paywall state when there is no license', () => { + vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled').mockReturnValue(false); + + const { getByTestId, queryByTestId } = renderComponent(); + + expect(queryByTestId('audit-logs-content-licensed')).not.toBeInTheDocument(); + expect(getByTestId('audit-logs-content-unlicensed')).toBeInTheDocument(); + }); + + it('should render licensed content', () => { + vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled').mockReturnValue(true); + + const { getByTestId, queryByTestId } = renderComponent(); + + expect(getByTestId('audit-logs-content-licensed')).toBeInTheDocument(); + expect(queryByTestId('audit-logs-content-unlicensed')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 82dd3da711..790b704bd2 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2112,6 +2112,7 @@ export interface IN8nUISettings { advancedExecutionFilters: boolean; variables: boolean; versionControl: boolean; + auditLogs: boolean; }; hideUsagePage: boolean; license: {