diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 023e3e827f..bb6e64f56f 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -142,6 +142,9 @@ export class Start extends Command { LoggerProxy.init(logger); logger.info('Initializing n8n process'); + // todo remove a few versions after release + logger.info('\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n'); + // Start directly with the init of the database to improve startup time const startDbInitPromise = Db.init().catch((error: Error) => { logger.error(`There was an error initializing DB: "${error.message}"`); diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 0da11e786a..476cbbcfa7 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -618,6 +618,27 @@ const config = convict({ }, }, + versionNotifications: { + enabled: { + doc: 'Whether feature is enabled to request notifications about new versions and security updates.', + format: Boolean, + default: true, + env: 'N8N_VERSION_NOTIFICATIONS_ENABLED', + }, + endpoint: { + doc: 'Endpoint to retrieve version information from.', + format: String, + default: 'https://api.n8n.io/versions/', + env: 'N8N_VERSION_NOTIFICATIONS_ENDPOINT', + }, + infoUrl: { + doc: `Url in New Versions Panel with more information on updating one's instance.`, + format: String, + default: 'https://docs.n8n.io/getting-started/installation/updating.html', + env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL', + }, + }, + }); // Overwrite default configuration with settings which got defined in diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index e1c09cf55e..d2aff0eb86 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -312,6 +312,11 @@ export interface IN8nConfigNodes { exclude: string[]; } +export interface IVersionNotificationSettings { + enabled: boolean; + endpoint: string; + infoUrl: string; +} export interface IN8nUISettings { endpointWebhook: string; @@ -331,6 +336,8 @@ export interface IN8nUISettings { n8nMetadata?: { [key: string]: string | number | undefined; }; + versionNotifications: IVersionNotificationSettings; + instanceId: string; } export interface IPackageVersions { diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index b36a0fe0f9..0abd3f60f1 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -21,7 +21,7 @@ import * as clientOAuth1 from 'oauth-1.0a'; import { RequestOptions } from 'oauth-1.0a'; import * as csrf from 'csrf'; import * as requestPromise from 'request-promise-native'; -import { createHmac } from 'crypto'; +import { createHash, createHmac } from 'crypto'; // IMPORTANT! Do not switch to anther bcrypt library unless really necessary and // tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ... import { compare } from 'bcryptjs'; @@ -196,6 +196,12 @@ class App { 'oauth1': urlBaseWebhook + `${this.restEndpoint}/oauth1-credential/callback`, 'oauth2': urlBaseWebhook + `${this.restEndpoint}/oauth2-credential/callback`, }, + versionNotifications: { + enabled: config.get('versionNotifications.enabled'), + endpoint: config.get('versionNotifications.endpoint'), + infoUrl: config.get('versionNotifications.infoUrl'), + }, + instanceId: '', }; } @@ -225,6 +231,7 @@ class App { this.versions = await GenericHelpers.getVersions(); this.frontendSettings.versionCli = this.versions.cli; + this.frontendSettings.instanceId = await generateInstanceId() as string; await this.externalHooks.run('frontend.settings', [this.frontendSettings]); @@ -2210,3 +2217,10 @@ async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: nu const count = await Db.collections.Execution!.count(countFilter); return { count, estimate: false }; } + +async function generateInstanceId() { + const encryptionKey = await UserSettings.getEncryptionKey(); + const hash = encryptionKey ? createHash('sha256').update(encryptionKey.slice(Math.round(encryptionKey.length / 2))).digest('hex') : undefined; + + return hash; +} diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 4486c5c02d..0acc895567 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -25,7 +25,9 @@ "test:unit": "vue-cli-service test:unit" }, "dependencies": { - "v-click-outside": "^3.1.2" + "timeago.js": "^4.0.2", + "v-click-outside": "^3.1.2", + "vue-fragment": "^1.5.2" }, "devDependencies": { "@beyonk/google-fonts-webpack-plugin": "^1.5.0", diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index cc3f89b8c6..6151d7e37e 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -445,6 +445,12 @@ export interface IPushDataConsoleMessage { message: string; } +export interface IVersionNotificationSettings { + enabled: boolean; + endpoint: string; + infoUrl: string; +} + export interface IN8nUISettings { endpointWebhook: string; endpointWebhookTest: string; @@ -463,6 +469,8 @@ export interface IN8nUISettings { n8nMetadata?: { [key: string]: string | number | undefined; }; + versionNotifications: IVersionNotificationSettings; + instanceId: string; } export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { @@ -547,6 +555,29 @@ export interface ITagRow { delete?: boolean; } +export interface IVersion { + name: string; + nodes: IVersionNode[]; + createdAt: string; + description: string; + documentationUrl: string; + hasBreakingChange: boolean; + hasSecurityFix: boolean; + hasSecurityIssue: boolean; + securityIssueFixVersion: string; +} + +export interface IVersionNode { + name: string; + displayName: string; + icon: string; + defaults: INodeParameters; + iconData: { + type: string; + icon?: string; + fileBuffer?: string; + }; +} export interface IRootState { activeExecutions: IExecutionsCurrentSummaryExtended[]; activeWorkflows: string[]; @@ -583,6 +614,7 @@ export interface IRootState { urlBaseWebhook: string; workflow: IWorkflowDb; sidebarMenuItems: IMenuItem[]; + instanceId: string; } export interface ITagsState { @@ -605,6 +637,12 @@ export interface IUiState { isPageLoading: boolean; } +export interface IVersionsState { + versionNotificationSettings: IVersionNotificationSettings; + nextVersions: IVersion[]; + currentVersion: IVersion | undefined; +} + export interface IWorkflowsState { } diff --git a/packages/editor-ui/src/api/helpers.ts b/packages/editor-ui/src/api/helpers.ts index 739242c12a..1ae753db1c 100644 --- a/packages/editor-ui/src/api/helpers.ts +++ b/packages/editor-ui/src/api/helpers.ts @@ -6,7 +6,6 @@ import { IRestApiContext, } from '../Interface'; - class ResponseError extends Error { // The HTTP status code of response httpStatusCode?: number; @@ -91,6 +90,6 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho return response.data; } -export async function get(baseURL: string, endpoint: string, params?: IDataObject) { - return await request({method: 'GET', baseURL, endpoint, data: params}); +export async function get(baseURL: string, endpoint: string, params?: IDataObject, headers?: IDataObject) { + return await request({method: 'GET', baseURL, endpoint, headers, data: params}); } diff --git a/packages/editor-ui/src/api/versions.ts b/packages/editor-ui/src/api/versions.ts new file mode 100644 index 0000000000..009f6bc4b7 --- /dev/null +++ b/packages/editor-ui/src/api/versions.ts @@ -0,0 +1,9 @@ +import { IVersion } from '@/Interface'; +import { INSTANCE_ID_HEADER } from '@/constants'; +import { IDataObject } from 'n8n-workflow'; +import { get } from './helpers'; + +export async function getNextVersions(endpoint: string, version: string, instanceId: string): Promise { + const headers = {[INSTANCE_ID_HEADER as string] : instanceId}; + return await get(endpoint, version, {}, headers); +} diff --git a/packages/editor-ui/src/components/Badge.vue b/packages/editor-ui/src/components/Badge.vue new file mode 100644 index 0000000000..22fa536c7b --- /dev/null +++ b/packages/editor-ui/src/components/Badge.vue @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/packages/editor-ui/src/components/GiftNotificationIcon.vue b/packages/editor-ui/src/components/GiftNotificationIcon.vue new file mode 100644 index 0000000000..e10fa9c5ae --- /dev/null +++ b/packages/editor-ui/src/components/GiftNotificationIcon.vue @@ -0,0 +1,36 @@ + + + \ No newline at end of file diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index cadabe7884..a8d4d14fcc 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -127,6 +127,14 @@ + @@ -149,6 +157,7 @@ import About from '@/components/About.vue'; import CredentialsEdit from '@/components/CredentialsEdit.vue'; import CredentialsList from '@/components/CredentialsList.vue'; import ExecutionsList from '@/components/ExecutionsList.vue'; +import GiftNotificationIcon from './GiftNotificationIcon.vue'; import WorkflowSettings from '@/components/WorkflowSettings.vue'; import { genericHelpers } from '@/components/mixins/genericHelpers'; @@ -212,6 +221,7 @@ export default mixins( CredentialsEdit, CredentialsList, ExecutionsList, + GiftNotificationIcon, WorkflowSettings, MenuItemsIterator, }, @@ -232,6 +242,10 @@ export default mixins( ...mapGetters('ui', { isCollapsed: 'sidebarMenuCollapsed', }), + ...mapGetters('versions', [ + 'hasVersionUpdates', + 'nextVersions', + ]), exeuctionId (): string | undefined { return this.$route.params.id; }, @@ -311,6 +325,9 @@ export default mixins( openTagManager() { this.$store.dispatch('ui/openTagsManagerModal'); }, + openUpdatesPanel() { + this.$store.dispatch('ui/openUpdatesPanel'); + }, async stopExecution () { const executionId = this.$store.getters.activeExecutionId; if (executionId === null) { @@ -574,6 +591,39 @@ a.logo { &.expanded { width: $--sidebar-expanded-width; } + + ul { + display: flex; + flex-direction: column; + } +} + +.footer-menu-items { + display: flex; + flex-grow: 1; + flex-direction: column; + justify-content: flex-end; + padding-bottom: 32px; +} + +.el-menu-item.updates { + color: $--sidebar-inactive-color; + .item-title-root { + font-size: 13px; + top: 0 !important; + } + + &:hover { + color: $--sidebar-active-color; + } + + .gift-container { + display: flex; + justify-content: flex-start; + align-items: center; + height: 100%; + width: 100%; + } } diff --git a/packages/editor-ui/src/components/Modal.vue b/packages/editor-ui/src/components/Modal.vue index 274d6fc942..e73daea116 100644 --- a/packages/editor-ui/src/components/Modal.vue +++ b/packages/editor-ui/src/components/Modal.vue @@ -1,6 +1,21 @@