mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'n8n-io:master' into MongoDB_vector_store
This commit is contained in:
commit
afe56adc0e
52
CHANGELOG.md
52
CHANGELOG.md
|
@ -1,3 +1,55 @@
|
||||||
|
# [1.82.0](https://github.com/n8n-io/n8n/compare/n8n@1.81.0...n8n@1.82.0) (2025-03-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Call n8n Workflow Tool Node:** Support concurrent invocations of the tool ([#13526](https://github.com/n8n-io/n8n/issues/13526)) ([5334661](https://github.com/n8n-io/n8n/commit/5334661b76909f48aa4e45af889e6180c025eed6))
|
||||||
|
* **core:** Gracefully handle missing tasks metadata ([#13632](https://github.com/n8n-io/n8n/issues/13632)) ([999fb81](https://github.com/n8n-io/n8n/commit/999fb8174ae6bb34354cb8c6f85f769cb64e8ae4))
|
||||||
|
* **core:** Remove `index.html` caching entirely ([#13563](https://github.com/n8n-io/n8n/issues/13563)) ([afba8f9](https://github.com/n8n-io/n8n/commit/afba8f9ff89054d54e1cf70070ae5710bc9ddd37))
|
||||||
|
* **editor:** Add workflows to the store when fetching current page ([#13583](https://github.com/n8n-io/n8n/issues/13583)) ([c4f3293](https://github.com/n8n-io/n8n/commit/c4f329377828d80a54b71f5733ea7d9b4ee91f48))
|
||||||
|
* **editor:** Ai 672 minor UI fixes on evaluation creation ([#13461](https://github.com/n8n-io/n8n/issues/13461)) ([b791677](https://github.com/n8n-io/n8n/commit/b791677ffa8c82161c4c40b65bc62d93f2e7bc9e))
|
||||||
|
* **editor:** Ai 675 minor tweaks to tests list ([#13467](https://github.com/n8n-io/n8n/issues/13467)) ([5ad950f](https://github.com/n8n-io/n8n/commit/5ad950f60371546414ff17eb31171f2259e70f57))
|
||||||
|
* **editor:** Don't show duplicate logs when tree is deeply nested ([#13537](https://github.com/n8n-io/n8n/issues/13537)) ([d550382](https://github.com/n8n-io/n8n/commit/d550382a4a43c54cae47e9071236aa18efe38a5d))
|
||||||
|
* **editor:** Fix browser crash with large execution result ([#13580](https://github.com/n8n-io/n8n/issues/13580)) ([1c8c7e3](https://github.com/n8n-io/n8n/commit/1c8c7e34f9d2c8363c441aeb8c562ac91088a687))
|
||||||
|
* **editor:** Fix github star button layout ([#13630](https://github.com/n8n-io/n8n/issues/13630)) ([139b5b3](https://github.com/n8n-io/n8n/commit/139b5b378daba6df18639eeb4f326edce7752e11))
|
||||||
|
* **editor:** Fix icon color on 'Call n8n Workflow Tool' node ([#13568](https://github.com/n8n-io/n8n/issues/13568)) ([90d0943](https://github.com/n8n-io/n8n/commit/90d09431af97570a3a6adfb0470a18681af28001))
|
||||||
|
* **editor:** Fix icon spacing in accordion title ([#13539](https://github.com/n8n-io/n8n/issues/13539)) ([ebaaf0e](https://github.com/n8n-io/n8n/commit/ebaaf0e3d9602052f76f61b90fb073e390896cea))
|
||||||
|
* **editor:** Fix keyboard shortcuts no longer working after editing sticky note ([#13502](https://github.com/n8n-io/n8n/issues/13502)) ([ab41fc3](https://github.com/n8n-io/n8n/commit/ab41fc3fb5f15e9c7ce7279b46cec90a511d0e0d))
|
||||||
|
* **editor:** Fix workflows list status filter ([#13621](https://github.com/n8n-io/n8n/issues/13621)) ([4067fb0](https://github.com/n8n-io/n8n/commit/4067fb0b12d242c795c6598df6c4090d48cec7b1))
|
||||||
|
* **editor:** Hide fromAI button in old workflow tool ([#13552](https://github.com/n8n-io/n8n/issues/13552)) ([6ef8d34](https://github.com/n8n-io/n8n/commit/6ef8d34f969ddb9e80b82dc50b38698249089af2))
|
||||||
|
* **editor:** Parse out nodeType ([#13474](https://github.com/n8n-io/n8n/issues/13474)) ([1cd13b6](https://github.com/n8n-io/n8n/commit/1cd13b639efcfabf183740bb6634023c66d5ce99))
|
||||||
|
* **editor:** Show dropdown scrollbars only when appropriate ([#13562](https://github.com/n8n-io/n8n/issues/13562)) ([615a42a](https://github.com/n8n-io/n8n/commit/615a42afd52d0d95dd30ed9aa231b9921e0708fe))
|
||||||
|
* **editor:** Show JSON full-screen Editor Window in Full Height ([#13350](https://github.com/n8n-io/n8n/issues/13350)) ([46dcce3](https://github.com/n8n-io/n8n/commit/46dcce341fbfa1c2a44a08f3dc93f1f8f16808c8))
|
||||||
|
* **editor:** Show scrollbar in Element UI popup ([#13259](https://github.com/n8n-io/n8n/issues/13259)) ([c021a7e](https://github.com/n8n-io/n8n/commit/c021a7e4b2daccc59541bab25c1447339dd68c09))
|
||||||
|
* **editor:** Undo keybinding changes related to window focus/blur events ([#13559](https://github.com/n8n-io/n8n/issues/13559)) ([6ddcc1f](https://github.com/n8n-io/n8n/commit/6ddcc1f8c93f86b0d111cae1b24518d621d8fe84))
|
||||||
|
* **Odoo Node:** Model and fields dynamic fetching errors ([#13511](https://github.com/n8n-io/n8n/issues/13511)) ([294f019](https://github.com/n8n-io/n8n/commit/294f0194145ca4139d9d9cea0729bf83d0871c94))
|
||||||
|
* **Postgres Node:** Accommodate null values in query parameters for expressions ([#13544](https://github.com/n8n-io/n8n/issues/13544)) ([6c266ac](https://github.com/n8n-io/n8n/commit/6c266acced95500148532b4fc015fe5d9587db76))
|
||||||
|
* **QuickBooks Online Node:** Add qty to quickbooks invoice line details ([#13602](https://github.com/n8n-io/n8n/issues/13602)) ([7c4e2f0](https://github.com/n8n-io/n8n/commit/7c4e2f014c0b38935a4d661646e773ad26fc97e1))
|
||||||
|
* **seven Node:** Remove obsolete options and fix typos ([#13122](https://github.com/n8n-io/n8n/issues/13122)) ([d02c8b0](https://github.com/n8n-io/n8n/commit/d02c8b0d7dbd4144c954a66aa0e78e43122b6e9a))
|
||||||
|
* **Switch Node:** Fix an issue in ordering rules in Switch Node ([#13476](https://github.com/n8n-io/n8n/issues/13476)) ([0fb6607](https://github.com/n8n-io/n8n/commit/0fb66076ba6120a7cb2401102ff8d1d6220ae106))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Anthropic Chat Model Node:** Fetch models dynamically & support thinking ([#13543](https://github.com/n8n-io/n8n/issues/13543)) ([461df37](https://github.com/n8n-io/n8n/commit/461df371f76b9dee9916a985a2bd2197facbcf6b))
|
||||||
|
* **Azure Storage Node:** New node ([#12536](https://github.com/n8n-io/n8n/issues/12536)) ([727f6f3](https://github.com/n8n-io/n8n/commit/727f6f3c0e5cef2d0cd4cd1ef1c6fa8f4d3f69ec))
|
||||||
|
* **core:** Add metric for active workflow count ([#13420](https://github.com/n8n-io/n8n/issues/13420)) ([3aa679e](https://github.com/n8n-io/n8n/commit/3aa679e4ac411d0d34e039fa6c43bc98f2e3670f))
|
||||||
|
* **core:** Fix partial workflow execution with specific trigger data ([#13505](https://github.com/n8n-io/n8n/issues/13505)) ([9029dac](https://github.com/n8n-io/n8n/commit/9029dace5c682e4b5df4f18f2f51098dce6436e5))
|
||||||
|
* **core:** Make Tools Agent the default Agent type, deprecate other agent types ([#13459](https://github.com/n8n-io/n8n/issues/13459)) ([a60d106](https://github.com/n8n-io/n8n/commit/a60d106ebb4fb71e80f90a17965d7fb79d7806c6))
|
||||||
|
* **core:** Support executing single nodes not part of a graph as a partial execution ([#13529](https://github.com/n8n-io/n8n/issues/13529)) ([8a34f02](https://github.com/n8n-io/n8n/commit/8a34f027c531f0d37fc8088c13d7e289cd8897ce))
|
||||||
|
* **editor:** Add functionality to create folders ([#13473](https://github.com/n8n-io/n8n/issues/13473)) ([2cb9d9e](https://github.com/n8n-io/n8n/commit/2cb9d9e29fc961a417d06c1449b79d4a0a66658e))
|
||||||
|
* **editor:** Automatically tidy up workflows ([#13471](https://github.com/n8n-io/n8n/issues/13471)) ([f381a24](https://github.com/n8n-io/n8n/commit/f381a24145271f4df4fa5c9345bb12c984f6e1fc))
|
||||||
|
* **editor:** Indicate dirty nodes with yellow borders/connectors on canvas ([#13040](https://github.com/n8n-io/n8n/issues/13040)) ([75493ef](https://github.com/n8n-io/n8n/commit/75493ef6ef4ee47d0ccf217cd5c2e58754f60c12))
|
||||||
|
* **editor:** Rename 'In-Memory Vector Store' to 'Simple Vector Store' ([#13472](https://github.com/n8n-io/n8n/issues/13472)) ([35c00d0](https://github.com/n8n-io/n8n/commit/35c00d0c846e8a1e214aea3690ea60ff80d03eed))
|
||||||
|
* **editor:** Rename 'Window Buffer Memory' to 'Simple Memory' ([#13477](https://github.com/n8n-io/n8n/issues/13477)) ([819fc2d](https://github.com/n8n-io/n8n/commit/819fc2da63ce7f06d4702bce698d382eb64c45a3))
|
||||||
|
* Hackmation - automatically switch to expression mode ([#13213](https://github.com/n8n-io/n8n/issues/13213)) ([6953b0d](https://github.com/n8n-io/n8n/commit/6953b0d53a28448022c9de0a2f6294c9390a3b48))
|
||||||
|
* **n8n Form Trigger Node, Chat Trigger Node:** Allow to customize form and chat css ([#13506](https://github.com/n8n-io/n8n/issues/13506)) ([289041e](https://github.com/n8n-io/n8n/commit/289041e997eedb660356cdbd259660b7c3117194))
|
||||||
|
* **n8n Vertica credentials only Node:** New node ([#12256](https://github.com/n8n-io/n8n/issues/12256)) ([d3fe3de](https://github.com/n8n-io/n8n/commit/d3fe3dea32207dfdb2a43db0def96466a31daa66))
|
||||||
|
* Update AWS credential to support more regions ([#13524](https://github.com/n8n-io/n8n/issues/13524)) ([b50658c](https://github.com/n8n-io/n8n/commit/b50658cbc64c0a6fc000b11dca0cca49cc707471))
|
||||||
|
* WhatsApp Business Cloud Node - new operation sendAndWait ([#12941](https://github.com/n8n-io/n8n/issues/12941)) ([97defb3](https://github.com/n8n-io/n8n/commit/97defb3a833bb269a4a3fc573a8e250a0d0e0deb))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.81.0](https://github.com/n8n-io/n8n/compare/n8n@1.80.0...n8n@1.81.0) (2025-02-24)
|
# [1.81.0](https://github.com/n8n-io/n8n/compare/n8n@1.80.0...n8n@1.81.0) (2025-02-24)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
|
||||||
|
|
||||||
it('should login and logout', () => {
|
it('should login and logout', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get('input[name="email"]').type(INSTANCE_OWNER.email);
|
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_OWNER.email);
|
||||||
cy.get('input[name="password"]').type(INSTANCE_OWNER.password);
|
cy.get('input[name="password"]').type(INSTANCE_OWNER.password);
|
||||||
cy.getByTestId('form-submit-button').click();
|
cy.getByTestId('form-submit-button').click();
|
||||||
mainSidebar.getters.logo().should('be.visible');
|
mainSidebar.getters.logo().should('be.visible');
|
||||||
|
@ -47,7 +47,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
|
||||||
mainSidebar.actions.openUserMenu();
|
mainSidebar.actions.openUserMenu();
|
||||||
cy.getByTestId('user-menu-item-logout').click();
|
cy.getByTestId('user-menu-item-logout').click();
|
||||||
|
|
||||||
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
|
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_MEMBERS[0].email);
|
||||||
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
||||||
cy.getByTestId('form-submit-button').click();
|
cy.getByTestId('form-submit-button').click();
|
||||||
mainSidebar.getters.logo().should('be.visible');
|
mainSidebar.getters.logo().should('be.visible');
|
||||||
|
|
|
@ -506,7 +506,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
||||||
mainSidebar.actions.openUserMenu();
|
mainSidebar.actions.openUserMenu();
|
||||||
cy.getByTestId('user-menu-item-logout').click();
|
cy.getByTestId('user-menu-item-logout').click();
|
||||||
|
|
||||||
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
|
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_MEMBERS[0].email);
|
||||||
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
||||||
cy.getByTestId('form-submit-button').click();
|
cy.getByTestId('form-submit-button').click();
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class SigninPage extends BasePage {
|
||||||
|
|
||||||
getters = {
|
getters = {
|
||||||
form: () => cy.getByTestId('auth-form'),
|
form: () => cy.getByTestId('auth-form'),
|
||||||
email: () => cy.getByTestId('email'),
|
email: () => cy.getByTestId('emailOrLdapLoginId'),
|
||||||
password: () => cy.getByTestId('password'),
|
password: () => cy.getByTestId('password'),
|
||||||
submit: () => cy.get('button'),
|
submit: () => cy.get('button'),
|
||||||
};
|
};
|
||||||
|
|
|
@ -69,7 +69,7 @@ Cypress.Commands.add('signin', ({ email, password }) => {
|
||||||
.request({
|
.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `${BACKEND_BASE_URL}/rest/login`,
|
url: `${BACKEND_BASE_URL}/rest/login`,
|
||||||
body: { email, password },
|
body: { emailOrLdapLoginId: email, password },
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-monorepo",
|
"name": "n8n-monorepo",
|
||||||
"version": "1.81.0",
|
"version": "1.82.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.15",
|
"node": ">=20.15",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/api-types",
|
"name": "@n8n/api-types",
|
||||||
"version": "0.16.0",
|
"version": "0.17.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -6,7 +6,7 @@ describe('LoginRequestDto', () => {
|
||||||
{
|
{
|
||||||
name: 'complete valid login request',
|
name: 'complete valid login request',
|
||||||
request: {
|
request: {
|
||||||
email: 'test@example.com',
|
emailOrLdapLoginId: 'test@example.com',
|
||||||
password: 'securePassword123',
|
password: 'securePassword123',
|
||||||
mfaCode: '123456',
|
mfaCode: '123456',
|
||||||
},
|
},
|
||||||
|
@ -14,14 +14,14 @@ describe('LoginRequestDto', () => {
|
||||||
{
|
{
|
||||||
name: 'login request without optional MFA',
|
name: 'login request without optional MFA',
|
||||||
request: {
|
request: {
|
||||||
email: 'test@example.com',
|
emailOrLdapLoginId: 'test@example.com',
|
||||||
password: 'securePassword123',
|
password: 'securePassword123',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'login request with both mfaCode and mfaRecoveryCode',
|
name: 'login request with both mfaCode and mfaRecoveryCode',
|
||||||
request: {
|
request: {
|
||||||
email: 'test@example.com',
|
emailOrLdapLoginId: 'test@example.com',
|
||||||
password: 'securePassword123',
|
password: 'securePassword123',
|
||||||
mfaCode: '123456',
|
mfaCode: '123456',
|
||||||
mfaRecoveryCode: 'recovery-code-123',
|
mfaRecoveryCode: 'recovery-code-123',
|
||||||
|
@ -30,7 +30,7 @@ describe('LoginRequestDto', () => {
|
||||||
{
|
{
|
||||||
name: 'login request with only mfaRecoveryCode',
|
name: 'login request with only mfaRecoveryCode',
|
||||||
request: {
|
request: {
|
||||||
email: 'test@example.com',
|
emailOrLdapLoginId: 'test@example.com',
|
||||||
password: 'securePassword123',
|
password: 'securePassword123',
|
||||||
mfaRecoveryCode: 'recovery-code-123',
|
mfaRecoveryCode: 'recovery-code-123',
|
||||||
},
|
},
|
||||||
|
@ -44,43 +44,35 @@ describe('LoginRequestDto', () => {
|
||||||
describe('Invalid requests', () => {
|
describe('Invalid requests', () => {
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
name: 'invalid email',
|
name: 'invalid emailOrLdapLoginId',
|
||||||
request: {
|
request: {
|
||||||
email: 'invalid-email',
|
emailOrLdapLoginId: 0,
|
||||||
password: 'securePassword123',
|
password: 'securePassword123',
|
||||||
},
|
},
|
||||||
expectedErrorPath: ['email'],
|
expectedErrorPath: ['emailOrLdapLoginId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'empty password',
|
name: 'empty password',
|
||||||
request: {
|
request: {
|
||||||
email: 'test@example.com',
|
emailOrLdapLoginId: 'test@example.com',
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
expectedErrorPath: ['password'],
|
expectedErrorPath: ['password'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'missing email',
|
name: 'missing emailOrLdapLoginId',
|
||||||
request: {
|
request: {
|
||||||
password: 'securePassword123',
|
password: 'securePassword123',
|
||||||
},
|
},
|
||||||
expectedErrorPath: ['email'],
|
expectedErrorPath: ['emailOrLdapLoginId'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'missing password',
|
name: 'missing password',
|
||||||
request: {
|
request: {
|
||||||
email: 'test@example.com',
|
emailOrLdapLoginId: 'test@example.com',
|
||||||
},
|
},
|
||||||
expectedErrorPath: ['password'],
|
expectedErrorPath: ['password'],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'whitespace in email and password',
|
|
||||||
request: {
|
|
||||||
email: ' test@example.com ',
|
|
||||||
password: ' securePassword123 ',
|
|
||||||
},
|
|
||||||
expectedErrorPath: ['email'],
|
|
||||||
},
|
|
||||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||||
const result = LoginRequestDto.safeParse(request);
|
const result = LoginRequestDto.safeParse(request);
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
|
|
@ -2,7 +2,12 @@ import { z } from 'zod';
|
||||||
import { Z } from 'zod-class';
|
import { Z } from 'zod-class';
|
||||||
|
|
||||||
export class LoginRequestDto extends Z.class({
|
export class LoginRequestDto extends Z.class({
|
||||||
email: z.string().email(),
|
/*
|
||||||
|
* The LDAP username does not need to be an email, so email validation
|
||||||
|
* is not enforced here. The controller determines whether this is an
|
||||||
|
* email and validates when LDAP is disabled
|
||||||
|
*/
|
||||||
|
emailOrLdapLoginId: z.string().trim(),
|
||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
mfaCode: z.string().optional(),
|
mfaCode: z.string().optional(),
|
||||||
mfaRecoveryCode: z.string().optional(),
|
mfaRecoveryCode: z.string().optional(),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/n8n-benchmark",
|
"name": "@n8n/n8n-benchmark",
|
||||||
"version": "1.11.0",
|
"version": "1.12.0",
|
||||||
"description": "Cli for running benchmark tests for n8n",
|
"description": "Cli for running benchmark tests for n8n",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/client-oauth2",
|
"name": "@n8n/client-oauth2",
|
||||||
"version": "0.22.0",
|
"version": "0.23.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/config",
|
"name": "@n8n/config",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/di",
|
"name": "@n8n/di",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/imap",
|
"name": "@n8n/imap",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/json-schema-to-zod",
|
"name": "@n8n/json-schema-to-zod",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "Converts JSON schema objects into Zod schemas",
|
"description": "Converts JSON schema objects into Zod schemas",
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"main": "./dist/cjs/index.js",
|
"main": "./dist/cjs/index.js",
|
||||||
|
|
|
@ -28,7 +28,7 @@ export class ToolWorkflow extends VersionedNodeType {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVersion: 2,
|
defaultVersion: 2.1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||||
|
@ -37,6 +37,7 @@ export class ToolWorkflow extends VersionedNodeType {
|
||||||
1.2: new ToolWorkflowV1(baseDescription),
|
1.2: new ToolWorkflowV1(baseDescription),
|
||||||
1.3: new ToolWorkflowV1(baseDescription),
|
1.3: new ToolWorkflowV1(baseDescription),
|
||||||
2: new ToolWorkflowV2(baseDescription),
|
2: new ToolWorkflowV2(baseDescription),
|
||||||
|
2.1: new ToolWorkflowV2(baseDescription),
|
||||||
};
|
};
|
||||||
super(nodeVersions, baseDescription);
|
super(nodeVersions, baseDescription);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,9 @@ export class ToolWorkflowV2 implements INodeType {
|
||||||
};
|
};
|
||||||
|
|
||||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||||
const workflowToolService = new WorkflowToolService(this);
|
const returnAllItems = this.getNode().typeVersion > 2;
|
||||||
|
|
||||||
|
const workflowToolService = new WorkflowToolService(this, { returnAllItems });
|
||||||
const name = this.getNodeParameter('name', itemIndex) as string;
|
const name = this.getNodeParameter('name', itemIndex) as string;
|
||||||
const description = this.getNodeParameter('description', itemIndex) as string;
|
const description = this.getNodeParameter('description', itemIndex) as string;
|
||||||
|
|
||||||
|
|
|
@ -187,6 +187,66 @@ describe('WorkflowTool::WorkflowToolService', () => {
|
||||||
expect(result.subExecutionId).toBe('test-execution');
|
expect(result.subExecutionId).toBe('test-execution');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should successfully execute workflow and return first item of many', async () => {
|
||||||
|
const workflowInfo = { id: 'test-workflow' };
|
||||||
|
const items: INodeExecutionData[] = [];
|
||||||
|
const workflowProxyMock = {
|
||||||
|
$execution: { id: 'exec-id' },
|
||||||
|
$workflow: { id: 'workflow-id' },
|
||||||
|
} as unknown as IWorkflowDataProxyData;
|
||||||
|
|
||||||
|
const TEST_RESPONSE_1 = { msg: 'test response 1' };
|
||||||
|
const TEST_RESPONSE_2 = { msg: 'test response 2' };
|
||||||
|
|
||||||
|
const mockResponse: ExecuteWorkflowData = {
|
||||||
|
data: [[{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]],
|
||||||
|
executionId: 'test-execution',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await service['executeSubWorkflow'](
|
||||||
|
context,
|
||||||
|
workflowInfo,
|
||||||
|
items,
|
||||||
|
workflowProxyMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.response).toBe(TEST_RESPONSE_1);
|
||||||
|
expect(result.subExecutionId).toBe('test-execution');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully execute workflow and return all items', async () => {
|
||||||
|
const serviceWithReturnAllItems = new WorkflowToolService(context, { returnAllItems: true });
|
||||||
|
const workflowInfo = { id: 'test-workflow' };
|
||||||
|
const items: INodeExecutionData[] = [];
|
||||||
|
const workflowProxyMock = {
|
||||||
|
$execution: { id: 'exec-id' },
|
||||||
|
$workflow: { id: 'workflow-id' },
|
||||||
|
} as unknown as IWorkflowDataProxyData;
|
||||||
|
|
||||||
|
const TEST_RESPONSE_1 = { msg: 'test response 1' };
|
||||||
|
const TEST_RESPONSE_2 = { msg: 'test response 2' };
|
||||||
|
|
||||||
|
const mockResponse: ExecuteWorkflowData = {
|
||||||
|
data: [[{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]],
|
||||||
|
executionId: 'test-execution',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await serviceWithReturnAllItems['executeSubWorkflow'](
|
||||||
|
context,
|
||||||
|
workflowInfo,
|
||||||
|
items,
|
||||||
|
workflowProxyMock,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.response).toEqual([{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]);
|
||||||
|
expect(result.subExecutionId).toBe('test-execution');
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw error when workflow execution fails', async () => {
|
it('should throw error when workflow execution fails', async () => {
|
||||||
jest.spyOn(context, 'executeWorkflow').mockRejectedValueOnce(new Error('Execution failed'));
|
jest.spyOn(context, 'executeWorkflow').mockRejectedValueOnce(new Error('Execution failed'));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||||
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
|
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
|
||||||
import get from 'lodash/get';
|
import { isArray, isObject } from 'lodash';
|
||||||
import isObject from 'lodash/isObject';
|
|
||||||
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
|
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
|
||||||
import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode';
|
import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode';
|
||||||
import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions';
|
import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions';
|
||||||
|
@ -29,6 +28,10 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
function isNodeExecutionData(data: unknown): data is INodeExecutionData[] {
|
||||||
|
return isArray(data) && Boolean(data.length) && isObject(data[0]) && 'json' in data[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Main class for creating the Workflow tool
|
Main class for creating the Workflow tool
|
||||||
Processes the node parameters and creates AI Agent tool capable of executing n8n workflows
|
Processes the node parameters and creates AI Agent tool capable of executing n8n workflows
|
||||||
|
@ -43,10 +46,16 @@ export class WorkflowToolService {
|
||||||
// Sub-workflow execution id, will be set after the sub-workflow is executed
|
// Sub-workflow execution id, will be set after the sub-workflow is executed
|
||||||
private subExecutionId: string | undefined;
|
private subExecutionId: string | undefined;
|
||||||
|
|
||||||
constructor(private baseContext: ISupplyDataFunctions) {
|
private returnAllItems: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private baseContext: ISupplyDataFunctions,
|
||||||
|
options?: { returnAllItems: boolean },
|
||||||
|
) {
|
||||||
const subWorkflowInputs = this.baseContext.getNode().parameters
|
const subWorkflowInputs = this.baseContext.getNode().parameters
|
||||||
.workflowInputs as ResourceMapperValue;
|
.workflowInputs as ResourceMapperValue;
|
||||||
this.useSchema = (subWorkflowInputs?.schema ?? []).length > 0;
|
this.useSchema = (subWorkflowInputs?.schema ?? []).length > 0;
|
||||||
|
this.returnAllItems = options?.returnAllItems ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates the tool based on the provided parameters
|
// Creates the tool based on the provided parameters
|
||||||
|
@ -65,7 +74,7 @@ export class WorkflowToolService {
|
||||||
const toolHandler = async (
|
const toolHandler = async (
|
||||||
query: string | IDataObject,
|
query: string | IDataObject,
|
||||||
runManager?: CallbackManagerForToolRun,
|
runManager?: CallbackManagerForToolRun,
|
||||||
): Promise<string> => {
|
): Promise<IDataObject | IDataObject[] | string> => {
|
||||||
const localRunIndex = runIndex++;
|
const localRunIndex = runIndex++;
|
||||||
// We need to clone the context here to handle runIndex correctly
|
// We need to clone the context here to handle runIndex correctly
|
||||||
// Otherwise the runIndex will be shared between different executions
|
// Otherwise the runIndex will be shared between different executions
|
||||||
|
@ -74,10 +83,23 @@ export class WorkflowToolService {
|
||||||
runIndex: localRunIndex,
|
runIndex: localRunIndex,
|
||||||
inputData: [[{ json: { query } }]],
|
inputData: [[{ json: { query } }]],
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.runFunction(context, query, itemIndex, runManager);
|
const response = await this.runFunction(context, query, itemIndex, runManager);
|
||||||
|
|
||||||
const processedResponse = this.handleToolResponse(response);
|
const processedResponse = this.handleToolResponse(response);
|
||||||
|
|
||||||
|
let responseData: INodeExecutionData[];
|
||||||
|
if (isNodeExecutionData(response)) {
|
||||||
|
responseData = response;
|
||||||
|
} else {
|
||||||
|
const reParsedData = jsonParse<IDataObject>(processedResponse, {
|
||||||
|
fallbackValue: { response: processedResponse },
|
||||||
|
});
|
||||||
|
|
||||||
|
responseData = [{ json: reParsedData }];
|
||||||
|
}
|
||||||
|
|
||||||
// Once the sub-workflow is executed, add the output data to the context
|
// Once the sub-workflow is executed, add the output data to the context
|
||||||
// This will be used to link the sub-workflow execution in the parent workflow
|
// This will be used to link the sub-workflow execution in the parent workflow
|
||||||
let metadata: ITaskMetadata | undefined;
|
let metadata: ITaskMetadata | undefined;
|
||||||
|
@ -89,13 +111,11 @@ export class WorkflowToolService {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const json = jsonParse<IDataObject>(processedResponse, {
|
|
||||||
fallbackValue: { response: processedResponse },
|
|
||||||
});
|
|
||||||
void context.addOutputData(
|
void context.addOutputData(
|
||||||
NodeConnectionType.AiTool,
|
NodeConnectionType.AiTool,
|
||||||
localRunIndex,
|
localRunIndex,
|
||||||
[[{ json }]],
|
[responseData],
|
||||||
metadata,
|
metadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -126,6 +146,14 @@ export class WorkflowToolService {
|
||||||
return response.toString();
|
return response.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNodeExecutionData(response)) {
|
||||||
|
return JSON.stringify(
|
||||||
|
response.map((item) => item.json),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isObject(response)) {
|
if (isObject(response)) {
|
||||||
return JSON.stringify(response, null, 2);
|
return JSON.stringify(response, null, 2);
|
||||||
}
|
}
|
||||||
|
@ -148,7 +176,7 @@ export class WorkflowToolService {
|
||||||
items: INodeExecutionData[],
|
items: INodeExecutionData[],
|
||||||
workflowProxy: IWorkflowDataProxyData,
|
workflowProxy: IWorkflowDataProxyData,
|
||||||
runManager?: CallbackManagerForToolRun,
|
runManager?: CallbackManagerForToolRun,
|
||||||
): Promise<{ response: string; subExecutionId: string }> {
|
): Promise<{ response: string | IDataObject | INodeExecutionData[]; subExecutionId: string }> {
|
||||||
let receivedData: ExecuteWorkflowData;
|
let receivedData: ExecuteWorkflowData;
|
||||||
try {
|
try {
|
||||||
receivedData = await context.executeWorkflow(workflowInfo, items, runManager?.getChild(), {
|
receivedData = await context.executeWorkflow(workflowInfo, items, runManager?.getChild(), {
|
||||||
|
@ -163,7 +191,12 @@ export class WorkflowToolService {
|
||||||
throw new NodeOperationError(context.getNode(), error as Error);
|
throw new NodeOperationError(context.getNode(), error as Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: string | undefined = get(receivedData, 'data[0][0].json') as string | undefined;
|
let response: IDataObject | INodeExecutionData[] | undefined;
|
||||||
|
if (this.returnAllItems) {
|
||||||
|
response = receivedData?.data?.[0]?.length ? receivedData.data[0] : undefined;
|
||||||
|
} else {
|
||||||
|
response = receivedData?.data?.[0]?.[0]?.json;
|
||||||
|
}
|
||||||
if (response === undefined) {
|
if (response === undefined) {
|
||||||
throw new NodeOperationError(
|
throw new NodeOperationError(
|
||||||
context.getNode(),
|
context.getNode(),
|
||||||
|
@ -183,7 +216,7 @@ export class WorkflowToolService {
|
||||||
query: string | IDataObject,
|
query: string | IDataObject,
|
||||||
itemIndex: number,
|
itemIndex: number,
|
||||||
runManager?: CallbackManagerForToolRun,
|
runManager?: CallbackManagerForToolRun,
|
||||||
): Promise<string> {
|
): Promise<string | IDataObject | INodeExecutionData[]> {
|
||||||
const source = context.getNodeParameter('source', itemIndex) as string;
|
const source = context.getNodeParameter('source', itemIndex) as string;
|
||||||
const workflowProxy = context.getWorkflowDataProxy(0);
|
const workflowProxy = context.getWorkflowDataProxy(0);
|
||||||
|
|
||||||
|
@ -304,7 +337,10 @@ export class WorkflowToolService {
|
||||||
private async createStructuredTool(
|
private async createStructuredTool(
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
func: (query: string | IDataObject, runManager?: CallbackManagerForToolRun) => Promise<string>,
|
func: (
|
||||||
|
query: string | IDataObject,
|
||||||
|
runManager?: CallbackManagerForToolRun,
|
||||||
|
) => Promise<string | IDataObject | IDataObject[]>,
|
||||||
): Promise<DynamicStructuredTool | DynamicTool> {
|
): Promise<DynamicStructuredTool | DynamicTool> {
|
||||||
const collectedArguments = await this.extractFromAIParameters();
|
const collectedArguments = await this.extractFromAIParameters();
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const versionDescription: INodeTypeDescription = {
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Call n8n Workflow Tool',
|
name: 'Call n8n Workflow Tool',
|
||||||
},
|
},
|
||||||
version: [2],
|
version: [2, 2.1],
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [NodeConnectionType.AiTool],
|
outputs: [NodeConnectionType.AiTool],
|
||||||
outputNames: ['Tool'],
|
outputNames: ['Tool'],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/n8n-nodes-langchain",
|
"name": "@n8n/n8n-nodes-langchain",
|
||||||
"version": "1.81.0",
|
"version": "1.82.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/permissions",
|
"name": "@n8n/permissions",
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/task-runner",
|
"name": "@n8n/task-runner",
|
||||||
"version": "1.18.0",
|
"version": "1.19.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"start": "node dist/start.js",
|
"start": "node dist/start.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/typescript-config",
|
"name": "@n8n/typescript-config",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"tsconfig.backend.json",
|
"tsconfig.backend.json",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/utils",
|
"name": "@n8n/utils",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/vitest-config",
|
"name": "@n8n/vitest-config",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "catalog:frontend",
|
"vite": "catalog:frontend",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "1.81.0",
|
"version": "1.82.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import type { LoginRequestDto } from '@n8n/api-types';
|
||||||
|
import { Container } from '@n8n/di';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { Logger } from 'n8n-core';
|
||||||
|
|
||||||
|
import * as auth from '@/auth';
|
||||||
|
import { AuthService } from '@/auth/auth.service';
|
||||||
|
import config from '@/config';
|
||||||
|
import type { User } from '@/databases/entities/user';
|
||||||
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
|
import { EventService } from '@/events/event.service';
|
||||||
|
import { License } from '@/license';
|
||||||
|
import { MfaService } from '@/mfa/mfa.service';
|
||||||
|
import { PostHogClient } from '@/posthog';
|
||||||
|
import type { AuthenticatedRequest } from '@/requests';
|
||||||
|
import { UserService } from '@/services/user.service';
|
||||||
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
|
import { AuthController } from '../auth.controller';
|
||||||
|
|
||||||
|
jest.mock('@/auth');
|
||||||
|
|
||||||
|
const mockedAuth = auth as jest.Mocked<typeof auth>;
|
||||||
|
|
||||||
|
describe('AuthController', () => {
|
||||||
|
mockInstance(Logger);
|
||||||
|
mockInstance(EventService);
|
||||||
|
mockInstance(AuthService);
|
||||||
|
mockInstance(MfaService);
|
||||||
|
mockInstance(UserService);
|
||||||
|
mockInstance(UserRepository);
|
||||||
|
mockInstance(PostHogClient);
|
||||||
|
mockInstance(License);
|
||||||
|
const controller = Container.get(AuthController);
|
||||||
|
const userService = Container.get(UserService);
|
||||||
|
const authService = Container.get(AuthService);
|
||||||
|
const eventsService = Container.get(EventService);
|
||||||
|
const postHog = Container.get(PostHogClient);
|
||||||
|
|
||||||
|
describe('login', () => {
|
||||||
|
it('should not validate email in "emailOrLdapLoginId" if LDAP is enabled', async () => {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
const browserId = '1';
|
||||||
|
|
||||||
|
const member = mock<User>({
|
||||||
|
id: '123',
|
||||||
|
role: 'global:member',
|
||||||
|
mfaEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = mock<LoginRequestDto>({
|
||||||
|
emailOrLdapLoginId: 'non email',
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = mock<AuthenticatedRequest>({
|
||||||
|
user: member,
|
||||||
|
body,
|
||||||
|
browserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = mock<Response>();
|
||||||
|
|
||||||
|
mockedAuth.handleEmailLogin.mockResolvedValue(member);
|
||||||
|
|
||||||
|
mockedAuth.handleLdapLogin.mockResolvedValue(member);
|
||||||
|
|
||||||
|
config.set('userManagement.authenticationMethod', 'ldap');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
await controller.login(req, res, body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
expect(mockedAuth.handleEmailLogin).toHaveBeenCalledWith(
|
||||||
|
body.emailOrLdapLoginId,
|
||||||
|
body.password,
|
||||||
|
);
|
||||||
|
expect(mockedAuth.handleLdapLogin).toHaveBeenCalledWith(
|
||||||
|
body.emailOrLdapLoginId,
|
||||||
|
body.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(authService.issueCookie).toHaveBeenCalledWith(res, member, browserId);
|
||||||
|
expect(eventsService.emit).toHaveBeenCalledWith('user-logged-in', {
|
||||||
|
user: member,
|
||||||
|
authenticationMethod: 'ldap',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userService.toPublic).toHaveBeenCalledWith(member, {
|
||||||
|
posthog: postHog,
|
||||||
|
withScopes: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types';
|
import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types';
|
||||||
|
import { isEmail } from 'class-validator';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Logger } from 'n8n-core';
|
import { Logger } from 'n8n-core';
|
||||||
|
|
||||||
|
@ -44,14 +45,19 @@ export class AuthController {
|
||||||
res: Response,
|
res: Response,
|
||||||
@Body payload: LoginRequestDto,
|
@Body payload: LoginRequestDto,
|
||||||
): Promise<PublicUser | undefined> {
|
): Promise<PublicUser | undefined> {
|
||||||
const { email, password, mfaCode, mfaRecoveryCode } = payload;
|
const { emailOrLdapLoginId, password, mfaCode, mfaRecoveryCode } = payload;
|
||||||
|
|
||||||
let user: User | undefined;
|
let user: User | undefined;
|
||||||
|
|
||||||
let usedAuthenticationMethod = getCurrentAuthenticationMethod();
|
let usedAuthenticationMethod = getCurrentAuthenticationMethod();
|
||||||
|
|
||||||
|
if (usedAuthenticationMethod === 'email' && !isEmail(emailOrLdapLoginId)) {
|
||||||
|
throw new BadRequestError('Invalid email address');
|
||||||
|
}
|
||||||
|
|
||||||
if (isSamlCurrentAuthenticationMethod()) {
|
if (isSamlCurrentAuthenticationMethod()) {
|
||||||
// attempt to fetch user data with the credentials, but don't log in yet
|
// attempt to fetch user data with the credentials, but don't log in yet
|
||||||
const preliminaryUser = await handleEmailLogin(email, password);
|
const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password);
|
||||||
// if the user is an owner, continue with the login
|
// if the user is an owner, continue with the login
|
||||||
if (
|
if (
|
||||||
preliminaryUser?.role === 'global:owner' ||
|
preliminaryUser?.role === 'global:owner' ||
|
||||||
|
@ -63,15 +69,15 @@ export class AuthController {
|
||||||
throw new AuthError('SSO is enabled, please log in with SSO');
|
throw new AuthError('SSO is enabled, please log in with SSO');
|
||||||
}
|
}
|
||||||
} else if (isLdapCurrentAuthenticationMethod()) {
|
} else if (isLdapCurrentAuthenticationMethod()) {
|
||||||
const preliminaryUser = await handleEmailLogin(email, password);
|
const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password);
|
||||||
if (preliminaryUser?.role === 'global:owner') {
|
if (preliminaryUser?.role === 'global:owner') {
|
||||||
user = preliminaryUser;
|
user = preliminaryUser;
|
||||||
usedAuthenticationMethod = 'email';
|
usedAuthenticationMethod = 'email';
|
||||||
} else {
|
} else {
|
||||||
user = await handleLdapLogin(email, password);
|
user = await handleLdapLogin(emailOrLdapLoginId, password);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user = await handleEmailLogin(email, password);
|
user = await handleEmailLogin(emailOrLdapLoginId, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -101,7 +107,7 @@ export class AuthController {
|
||||||
}
|
}
|
||||||
this.eventService.emit('user-login-failed', {
|
this.eventService.emit('user-login-failed', {
|
||||||
authenticationMethod: usedAuthenticationMethod,
|
authenticationMethod: usedAuthenticationMethod,
|
||||||
userEmail: email,
|
userEmail: emailOrLdapLoginId,
|
||||||
reason: 'wrong credentials',
|
reason: 'wrong credentials',
|
||||||
});
|
});
|
||||||
throw new AuthError('Wrong username or password. Do you have caps lock on?');
|
throw new AuthError('Wrong username or password. Do you have caps lock on?');
|
||||||
|
|
|
@ -43,7 +43,7 @@ describe('POST /login', () => {
|
||||||
|
|
||||||
test('should log user in', async () => {
|
test('should log user in', async () => {
|
||||||
const response = await testServer.authlessAgent.post('/login').send({
|
const response = await testServer.authlessAgent.post('/login').send({
|
||||||
email: owner.email,
|
emailOrLdapLoginId: owner.email,
|
||||||
password: ownerPassword,
|
password: ownerPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ describe('POST /login', () => {
|
||||||
await mfaService.enableMfa(owner.id);
|
await mfaService.enableMfa(owner.id);
|
||||||
|
|
||||||
const response = await testServer.authlessAgent.post('/login').send({
|
const response = await testServer.authlessAgent.post('/login').send({
|
||||||
email: owner.email,
|
emailOrLdapLoginId: owner.email,
|
||||||
password: ownerPassword,
|
password: ownerPassword,
|
||||||
mfaCode: mfaService.totp.generateTOTP(secret),
|
mfaCode: mfaService.totp.generateTOTP(secret),
|
||||||
});
|
});
|
||||||
|
@ -131,7 +131,7 @@ describe('POST /login', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await testServer.authlessAgent.post('/login').send({
|
const response = await testServer.authlessAgent.post('/login').send({
|
||||||
email: member.email,
|
emailOrLdapLoginId: member.email,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
expect(response.statusCode).toBe(403);
|
expect(response.statusCode).toBe(403);
|
||||||
|
@ -148,19 +148,16 @@ describe('POST /login', () => {
|
||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail on invalid email in the payload', async () => {
|
test('should fail with invalid email in the payload is the current authentication method is "email"', async () => {
|
||||||
|
config.set('userManagement.authenticationMethod', 'email');
|
||||||
|
|
||||||
const response = await testServer.authlessAgent.post('/login').send({
|
const response = await testServer.authlessAgent.post('/login').send({
|
||||||
email: 'invalid-email',
|
emailOrLdapLoginId: 'invalid-email',
|
||||||
password: ownerPassword,
|
password: ownerPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.statusCode).toBe(400);
|
expect(response.statusCode).toBe(400);
|
||||||
expect(response.body).toEqual({
|
expect(response.body.message).toBe('Invalid email address');
|
||||||
validation: 'email',
|
|
||||||
code: 'invalid_string',
|
|
||||||
message: 'Invalid email',
|
|
||||||
path: ['email'],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -470,7 +470,7 @@ describe('POST /login', () => {
|
||||||
|
|
||||||
const response = await testServer.authlessAgent
|
const response = await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: ldapUser.mail, password: 'password' });
|
.send({ emailOrLdapLoginId: ldapUser.mail, password: 'password' });
|
||||||
|
|
||||||
expect(response.statusCode).toBe(200);
|
expect(response.statusCode).toBe(200);
|
||||||
expect(response.headers['set-cookie']).toBeDefined();
|
expect(response.headers['set-cookie']).toBeDefined();
|
||||||
|
@ -529,7 +529,7 @@ describe('POST /login', () => {
|
||||||
|
|
||||||
const response = await testServer.authlessAgent
|
const response = await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: owner.email, password: 'password' });
|
.send({ emailOrLdapLoginId: owner.email, password: 'password' });
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data?.signInType).toBeDefined();
|
expect(response.body.data?.signInType).toBeDefined();
|
||||||
|
|
|
@ -268,7 +268,7 @@ describe('Change password with MFA enabled', () => {
|
||||||
.authAgentFor(user)
|
.authAgentFor(user)
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({
|
.send({
|
||||||
email: user.email,
|
emailOrLdapLoginId: user.email,
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
mfaCode: new TOTPService().generateTOTP(rawSecret),
|
mfaCode: new TOTPService().generateTOTP(rawSecret),
|
||||||
})
|
})
|
||||||
|
@ -306,7 +306,10 @@ describe('Login', () => {
|
||||||
|
|
||||||
const user = await createUser({ password });
|
const user = await createUser({ password });
|
||||||
|
|
||||||
await testServer.authlessAgent.post('/login').send({ email: user.email, password }).expect(200);
|
await testServer.authlessAgent
|
||||||
|
.post('/login')
|
||||||
|
.send({ emailOrLdapLoginId: user.email, password })
|
||||||
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET /login should not include mfaSecret and mfaRecoveryCodes property in response', async () => {
|
test('GET /login should not include mfaSecret and mfaRecoveryCodes property in response', async () => {
|
||||||
|
@ -323,7 +326,7 @@ describe('Login', () => {
|
||||||
|
|
||||||
await testServer.authlessAgent
|
await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: user.email, password: rawPassword })
|
.send({ emailOrLdapLoginId: user.email, password: rawPassword })
|
||||||
.expect(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -333,7 +336,7 @@ describe('Login', () => {
|
||||||
|
|
||||||
await testServer.authlessAgent
|
await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: user.email, password: rawPassword, mfaCode: 'wrongvalue' })
|
.send({ emailOrLdapLoginId: user.email, password: rawPassword, mfaCode: 'wrongvalue' })
|
||||||
.expect(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -342,7 +345,7 @@ describe('Login', () => {
|
||||||
|
|
||||||
const response = await testServer.authlessAgent
|
const response = await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: user.email, password: rawPassword })
|
.send({ emailOrLdapLoginId: user.email, password: rawPassword })
|
||||||
.expect(401);
|
.expect(401);
|
||||||
|
|
||||||
expect(response.body.code).toBe(998);
|
expect(response.body.code).toBe(998);
|
||||||
|
@ -355,7 +358,7 @@ describe('Login', () => {
|
||||||
|
|
||||||
const response = await testServer.authlessAgent
|
const response = await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: user.email, password: rawPassword, mfaCode: token })
|
.send({ emailOrLdapLoginId: user.email, password: rawPassword, mfaCode: token })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const data = response.body.data;
|
const data = response.body.data;
|
||||||
|
@ -370,7 +373,11 @@ describe('Login', () => {
|
||||||
|
|
||||||
await testServer.authlessAgent
|
await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: user.email, password: rawPassword, mfaRecoveryCode: 'wrongvalue' })
|
.send({
|
||||||
|
emailOrLdapLoginId: user.email,
|
||||||
|
password: rawPassword,
|
||||||
|
mfaRecoveryCode: 'wrongvalue',
|
||||||
|
})
|
||||||
.expect(401);
|
.expect(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -379,7 +386,11 @@ describe('Login', () => {
|
||||||
|
|
||||||
const response = await testServer.authlessAgent
|
const response = await testServer.authlessAgent
|
||||||
.post('/login')
|
.post('/login')
|
||||||
.send({ email: user.email, password: rawPassword, mfaRecoveryCode: rawRecoveryCodes[0] })
|
.send({
|
||||||
|
emailOrLdapLoginId: user.email,
|
||||||
|
password: rawPassword,
|
||||||
|
mfaRecoveryCode: rawRecoveryCodes[0],
|
||||||
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const data = response.body.data;
|
const data = response.body.data;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "1.80.0",
|
"version": "1.81.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/chat",
|
"name": "@n8n/chat",
|
||||||
"version": "0.34.0",
|
"version": "0.35.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm run storybook",
|
"dev": "pnpm run storybook",
|
||||||
"build": "pnpm build:vite && pnpm build:bundle",
|
"build": "pnpm build:vite && pnpm build:bundle",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/composables",
|
"name": "@n8n/composables",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/design-system",
|
"name": "@n8n/design-system",
|
||||||
"version": "1.69.0",
|
"version": "1.70.0",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"import": "src/index.ts",
|
"import": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "1.81.0",
|
"version": "1.82.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
|
LoginRequestDto,
|
||||||
PasswordUpdateRequestDto,
|
PasswordUpdateRequestDto,
|
||||||
SettingsUpdateRequestDto,
|
SettingsUpdateRequestDto,
|
||||||
UserUpdateRequestDto,
|
UserUpdateRequestDto,
|
||||||
|
@ -21,7 +22,7 @@ export async function loginCurrentUser(
|
||||||
|
|
||||||
export async function login(
|
export async function login(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
params: { email: string; password: string; mfaCode?: string; mfaRecoveryToken?: string },
|
params: LoginRequestDto,
|
||||||
): Promise<CurrentUserResponse> {
|
): Promise<CurrentUserResponse> {
|
||||||
return await makeRestApiRequest(context, 'POST', '/login', params);
|
return await makeRestApiRequest(context, 'POST', '/login', params);
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,14 +166,14 @@ describe('RunData', () => {
|
||||||
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
|
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable pin data button when data is pinned', async () => {
|
it('should not disable pin data button when data is pinned [ADO-3143]', async () => {
|
||||||
const { getByTestId } = render({
|
const { getByTestId } = render({
|
||||||
defaultRunItems: [],
|
defaultRunItems: [],
|
||||||
displayMode: 'table',
|
displayMode: 'table',
|
||||||
pinnedData: [{ json: { name: 'Test' } }],
|
pinnedData: [{ json: { name: 'Test' } }],
|
||||||
});
|
});
|
||||||
const pinDataButton = getByTestId('ndv-pin-data');
|
const pinDataButton = getByTestId('ndv-pin-data');
|
||||||
expect(pinDataButton).toBeDisabled();
|
expect(pinDataButton).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render callout when data is pinned in output panel', async () => {
|
it('should render callout when data is pinned in output panel', async () => {
|
||||||
|
|
|
@ -528,8 +528,7 @@ const showPinButton = computed(() => {
|
||||||
|
|
||||||
const pinButtonDisabled = computed(
|
const pinButtonDisabled = computed(
|
||||||
() =>
|
() =>
|
||||||
pinnedData.hasData.value ||
|
(!rawInputData.value.length && !pinnedData.hasData.value) ||
|
||||||
!rawInputData.value.length ||
|
|
||||||
!!binaryData.value?.length ||
|
!!binaryData.value?.length ||
|
||||||
isReadOnlyRoute.value ||
|
isReadOnlyRoute.value ||
|
||||||
readOnlyEnv.value,
|
readOnlyEnv.value,
|
||||||
|
|
|
@ -30,7 +30,7 @@ const renderComponent = createComponentRenderer(RunDataPinButton, {
|
||||||
},
|
},
|
||||||
dataPinningDocsUrl: '',
|
dataPinningDocsUrl: '',
|
||||||
pinnedData: {
|
pinnedData: {
|
||||||
hasData: false,
|
hasData: { value: false },
|
||||||
},
|
},
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
|
@ -121,4 +121,30 @@ describe('RunDataPinButton.vue', () => {
|
||||||
expect(getByRole('tooltip')).toBeVisible();
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
expect(getByRole('tooltip')).toHaveTextContent('disabled');
|
expect(getByRole('tooltip')).toHaveTextContent('disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('pins data on button click', async () => {
|
||||||
|
const { getByTestId, getByRole, emitted } = renderComponent({});
|
||||||
|
// Should show 'Pin data' tooltip and emit togglePinData event
|
||||||
|
await userEvent.hover(getByTestId('ndv-pin-data'));
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip').textContent).toContain('Pin data');
|
||||||
|
await userEvent.click(getByTestId('ndv-pin-data'));
|
||||||
|
expect(emitted().togglePinData).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show correct tooltip and unpin data on button click', async () => {
|
||||||
|
const { getByTestId, getByRole, emitted } = renderComponent({
|
||||||
|
props: {
|
||||||
|
pinnedData: {
|
||||||
|
hasData: { value: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Should show 'Unpin data' tooltip and emit togglePinData event
|
||||||
|
await userEvent.hover(getByTestId('ndv-pin-data'));
|
||||||
|
expect(getByRole('tooltip')).toBeVisible();
|
||||||
|
expect(getByRole('tooltip').textContent).toContain('Unpin data');
|
||||||
|
await userEvent.click(getByTestId('ndv-pin-data'));
|
||||||
|
expect(emitted().togglePinData).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,14 +37,19 @@ const visible = computed(() =>
|
||||||
{{ locale.baseText('node.discovery.pinData.ndv') }}
|
{{ locale.baseText('node.discovery.pinData.ndv') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<strong>{{ locale.baseText('ndv.pinData.pin.title') }}</strong>
|
<div v-if="pinnedData.hasData.value">
|
||||||
<N8nText size="small" tag="p">
|
<strong>{{ locale.baseText('ndv.pinData.unpin.title') }}</strong>
|
||||||
{{ locale.baseText('ndv.pinData.pin.description') }}
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<strong>{{ locale.baseText('ndv.pinData.pin.title') }}</strong>
|
||||||
|
<N8nText size="small" tag="p">
|
||||||
|
{{ locale.baseText('ndv.pinData.pin.description') }}
|
||||||
|
|
||||||
<N8nLink :to="props.dataPinningDocsUrl" size="small">
|
<N8nLink :to="props.dataPinningDocsUrl" size="small">
|
||||||
{{ locale.baseText('ndv.pinData.pin.link') }}
|
{{ locale.baseText('ndv.pinData.pin.link') }}
|
||||||
</N8nLink>
|
</N8nLink>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
|
|
|
@ -1042,9 +1042,10 @@
|
||||||
"ndv.title.rename": "Rename",
|
"ndv.title.rename": "Rename",
|
||||||
"ndv.title.renameNode": "Rename node",
|
"ndv.title.renameNode": "Rename node",
|
||||||
"ndv.pinData.pin.title": "Pin data",
|
"ndv.pinData.pin.title": "Pin data",
|
||||||
"ndv.pinData.pin.description": "Node will always output this data instead of executing.",
|
"ndv.pinData.pin.description": "Node will always output current data instead of executing. Doesn't apply to production executions.",
|
||||||
"ndv.pinData.pin.binary": "Pin Data is disabled as this node's output contains binary data.",
|
"ndv.pinData.pin.binary": "Pin Data is disabled as this node's output contains binary data.",
|
||||||
"ndv.pinData.pin.link": "More info",
|
"ndv.pinData.pin.link": "More info",
|
||||||
|
"ndv.pinData.unpin.title": "Unpin data",
|
||||||
"ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned",
|
"ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned",
|
||||||
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
|
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
|
||||||
"ndv.pinData.unpinAndExecute.title": "Unpin output data?",
|
"ndv.pinData.unpinAndExecute.title": "Unpin output data?",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
|
LoginRequestDto,
|
||||||
PasswordUpdateRequestDto,
|
PasswordUpdateRequestDto,
|
||||||
SettingsUpdateRequestDto,
|
SettingsUpdateRequestDto,
|
||||||
UserUpdateRequestDto,
|
UserUpdateRequestDto,
|
||||||
|
@ -181,12 +182,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginWithCreds = async (params: {
|
const loginWithCreds = async (params: LoginRequestDto) => {
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
mfaCode?: string;
|
|
||||||
mfaRecoveryCode?: string;
|
|
||||||
}) => {
|
|
||||||
const user = await usersApi.login(rootStore.restApiContext, params);
|
const user = await usersApi.login(rootStore.restApiContext, params);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import Logo from '@/components/Logo/Logo.vue';
|
||||||
import SSOLogin from '@/components/SSOLogin.vue';
|
import SSOLogin from '@/components/SSOLogin.vue';
|
||||||
import type { IFormBoxConfig } from '@/Interface';
|
import type { IFormBoxConfig } from '@/Interface';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import type { EmailOrLdapLoginIdAndPassword } from './SigninView.vue';
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -19,7 +20,7 @@ withDefaults(
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
update: [{ name: string; value: string }];
|
update: [{ name: string; value: string }];
|
||||||
submit: [values: { [key: string]: string }];
|
submit: [values: EmailOrLdapLoginIdAndPassword];
|
||||||
secondaryClick: [];
|
secondaryClick: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ const onUpdate = (e: { name: string; value: string }) => {
|
||||||
emit('update', e);
|
emit('update', e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (values: { [key: string]: string }) => {
|
const onSubmit = (values: EmailOrLdapLoginIdAndPassword) => {
|
||||||
emit('submit', values);
|
emit('submit', values);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ describe('SigninView', () => {
|
||||||
await userEvent.click(submitButton);
|
await userEvent.click(submitButton);
|
||||||
|
|
||||||
expect(usersStore.loginWithCreds).toHaveBeenCalledWith({
|
expect(usersStore.loginWithCreds).toHaveBeenCalledWith({
|
||||||
email: 'test@n8n.io',
|
emailOrLdapLoginId: 'test@n8n.io',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
mfaCode: undefined,
|
mfaCode: undefined,
|
||||||
mfaRecoveryCode: undefined,
|
mfaRecoveryCode: undefined,
|
||||||
|
|
|
@ -15,6 +15,14 @@ import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
|
|
||||||
import type { IFormBoxConfig } from '@/Interface';
|
import type { IFormBoxConfig } from '@/Interface';
|
||||||
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants';
|
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants';
|
||||||
|
import type { LoginRequestDto } from '@n8n/api-types';
|
||||||
|
|
||||||
|
export type EmailOrLdapLoginIdAndPassword = Pick<
|
||||||
|
LoginRequestDto,
|
||||||
|
'emailOrLdapLoginId' | 'password'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type MfaCodeOrMfaRecoveryCode = Pick<LoginRequestDto, 'mfaCode' | 'mfaRecoveryCode'>;
|
||||||
|
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
@ -29,7 +37,7 @@ const telemetry = useTelemetry();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showMfaView = ref(false);
|
const showMfaView = ref(false);
|
||||||
const email = ref('');
|
const emailOrLdapLoginId = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const reportError = ref(false);
|
const reportError = ref(false);
|
||||||
|
|
||||||
|
@ -50,7 +58,7 @@ const formConfig: IFormBoxConfig = reactive({
|
||||||
redirectLink: '/forgot-password',
|
redirectLink: '/forgot-password',
|
||||||
inputs: [
|
inputs: [
|
||||||
{
|
{
|
||||||
name: 'email',
|
name: 'emailOrLdapLoginId',
|
||||||
properties: {
|
properties: {
|
||||||
label: emailLabel.value,
|
label: emailLabel.value,
|
||||||
type: 'email',
|
type: 'email',
|
||||||
|
@ -78,23 +86,16 @@ const formConfig: IFormBoxConfig = reactive({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const onMFASubmitted = async (form: { mfaCode?: string; mfaRecoveryCode?: string }) => {
|
const onMFASubmitted = async (form: MfaCodeOrMfaRecoveryCode) => {
|
||||||
await login({
|
await login({
|
||||||
email: email.value,
|
emailOrLdapLoginId: emailOrLdapLoginId.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
mfaCode: form.mfaCode,
|
mfaCode: form.mfaCode,
|
||||||
mfaRecoveryCode: form.mfaRecoveryCode,
|
mfaRecoveryCode: form.mfaRecoveryCode,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormWithEmailAndPassword = (values: {
|
const onEmailPasswordSubmitted = async (form: EmailOrLdapLoginIdAndPassword) => {
|
||||||
[key: string]: string;
|
|
||||||
}): values is { email: string; password: string } => {
|
|
||||||
return 'email' in values && 'password' in values;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEmailPasswordSubmitted = async (form: { [key: string]: string }) => {
|
|
||||||
if (!isFormWithEmailAndPassword(form)) return;
|
|
||||||
await login(form);
|
await login(form);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,16 +112,11 @@ const getRedirectQueryParameter = () => {
|
||||||
return redirect;
|
return redirect;
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async (form: {
|
const login = async (form: LoginRequestDto) => {
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
mfaCode?: string;
|
|
||||||
mfaRecoveryCode?: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await usersStore.loginWithCreds({
|
await usersStore.loginWithCreds({
|
||||||
email: form.email,
|
emailOrLdapLoginId: form.emailOrLdapLoginId,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
mfaCode: form.mfaCode,
|
mfaCode: form.mfaCode,
|
||||||
mfaRecoveryCode: form.mfaRecoveryCode,
|
mfaRecoveryCode: form.mfaRecoveryCode,
|
||||||
|
@ -185,8 +181,8 @@ const onFormChanged = (toForm: string) => {
|
||||||
reportError.value = false;
|
reportError.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const cacheCredentials = (form: { email: string; password: string }) => {
|
const cacheCredentials = (form: EmailOrLdapLoginIdAndPassword) => {
|
||||||
email.value = form.email;
|
emailOrLdapLoginId.value = form.emailOrLdapLoginId;
|
||||||
password.value = form.password;
|
password.value = form.password;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-node-dev",
|
"name": "n8n-node-dev",
|
||||||
"version": "1.80.0",
|
"version": "1.81.0",
|
||||||
"description": "CLI to simplify n8n credentials/node development",
|
"description": "CLI to simplify n8n credentials/node development",
|
||||||
"main": "dist/src/index",
|
"main": "dist/src/index",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
|
|
@ -457,14 +457,12 @@ export class Github implements INodeType {
|
||||||
required: true,
|
required: true,
|
||||||
modes: [
|
modes: [
|
||||||
{
|
{
|
||||||
displayName: 'Workflow',
|
displayName: 'From List',
|
||||||
name: 'list',
|
name: 'list',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
placeholder: 'Select a workflow...',
|
placeholder: 'Select a workflow...',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
searchListMethod: 'getWorkflows',
|
searchListMethod: 'getWorkflows',
|
||||||
searchable: true,
|
|
||||||
searchFilterRequired: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -482,6 +480,21 @@ export class Github implements INodeType {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By File Name',
|
||||||
|
name: 'filename',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'e.g. main.yaml or main.yml',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '[a-zA-Z0-9_-]+.(yaml|yml)',
|
||||||
|
errorMessage: 'Not a valid Github Workflow File Name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
|
@ -2501,7 +2514,9 @@ export class Github implements INodeType {
|
||||||
|
|
||||||
requestMethod = 'POST';
|
requestMethod = 'POST';
|
||||||
|
|
||||||
const workflowId = this.getNodeParameter('workflowId', i) as string;
|
const workflowId = this.getNodeParameter('workflowId', i, undefined, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as string;
|
||||||
|
|
||||||
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`;
|
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`;
|
||||||
body.ref = this.getNodeParameter('ref', i) as string;
|
body.ref = this.getNodeParameter('ref', i) as string;
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import nock from 'nock';
|
||||||
|
|
||||||
|
import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
|
||||||
|
|
||||||
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
|
describe('Test Github Node', () => {
|
||||||
|
describe('Workflow Dispatch', () => {
|
||||||
|
const now = 1683028800000;
|
||||||
|
const owner = 'testOwner';
|
||||||
|
const repository = 'testRepository';
|
||||||
|
const workflowId = 147025216;
|
||||||
|
const usersResponse = {
|
||||||
|
total_count: 12,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
login: 'testOwner',
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const repositoriesResponse = {
|
||||||
|
total_count: 40,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 3081286,
|
||||||
|
name: 'testRepository',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const workflowsResponse = {
|
||||||
|
total_count: 2,
|
||||||
|
workflows: [
|
||||||
|
{
|
||||||
|
id: workflowId,
|
||||||
|
node_id: 'MDg6V29ya2Zsb3cxNjEzMzU=',
|
||||||
|
name: 'CI',
|
||||||
|
path: '.github/workflows/blank.yaml',
|
||||||
|
state: 'active',
|
||||||
|
created_at: '2020-01-08T23:48:37.000-08:00',
|
||||||
|
updated_at: '2020-01-08T23:50:21.000-08:00',
|
||||||
|
url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/161335',
|
||||||
|
html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/161335',
|
||||||
|
badge_url: 'https://github.com/octo-org/octo-repo/workflows/CI/badge.svg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 269289,
|
||||||
|
node_id: 'MDE4OldvcmtmbG93IFNlY29uZGFyeTI2OTI4OQ==',
|
||||||
|
name: 'Linter',
|
||||||
|
path: '.github/workflows/linter.yaml',
|
||||||
|
state: 'active',
|
||||||
|
created_at: '2020-01-08T23:48:37.000-08:00',
|
||||||
|
updated_at: '2020-01-08T23:50:21.000-08:00',
|
||||||
|
url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/269289',
|
||||||
|
html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/269289',
|
||||||
|
badge_url: 'https://github.com/octo-org/octo-repo/workflows/Linter/badge.svg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
|
||||||
|
await initBinaryDataService();
|
||||||
|
});
|
||||||
|
beforeEach(async () => {
|
||||||
|
const baseUrl = 'https://api.github.com';
|
||||||
|
nock.cleanAll();
|
||||||
|
nock(baseUrl)
|
||||||
|
.persist()
|
||||||
|
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||||
|
.get('/search/users')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, usersResponse)
|
||||||
|
.get('/search/repositories')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, repositoriesResponse)
|
||||||
|
.get(`/repos/${owner}/${repository}/actions/workflows`)
|
||||||
|
.reply(200, workflowsResponse)
|
||||||
|
.post(`/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`, {
|
||||||
|
ref: 'main',
|
||||||
|
inputs: {},
|
||||||
|
})
|
||||||
|
.reply(200, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
});
|
||||||
|
testWorkflows(workflows);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,86 @@
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [-300, 260],
|
||||||
|
"id": "b14bf20f-78b0-490a-bbc6-d02b1af4c03c",
|
||||||
|
"name": "When clicking ‘Test workflow’"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"resource": "workflow",
|
||||||
|
"workflowId": {
|
||||||
|
"__rl": true,
|
||||||
|
"value": 147025216,
|
||||||
|
"mode": "list",
|
||||||
|
"cachedResultName": "CI"
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"__rl": true,
|
||||||
|
"value": "testOwner",
|
||||||
|
"mode": "name"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"__rl": true,
|
||||||
|
"value": "testRepository",
|
||||||
|
"mode": "name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.github",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [-80, 260],
|
||||||
|
"id": "061752c9-507c-4b27-ba18-47b21d487aed",
|
||||||
|
"name": "GitHub",
|
||||||
|
"credentials": {
|
||||||
|
"githubApi": {
|
||||||
|
"id": "1",
|
||||||
|
"name": "GitHub account"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [120, 260],
|
||||||
|
"id": "3bc54e8f-eeba-496d-a95f-bb8927eff671",
|
||||||
|
"name": "No Operation, do nothing"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking ‘Test workflow’": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "GitHub",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"GitHub": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "No Operation, do nothing",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {
|
||||||
|
"No Operation, do nothing": [
|
||||||
|
{
|
||||||
|
"json": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-nodes-base",
|
"name": "n8n-nodes-base",
|
||||||
"version": "1.80.0",
|
"version": "1.81.0",
|
||||||
"description": "Base nodes of n8n",
|
"description": "Base nodes of n8n",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-workflow",
|
"name": "n8n-workflow",
|
||||||
"version": "1.79.0",
|
"version": "1.80.0",
|
||||||
"description": "Workflow base code of n8n",
|
"description": "Workflow base code of n8n",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
|
|
Loading…
Reference in a new issue