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 @@
+
+
+
+
+
{{
+ locale.baseText('settings.auditLogs.title')
+ }}
+
+
+
+ {{ locale.baseText('settings.auditLogs.actionBox.title') }}
+
+
+
+
+
+
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: {