diff --git a/PROCFILE b/PROCFILE new file mode 100644 index 0000000000000..797d9ec999a16 --- /dev/null +++ b/PROCFILE @@ -0,0 +1 @@ +web: diff --git a/app.json b/app.json new file mode 100644 index 0000000000000..a0f8043838b29 --- /dev/null +++ b/app.json @@ -0,0 +1,3 @@ +{ + "stack": "heroku-20" +} diff --git a/package.json b/package.json index 86c7c4926af12..0332d654be9d6 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "tsc-watch": "^6.0.0", "turbo": "1.8.8", "typescript": "*", - "vite": "^4.0.4", + "vite": "^4.3.8", "vitest": "^0.28.5", "vue-template-compiler": "^2.7.14", "vue-tsc": "^1.0.24" diff --git a/packages/cli/bin/n8n b/packages/cli/bin/n8n index 92c38b416b048..9dc7c7ffd01d9 100755 --- a/packages/cli/bin/n8n +++ b/packages/cli/bin/n8n @@ -21,7 +21,7 @@ if (process.argv.length === 2) { const nodeVersion = process.versions.node; const nodeVersionMajor = require('semver').major(nodeVersion); -if (![16, 18].includes(nodeVersionMajor)) { +if (![16, 18, 19, 20, 22].includes(nodeVersionMajor)) { console.log(` Your Node.js version (${nodeVersion}) is currently not supported by n8n. Please use Node.js v16 (recommended), or v18 instead! diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/AbstractServer.ts index ab27a710c53ae..2a58444e8b1f5 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/AbstractServer.ts @@ -27,6 +27,8 @@ import { corsMiddleware } from '@/middlewares'; import { TestWebhooks } from '@/TestWebhooks'; import { WaitingWebhooks } from '@/WaitingWebhooks'; import { WEBHOOK_METHODS } from '@/WebhookHelpers'; +import dotenv from 'dotenv'; +dotenv.config(); const emptyBuffer = Buffer.alloc(0); @@ -401,7 +403,10 @@ export abstract class AbstractServer { this.server = http.createServer(app); } - const PORT = config.getEnv('port'); + //const PORT = config.getEnv('port'); + const prt = parseInt(process.env.PORT!); + const PORT = prt; + console.log(`port from ENV is ${prt}`); const ADDRESS = config.getEnv('listen_address'); this.server.on('error', (error: Error & { code: string }) => { diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 1cbfca63073ee..1de34856d34f9 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -67,6 +67,7 @@ import { whereClause } from './UserManagement/UserManagementHelper'; import { WorkflowsService } from './workflows/workflows.services'; import { START_NODES } from './constants'; import { webhookNotFoundErrorMessage } from './utils'; +import { In } from 'typeorm'; const WEBHOOK_PROD_UNREGISTERED_HINT = "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"; @@ -168,11 +169,7 @@ export class ActiveWorkflowRunner { activeWorkflowIds.push.apply(activeWorkflowIds, this.activeWorkflows.allActiveWorkflows()); const activeWorkflows = await this.getActiveWorkflows(); - activeWorkflowIds = [ - ...activeWorkflowIds, - ...activeWorkflows.map((workflow) => workflow.id.toString()), - ]; - + activeWorkflowIds = [...activeWorkflowIds, ...activeWorkflows]; // Make sure IDs are unique activeWorkflowIds = Array.from(new Set(activeWorkflowIds)); @@ -348,30 +345,31 @@ export class ActiveWorkflowRunner { /** * Returns the ids of the currently active workflows */ - async getActiveWorkflows(user?: User): Promise { + async getActiveWorkflows(user?: User): Promise { let activeWorkflows: WorkflowEntity[] = []; - if (!user || user.globalRole.name === 'owner') { activeWorkflows = await Db.collections.Workflow.find({ select: ['id'], where: { active: true }, }); + return activeWorkflows.map((workflow) => workflow.id.toString()); } else { + const active = await Db.collections.Workflow.find({ + select: ['id'], + where: { active: true }, + }); + const activeIds = active.map((workflow) => workflow.id); + const where = whereClause({ + user, + entityType: 'workflow', + }); + Object.assign(where, { workflowId: In(activeIds) }); const shared = await Db.collections.SharedWorkflow.find({ - relations: ['workflow'], - where: whereClause({ - user, - entityType: 'workflow', - }), + select: ['workflowId'], + where, }); - - activeWorkflows = shared.reduce((acc, cur) => { - if (cur.workflow.active) acc.push(cur.workflow); - return acc; - }, []); + return shared.map((id) => id.workflowId.toString()); } - - return activeWorkflows.filter((workflow) => this.activationErrors[workflow.id] === undefined); } /** diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 1e0b168688892..f2157061f640d 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -93,6 +93,7 @@ export function getConnectionOptions(dbType: DatabaseType): ConnectionOptions { const sslCert = config.getEnv('database.postgresdb.ssl.cert'); const sslKey = config.getEnv('database.postgresdb.ssl.key'); const sslRejectUnauthorized = config.getEnv('database.postgresdb.ssl.rejectUnauthorized'); + process.env['PGSSLMODE'] = 'require'; let ssl: TlsOptions | undefined; if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) { diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index 9a4597f92a203..4d252d4ad6767 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -98,6 +98,7 @@ export class License { isFeatureEnabled(feature: string): boolean { if (!this.manager) { + getLogger().warn('License manager not initialized'); return false; } diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts index adb37dce57dd1..8ed5031628853 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts @@ -24,8 +24,6 @@ export async function getSharedWorkflowIds(user: User): Promise { select: ['workflowId'], }); return sharedWorkflows.map(({ workflowId }) => workflowId); - - return sharedWorkflows.map(({ workflowId }) => workflowId); } export async function getSharedWorkflow( diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 8739fe9ed166d..6ad8ac7da2cf6 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -764,8 +764,7 @@ export class Server extends AbstractServer { this.app.get( `/${this.restEndpoint}/active`, ResponseHelper.send(async (req: WorkflowRequest.GetAllActive) => { - const activeWorkflows = await this.activeWorkflowRunner.getActiveWorkflows(req.user); - return activeWorkflows.map(({ id }) => id); + return this.activeWorkflowRunner.getActiveWorkflows(req.user); }), ); diff --git a/packages/cli/src/UserManagement/PermissionChecker.ts b/packages/cli/src/UserManagement/PermissionChecker.ts index d2327159d4b54..b8b851419aec0 100644 --- a/packages/cli/src/UserManagement/PermissionChecker.ts +++ b/packages/cli/src/UserManagement/PermissionChecker.ts @@ -44,6 +44,7 @@ export class PermissionChecker { const workflowSharings = await Db.collections.SharedWorkflow.find({ relations: ['workflow'], where: { workflowId: workflow.id }, + select: ['userId'], }); workflowUserIds = workflowSharings.map((s) => s.userId); } diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index f19278700b86f..cdfd6cb65768e 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -610,16 +610,14 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { (workflowDidSucceed && saveDataSuccessExecution === 'none') || (!workflowDidSucceed && saveDataErrorExecution === 'none') ) { - if (!fullRunData.waitTill) { - if (!isManualMode) { - executeErrorWorkflow( - this.workflowData, - fullRunData, - this.mode, - this.executionId, - this.retryOf, - ); - } + if (!fullRunData.waitTill && !isManualMode) { + executeErrorWorkflow( + this.workflowData, + fullRunData, + this.mode, + this.executionId, + this.retryOf, + ); // Data is always saved, so we remove from database await Db.collections.Execution.delete(this.executionId); await BinaryDataManager.getInstance().markDataForDeletionByExecutionId( diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 2368918a5705d..b8d8c5a322475 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -19,6 +19,7 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import type { IExternalHooksClass } from '@/Interfaces'; import { InternalHooks } from '@/InternalHooks'; import { PostHogClient } from '@/posthog'; +import { License } from '@/License'; export const UM_FIX_INSTRUCTION = 'Please fix the database by running ./packages/cli/bin/n8n user-management:reset'; @@ -119,6 +120,28 @@ export abstract class BaseCommand extends Command { await this.externalHooks.init(); } + async initLicense(): Promise { + const license = Container.get(License); + await license.init(this.instanceId); + + const activationKey = config.getEnv('license.activationKey'); + + if (activationKey) { + const hasCert = (await license.loadCertStr()).length > 0; + + if (hasCert) { + return LoggerProxy.debug('Skipping license activation'); + } + + try { + LoggerProxy.debug('Attempting license activation'); + await license.activate(activationKey); + } catch (e) { + LoggerProxy.error('Could not activate license', e as Error); + } + } + } + async finally(error: Error | undefined) { if (inTest || this.id === 'start') return; if (Db.connectionState.connected) { diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 016fc48a9ef1b..82640335cdf14 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -28,7 +28,6 @@ import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants'; import { eventBus } from '@/eventbus'; import { BaseCommand } from './BaseCommand'; import { InternalHooks } from '@/InternalHooks'; -import { License } from '@/License'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const open = require('open'); @@ -186,28 +185,6 @@ export class Start extends BaseCommand { await Promise.all(files.map(compileFile)); } - async initLicense(): Promise { - const license = Container.get(License); - await license.init(this.instanceId); - - const activationKey = config.getEnv('license.activationKey'); - - if (activationKey) { - const hasCert = (await license.loadCertStr()).length > 0; - - if (hasCert) { - return LoggerProxy.debug('Skipping license activation'); - } - - try { - LoggerProxy.debug('Attempting license activation'); - await license.activate(activationKey); - } catch (e) { - LoggerProxy.error('Could not activate license', e as Error); - } - } - } - async init() { await this.initCrashJournal(); diff --git a/packages/cli/src/commands/webhook.ts b/packages/cli/src/commands/webhook.ts index da2fc44315ce6..a06ed770a9227 100644 --- a/packages/cli/src/commands/webhook.ts +++ b/packages/cli/src/commands/webhook.ts @@ -77,6 +77,7 @@ export class Webhook extends BaseCommand { await this.initCrashJournal(); await super.init(); + await this.initLicense(); await this.initBinaryManager(); await this.initExternalHooks(); } diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 48d2269d0e171..5463eca9f2467 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -224,6 +224,7 @@ export class Worker extends BaseCommand { await super.init(); this.logger.debug('Starting n8n worker...'); + await this.initLicense(); await this.initBinaryManager(); await this.initExternalHooks(); } diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 3492fe7c145df..f025b60eea0b2 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -80,7 +80,7 @@ export class AuthController { user = preliminaryUser; usedAuthenticationMethod = 'email'; } else { - throw new AuthError('SAML is enabled, please log in with SAML'); + throw new AuthError('SSO is enabled, please log in with SSO'); } } else if (isLdapCurrentAuthenticationMethod()) { user = await handleLdapLogin(email, password); diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 7f73c9229214a..2a5a5731a1c8a 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -15,7 +15,7 @@ }, "scripts": { "clean": "rimraf dist .turbo", - "build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build", + "build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=12288\" vite build", "typecheck": "vue-tsc --emitDeclarationOnly", "dev": "pnpm serve", "lint": "eslint --quiet --ext .js,.ts,.vue src", diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index d347b11b673aa..cbd8757126907 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -100,6 +100,7 @@ export default defineComponent({ message: this.$locale.baseText('startupError.message'), type: 'error', duration: 0, + dangerouslyUseHTMLString: true, }); throw e; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/index.ts b/packages/editor-ui/src/__tests__/server/endpoints/index.ts index 4ba3315e425fb..d79a81d30883b 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/index.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/index.ts @@ -4,6 +4,7 @@ import { routesForCredentials } from './credential'; import { routesForCredentialTypes } from './credentialType'; import { routesForVariables } from './variable'; import { routesForSettings } from './settings'; +import { routesForSSO } from './sso'; const endpoints: Array<(server: Server) => void> = [ routesForCredentials, @@ -11,6 +12,7 @@ const endpoints: Array<(server: Server) => void> = [ routesForUsers, routesForVariables, routesForSettings, + routesForSSO, ]; export { endpoints }; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/sso.ts b/packages/editor-ui/src/__tests__/server/endpoints/sso.ts new file mode 100644 index 0000000000000..61896ca1f7426 --- /dev/null +++ b/packages/editor-ui/src/__tests__/server/endpoints/sso.ts @@ -0,0 +1,36 @@ +import type { Server, Request } from 'miragejs'; +import { Response } from 'miragejs'; +import type { SamlPreferences, SamlPreferencesExtractedData } from '@/Interface'; +import { faker } from '@faker-js/faker'; +import type { AppSchema } from '@/__tests__/server/types'; +import { jsonParse } from 'n8n-workflow'; + +let samlConfig: SamlPreferences & SamlPreferencesExtractedData = { + metadata: '', + metadataUrl: '', + entityID: faker.internet.url(), + returnUrl: faker.internet.url(), +}; + +export function routesForSSO(server: Server) { + server.get('/rest/sso/saml/config', () => { + return new Response(200, {}, { data: samlConfig }); + }); + + server.post('/rest/sso/saml/config', (schema: AppSchema, request: Request) => { + const requestBody = jsonParse(request.requestBody) as Partial< + SamlPreferences & SamlPreferencesExtractedData + >; + + samlConfig = { + ...samlConfig, + ...requestBody, + }; + + return new Response(200, {}, { data: samlConfig }); + }); + + server.get('/rest/sso/saml/config/test', () => { + return new Response(200, {}, { data: '' }); + }); +} diff --git a/packages/editor-ui/src/__tests__/utils.ts b/packages/editor-ui/src/__tests__/utils.ts index f6f779709072a..9fa734192de18 100644 --- a/packages/editor-ui/src/__tests__/utils.ts +++ b/packages/editor-ui/src/__tests__/utils.ts @@ -3,7 +3,7 @@ import { UserManagementAuthenticationMethod } from '@/Interface'; import { render } from '@testing-library/vue'; import { PiniaVuePlugin } from 'pinia'; -export const retry = async (assertion: () => any, { interval = 20, timeout = 200 } = {}) => { +export const retry = async (assertion: () => any, { interval = 20, timeout = 1000 } = {}) => { return new Promise((resolve, reject) => { const startTime = Date.now(); diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index 22970613a3b9f..0dc0e8b3c33e3 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -302,6 +302,7 @@ export default defineComponent({ title: this.$locale.baseText('dataMapping.success.title'), message: this.$locale.baseText('dataMapping.success.moreInfo'), type: 'success', + dangerouslyUseHTMLString: true, }); this.ndvStore.disableMappingHint(); diff --git a/packages/editor-ui/src/components/WorkflowActivator.vue b/packages/editor-ui/src/components/WorkflowActivator.vue index 4c45ef080ae17..e5789529e12f6 100644 --- a/packages/editor-ui/src/components/WorkflowActivator.vue +++ b/packages/editor-ui/src/components/WorkflowActivator.vue @@ -132,6 +132,7 @@ export default defineComponent({ message: errorMessage, type: 'warning', duration: 0, + dangerouslyUseHTMLString: true, }); }, }, diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index 8d8cb39b0be4a..dd35729124bad 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -391,6 +391,7 @@ export default defineComponent({ cancelButtonText: this.$locale.baseText( 'workflows.shareModal.list.delete.confirm.cancelButtonText', ), + dangerouslyUseHTMLString: true, }, ); diff --git a/packages/editor-ui/src/composables/useToast.ts b/packages/editor-ui/src/composables/useToast.ts index 495c5687c28f8..5b46d0a2da0fa 100644 --- a/packages/editor-ui/src/composables/useToast.ts +++ b/packages/editor-ui/src/composables/useToast.ts @@ -56,6 +56,7 @@ export function useToast() { customClass?: string; closeOnClick?: boolean; type?: MessageType; + dangerouslyUseHTMLString?: boolean; }) { // eslint-disable-next-line prefer-const let notification: ElNotificationComponent; @@ -80,6 +81,7 @@ export function useToast() { duration: config.duration, customClass: config.customClass, type: config.type, + dangerouslyUseHTMLString: config.dangerouslyUseHTMLString ?? true, }); return notification; diff --git a/packages/editor-ui/src/mixins/genericHelpers.ts b/packages/editor-ui/src/mixins/genericHelpers.ts index acb12a66bd0d5..a93da5e24a970 100644 --- a/packages/editor-ui/src/mixins/genericHelpers.ts +++ b/packages/editor-ui/src/mixins/genericHelpers.ts @@ -57,6 +57,7 @@ export const genericHelpers = defineComponent({ message: this.$locale.baseText('genericHelpers.showMessage.message'), type: 'info', duration: 0, + dangerouslyUseHTMLString: true, }); return false; diff --git a/packages/editor-ui/src/mixins/pushConnection.ts b/packages/editor-ui/src/mixins/pushConnection.ts index 8c70aed0febeb..4f2110dfadcbe 100644 --- a/packages/editor-ui/src/mixins/pushConnection.ts +++ b/packages/editor-ui/src/mixins/pushConnection.ts @@ -438,6 +438,7 @@ export const pushConnection = defineComponent({ message: runDataExecutedErrorMessage, type: 'error', duration: 0, + dangerouslyUseHTMLString: true, }); } } else { diff --git a/packages/editor-ui/src/mixins/workflowHelpers.ts b/packages/editor-ui/src/mixins/workflowHelpers.ts index 914f1dc0bb7d6..1813146d21fda 100644 --- a/packages/editor-ui/src/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/mixins/workflowHelpers.ts @@ -757,6 +757,7 @@ export const workflowHelpers = defineComponent({ }), this.$locale.baseText('workflows.concurrentChanges.confirmMessage.title'), { + dangerouslyUseHTMLString: true, confirmButtonText: this.$locale.baseText( 'workflows.concurrentChanges.confirmMessage.confirmButtonText', ), diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 20c5415fbe0a6..5ee88b6760999 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1216,7 +1216,7 @@ "settings.log-streaming.actionBox.title": "Available on Enterprise plan", "settings.log-streaming.actionBox.description": "Log Streaming is available as a paid feature. Learn more about it.", "settings.log-streaming.actionBox.button": "See plans", - "settings.log-streaming.infoText": "Send logs to external endpoints of your choice. You can also write logs to a file or the console using environment variables. More info", + "settings.log-streaming.infoText": "Send logs to external endpoints of your choice. You can also write logs to a file or the console using environment variables. More info", "settings.log-streaming.addFirstTitle": "Set up a destination to get started", "settings.log-streaming.addFirst": "Add your first destination by clicking on the button and selecting a destination type.", "settings.log-streaming.saving": "Saving", @@ -1797,22 +1797,32 @@ "settings.ldap.section.synchronization.title": "Synchronization", "settings.sso": "SSO", "settings.sso.title": "Single Sign On", - "settings.sso.subtitle": "SAML 2.0", - "settings.sso.info": "SAML SSO (Security Assertion Markup Language Single Sign-On) is a type of authentication process that enables users to access multiple applications with a single set of login credentials. {link}", - "settings.sso.info.link": "More info.", + "settings.sso.subtitle": "SAML 2.0 Configuration", + "settings.sso.info": "Activate SAML SSO to enable passwordless login via your existing user management tool and enhance security through unified authentication.", + "settings.sso.info.link": "Learn how to configure SAML 2.0.", "settings.sso.activation.tooltip": "You need to save the settings first before activating SAML", "settings.sso.activated": "Activated", "settings.sso.deactivated": "Deactivated", "settings.sso.settings.redirectUrl.label": "Redirect URL", "settings.sso.settings.redirectUrl.copied": "Redirect URL copied to clipboard", - "settings.sso.settings.redirectUrl.help": "Save the Redirect URL as you’ll need it to configure these in the SAML provider’s settings.", + "settings.sso.settings.redirectUrl.help": "Copy the Redirect URL to configure your SAML provider", "settings.sso.settings.entityId.label": "Entity ID", "settings.sso.settings.entityId.copied": "Entity ID copied to clipboard", - "settings.sso.settings.entityId.help": "Save the Entity URL as you’ll need it to configure these in the SAML provider’s settings.", + "settings.sso.settings.entityId.help": "Copy the Entity ID URL to configure your SAML provider", "settings.sso.settings.ips.label": "Identity Provider Settings", - "settings.sso.settings.ips.help": "Add the raw Metadata XML provided by your Identity Provider", + "settings.sso.settings.ips.xml.help": "Paste here the raw Metadata XML provided by your Identity Provider", + "settings.sso.settings.ips.url.help": "Paste here the Internet Provider Metadata URL", + "settings.sso.settings.ips.url.placeholder": "e.g. https://samltest.id/saml/idp", + "settings.sso.settings.ips.options.url": "Metadata URL", + "settings.sso.settings.ips.options.xml": "XML", "settings.sso.settings.test": "Test settings", "settings.sso.settings.save": "Save settings", + "settings.sso.settings.save.activate.title": "Test and activate SAML SSO", + "settings.sso.settings.save.activate.message": "SAML SSO configuration saved successfully. Test your SAML SSO settings first, then activate to enable single sign-on for your organization.", + "settings.sso.settings.save.activate.cancel": "Cancel", + "settings.sso.settings.save.activate.test": "Test settings", + "settings.sso.settings.save.error": "Error saving SAML SSO configuration", + "settings.sso.settings.footer.hint": "Don't forget to activate SAML SSO once you've saved the settings.", "settings.sso.actionBox.title": "Available on Enterprise plan", "settings.sso.actionBox.description": "Use Single Sign On to consolidate authentication into a single platform to improve security and agility.", "settings.sso.actionBox.buttonText": "See plans", diff --git a/packages/editor-ui/src/stores/sso.store.ts b/packages/editor-ui/src/stores/sso.store.ts index 27b21de7f5a92..be8b90c2f9942 100644 --- a/packages/editor-ui/src/stores/sso.store.ts +++ b/packages/editor-ui/src/stores/sso.store.ts @@ -6,6 +6,7 @@ import { useSettingsStore } from '@/stores/settings.store'; import * as ssoApi from '@/api/sso'; import type { SamlPreferences } from '@/Interface'; import { updateCurrentUser } from '@/api/users'; +import type { SamlPreferencesExtractedData } from '@/Interface'; import { useUsersStore } from '@/stores/users.store'; export const useSSOStore = defineStore('sso', () => { @@ -15,10 +16,13 @@ export const useSSOStore = defineStore('sso', () => { const state = reactive({ loading: false, + samlConfig: undefined as (SamlPreferences & SamlPreferencesExtractedData) | undefined, }); const isLoading = computed(() => state.loading); + const samlConfig = computed(() => state.samlConfig); + const setLoading = (loading: boolean) => { state.loading = loading; }; @@ -56,7 +60,11 @@ export const useSSOStore = defineStore('sso', () => { ssoApi.toggleSamlConfig(rootStore.getRestApiContext, { loginEnabled: enabled }); const getSamlMetadata = async () => ssoApi.getSamlMetadata(rootStore.getRestApiContext); - const getSamlConfig = async () => ssoApi.getSamlConfig(rootStore.getRestApiContext); + const getSamlConfig = async () => { + const samlConfig = await ssoApi.getSamlConfig(rootStore.getRestApiContext); + state.samlConfig = samlConfig; + return samlConfig; + }; const saveSamlConfig = async (config: SamlPreferences) => ssoApi.saveSamlConfig(rootStore.getRestApiContext, config); const testSamlConfig = async () => ssoApi.testSamlConfig(rootStore.getRestApiContext); @@ -77,6 +85,7 @@ export const useSSOStore = defineStore('sso', () => { isEnterpriseSamlEnabled, isDefaultAuthenticationSaml, showSsoLoginButton, + samlConfig, getSSORedirectUrl, getSamlMetadata, getSamlConfig, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index f60fb0cea689c..2433fcf286e52 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -676,6 +676,7 @@ export default defineComponent({ // Close the creator panel if user clicked on the link if (this.createNodeActive) notice.close(); }, 0), + dangerouslyUseHTMLString: true, }); }, clearExecutionData() { @@ -1449,6 +1450,7 @@ export default defineComponent({ cancelButtonText: this.$locale.baseText( 'nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText', ), + dangerouslyUseHTMLString: true, }, ); @@ -3775,6 +3777,12 @@ export default defineComponent({ this.disableNodes([node]); } }, + onPageShow(e: PageTransitionEvent) { + // Page was restored from the bfcache (back-forward cache) + if (e.persisted) { + this.stopLoading(); + } + }, }, async mounted() { this.resetWorkspace(); @@ -3878,6 +3886,7 @@ export default defineComponent({ document.addEventListener('keydown', this.keyDown); document.addEventListener('keyup', this.keyUp); window.addEventListener('message', this.onPostMessageReceived); + window.addEventListener('pageshow', this.onPageShow); this.$root.$on('newWorkflow', this.newWorkflow); this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent); @@ -3902,6 +3911,7 @@ export default defineComponent({ document.removeEventListener('keyup', this.keyUp); window.removeEventListener('message', this.onPostMessageReceived); window.removeEventListener('beforeunload', this.onBeforeUnload); + window.removeEventListener('pageshow', this.onPageShow); this.$root.$off('newWorkflow', this.newWorkflow); this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent); diff --git a/packages/editor-ui/src/views/SettingsSso.vue b/packages/editor-ui/src/views/SettingsSso.vue index 0365378f2b9bb..4e7b045bb6521 100644 --- a/packages/editor-ui/src/views/SettingsSso.vue +++ b/packages/editor-ui/src/views/SettingsSso.vue @@ -1,55 +1,120 @@