🔀 Merge parent branch

This commit is contained in:
Iván Ovejero 2022-05-03 13:47:43 +02:00
commit 9bd62c7e1d
23 changed files with 20904 additions and 14366 deletions

View file

@ -1,3 +1,21 @@
# [0.175.0](https://github.com/n8n-io/n8n/compare/n8n@0.174.0...n8n@0.175.0) (2022-05-02)
### Bug Fixes
- **core:** Do not applying auth if UM is disabled ([#3218](https://github.com/n8n-io/n8n/issues/3218)) ([4ceac38](https://github.com/n8n-io/n8n/commit/4ceac38e0397be91bfc1b50d8a06ebf20de8c32e))
- **core:** Skip credential check of disabled nodes and improve error ([79ced8f](https://github.com/n8n-io/n8n/commit/79ced8f6774cc70d63369001d7c56d1d4a340261))
- **editor:** Fix bug with touchscreens ([#3206](https://github.com/n8n-io/n8n/issues/3206)) ([8d9e05e](https://github.com/n8n-io/n8n/commit/8d9e05e3c3ef61cd5a65ec00d3d1474f1195f653))
- **Hubspot Node:** Fix search operators ([#3208](https://github.com/n8n-io/n8n/issues/3208)) ([ea4a8b8](https://github.com/n8n-io/n8n/commit/ea4a8b88c9bbfde3304073070a430f2421477921))
- **Sendgrid Node:** Fix issue sending attachments ([#3213](https://github.com/n8n-io/n8n/issues/3213)) ([2b00881](https://github.com/n8n-io/n8n/commit/2b008815cad82619a66d4b30d1f79630c82be978))
- **Wise Node:** Respect time parameter on get: exchangeRate ([#3227](https://github.com/n8n-io/n8n/issues/3227)) ([c7d525a](https://github.com/n8n-io/n8n/commit/c7d525a60fda4aad653017cb90253426cce98b3b))
### Features
- **core:** Introduce simplified node versioning ([#3205](https://github.com/n8n-io/n8n/issues/3205)) ([d5b9b0c](https://github.com/n8n-io/n8n/commit/d5b9b0cb9596688b3bcad0010b65888428c297c6))
- **Google Sheets Node:** Allow to use header names as JSON path ([#3165](https://github.com/n8n-io/n8n/issues/3165)) ([770c4fe](https://github.com/n8n-io/n8n/commit/770c4fe6ebe70c7a507ee5e57348508b98fda11d))
- **Microsoft Dynamics CRM Node:** Add support for other regions than North America ([#3157](https://github.com/n8n-io/n8n/issues/3157)) ([4bdd607](https://github.com/n8n-io/n8n/commit/4bdd607fdf00f6c2155c9b3e3c9e74ac50e317f4))
- **Telegram Node:** Allow querying chat administrators ([#3226](https://github.com/n8n-io/n8n/issues/3226)) ([c02d259](https://github.com/n8n-io/n8n/commit/c02d259453f2f5b444569f4c1e06fbfc95cd3305)), closes [#3157](https://github.com/n8n-io/n8n/issues/3157)
# [0.174.0](https://github.com/n8n-io/n8n/compare/n8n@0.173.1...n8n@0.174.0) (2022-04-25) # [0.174.0](https://github.com/n8n-io/n8n/compare/n8n@0.173.1...n8n@0.174.0) (2022-04-25)
### Bug Fixes ### Bug Fixes

34909
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.174.0", "version": "0.175.0",
"private": true, "private": true,
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
"scripts": { "scripts": {

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.174.0", "version": "0.175.0",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -125,10 +125,10 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.115.0", "n8n-core": "~0.116.0",
"n8n-editor-ui": "~0.141.0", "n8n-editor-ui": "~0.142.0",
"n8n-nodes-base": "~0.172.0", "n8n-nodes-base": "~0.173.0",
"n8n-workflow": "~0.97.0", "n8n-workflow": "~0.98.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",

View file

@ -35,15 +35,12 @@ import { readFile } from 'fs/promises';
import _, { cloneDeep } from 'lodash'; import _, { cloneDeep } from 'lodash';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path'; import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import { import {
FindConditions,
FindManyOptions, FindManyOptions,
getConnection, getConnection,
getConnectionManager, getConnectionManager,
In, In,
IsNull, IsNull,
LessThan,
LessThanOrEqual, LessThanOrEqual,
MoreThan,
Not, Not,
Raw, Raw,
} from 'typeorm'; } from 'typeorm';
@ -61,13 +58,7 @@ import { createHmac } from 'crypto';
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ... // tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs'; import { compare } from 'bcryptjs';
import { import { BinaryDataManager, Credentials, LoadNodeParameterOptions, UserSettings } from 'n8n-core';
BinaryDataManager,
Credentials,
IBinaryDataConfig,
LoadNodeParameterOptions,
UserSettings,
} from 'n8n-core';
import { import {
ICredentialType, ICredentialType,
@ -154,14 +145,11 @@ import { WEBHOOK_METHODS } from './WebhookHelpers';
import { userManagementRouter } from './UserManagement'; import { userManagementRouter } from './UserManagement';
import { resolveJwt } from './UserManagement/auth/jwt'; import { resolveJwt } from './UserManagement/auth/jwt';
import { User } from './databases/entities/User'; import { User } from './databases/entities/User';
import { CredentialsEntity } from './databases/entities/CredentialsEntity';
import type { import type {
CredentialRequest,
ExecutionRequest, ExecutionRequest,
WorkflowRequest, WorkflowRequest,
NodeParameterOptionsRequest, NodeParameterOptionsRequest,
OAuthRequest, OAuthRequest,
AuthenticatedRequest,
TagsRequest, TagsRequest,
} from './requests'; } from './requests';
import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers'; import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers';
@ -170,7 +158,11 @@ import { SharedWorkflow } from './databases/entities/SharedWorkflow';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
import { credentialsController } from './api/credentials.api'; import { credentialsController } from './api/credentials.api';
import { oauth2CredentialController } from './api/oauth2Credential.api'; import { oauth2CredentialController } from './api/oauth2Credential.api';
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper'; import {
getInstanceBaseUrl,
isEmailSetUp,
isUserManagementEnabled,
} from './UserManagement/UserManagementHelper';
require('body-parser-xml')(bodyParser); require('body-parser-xml')(bodyParser);
@ -312,9 +304,7 @@ class App {
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'), config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'), defaultLocale: config.getEnv('defaultLocale'),
userManagement: { userManagement: {
enabled: enabled: isUserManagementEnabled(),
config.getEnv('userManagement.disabled') === false ||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true,
showSetupOnFirstLoad: showSetupOnFirstLoad:
config.getEnv('userManagement.disabled') === false && config.getEnv('userManagement.disabled') === false &&
config.getEnv('userManagement.isInstanceOwnerSetUp') === false && config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
@ -347,9 +337,7 @@ class App {
getSettingsForFrontend(): IN8nUISettings { getSettingsForFrontend(): IN8nUISettings {
// refresh user management status // refresh user management status
Object.assign(this.frontendSettings.userManagement, { Object.assign(this.frontendSettings.userManagement, {
enabled: enabled: isUserManagementEnabled(),
config.getEnv('userManagement.disabled') === false ||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true,
showSetupOnFirstLoad: showSetupOnFirstLoad:
config.getEnv('userManagement.disabled') === false && config.getEnv('userManagement.disabled') === false &&
config.getEnv('userManagement.isInstanceOwnerSetUp') === false && config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
@ -568,12 +556,14 @@ class App {
return; return;
} }
try { if (isUserManagementEnabled()) {
const authCookie = req.cookies?.[AUTH_COOKIE_NAME] ?? ''; try {
await resolveJwt(authCookie); const authCookie = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
} catch (error) { await resolveJwt(authCookie);
res.status(401).send('Unauthorized'); } catch (error) {
return; res.status(401).send('Unauthorized');
return;
}
} }
this.push.add(req.query.sessionId as string, req, res); this.push.add(req.query.sessionId as string, req, res);
@ -3044,9 +3034,7 @@ export async function start(): Promise<void> {
}, },
deploymentType: config.getEnv('deployment.type'), deploymentType: config.getEnv('deployment.type'),
binaryDataMode: binarDataConfig.mode, binaryDataMode: binarDataConfig.mode,
n8n_multi_user_allowed: n8n_multi_user_allowed: isUserManagementEnabled(),
config.getEnv('userManagement.disabled') === false ||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true,
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp', smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
}; };

View file

@ -32,6 +32,20 @@ export function isEmailSetUp(): boolean {
return smtp && host && user && pass; return smtp && host && user && pass;
} }
export function isUserManagementEnabled(): boolean {
return (
!config.getEnv('userManagement.disabled') ||
config.getEnv('userManagement.isInstanceOwnerSetUp')
);
}
export function isUserManagementDisabled(): boolean {
return (
config.getEnv('userManagement.disabled') &&
!config.getEnv('userManagement.isInstanceOwnerSetUp')
);
}
async function getInstanceOwnerRole(): Promise<Role> { async function getInstanceOwnerRole(): Promise<Role> {
const ownerRole = await Db.collections.Role.findOneOrFail({ const ownerRole = await Db.collections.Role.findOneOrFail({
where: { where: {

View file

@ -19,7 +19,13 @@ import { usersNamespace } from './users';
import { passwordResetNamespace } from './passwordReset'; import { passwordResetNamespace } from './passwordReset';
import { AuthenticatedRequest } from '../../requests'; import { AuthenticatedRequest } from '../../requests';
import { ownerNamespace } from './owner'; import { ownerNamespace } from './owner';
import { isAuthExcluded, isPostUsersId, isAuthenticatedRequest } from '../UserManagementHelper'; import {
isAuthExcluded,
isPostUsersId,
isAuthenticatedRequest,
isUserManagementDisabled,
} from '../UserManagementHelper';
import { Db } from '../..';
export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint: string): void { export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint: string): void {
// needed for testing; not adding overhead since it directly returns if req.cookies exists // needed for testing; not adding overhead since it directly returns if req.cookies exists
@ -47,7 +53,7 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint
this.app.use(passport.initialize()); this.app.use(passport.initialize());
this.app.use((req: Request, res: Response, next: NextFunction) => { this.app.use(async (req: Request, res: Response, next: NextFunction) => {
if ( if (
// TODO: refactor me!!! // TODO: refactor me!!!
// skip authentication for preflight requests // skip authentication for preflight requests
@ -73,6 +79,17 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint
return next(); return next();
} }
// skip authentication if user management is disabled
if (isUserManagementDisabled()) {
req.user = await Db.collections.User.findOneOrFail(
{},
{
relations: ['globalRole'],
},
);
return next();
}
return passport.authenticate('jwt', { session: false })(req, res, next); return passport.authenticate('jwt', { session: false })(req, res, next);
}); });

View file

@ -12,6 +12,7 @@ import {
getInstanceBaseUrl, getInstanceBaseUrl,
hashPassword, hashPassword,
isEmailSetUp, isEmailSetUp,
isUserManagementDisabled,
sanitizeUser, sanitizeUser,
validatePassword, validatePassword,
} from '../UserManagementHelper'; } from '../UserManagementHelper';
@ -55,7 +56,7 @@ export function usersNamespace(this: N8nApp): void {
} }
// TODO: this should be checked in the middleware rather than here // TODO: this should be checked in the middleware rather than here
if (config.getEnv('userManagement.disabled')) { if (isUserManagementDisabled()) {
Logger.debug( Logger.debug(
'Request to send email invite(s) to user(s) failed because user management is disabled', 'Request to send email invite(s) to user(s) failed because user management is disabled',
); );

View file

@ -6,6 +6,7 @@ import { RESPONSE_ERROR_MESSAGES } from '../../src/constants';
import type { Role } from '../../src/databases/entities/Role'; import type { Role } from '../../src/databases/entities/Role';
import { CredentialTypes, LoadNodesAndCredentials } from '../../src'; import { CredentialTypes, LoadNodesAndCredentials } from '../../src';
import { LOAD_NODES_AND_CREDS_TEST_TIMEOUT } from './shared/constants';
const SCOPES_ENDPOINT = '/oauth2-credential/scopes'; const SCOPES_ENDPOINT = '/oauth2-credential/scopes';
@ -14,7 +15,10 @@ let testDbName = '';
let globalOwnerRole: Role; let globalOwnerRole: Role;
beforeAll(async () => { beforeAll(async () => {
app = utils.initTestServer({ endpointGroups: ['oauth2-credential'], applyAuth: true }); app = utils.initTestServer({
endpointGroups: ['oauth2-credential'],
applyAuth: true,
});
const initResult = await testDb.init(); const initResult = await testDb.init();
testDbName = initResult.testDbName; testDbName = initResult.testDbName;
@ -28,7 +32,7 @@ beforeAll(async () => {
const credentialTypes = CredentialTypes(); const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes); await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
}); }, LOAD_NODES_AND_CREDS_TEST_TIMEOUT);
afterAll(async () => { afterAll(async () => {
await testDb.terminate(testDbName); await testDb.terminate(testDbName);

View file

@ -62,3 +62,8 @@ export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
* Timeout (in milliseconds) to account for fake SMTP service being slow to respond. * Timeout (in milliseconds) to account for fake SMTP service being slow to respond.
*/ */
export const SMTP_TEST_TIMEOUT = 30_000; export const SMTP_TEST_TIMEOUT = 30_000;
/**
* Timeout (in milliseconds) to account for `LoadNodesAndCredentials()` being slow to run on CI/CD server.
*/
export const LOAD_NODES_AND_CREDS_TEST_TIMEOUT = 30_000;

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-core", "name": "n8n-core",
"version": "0.115.0", "version": "0.116.0",
"description": "Core functionality of n8n", "description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -52,7 +52,7 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.97.0", "n8n-workflow": "~0.98.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"qs": "^6.10.1", "qs": "^6.10.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-design-system", "name": "n8n-design-system",
"version": "0.18.0", "version": "0.19.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
"author": { "author": {

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-editor-ui", "name": "n8n-editor-ui",
"version": "0.141.0", "version": "0.142.0",
"description": "Workflow Editor UI for n8n", "description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -28,7 +28,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1",
"luxon": "^2.3.0", "luxon": "^2.3.0",
"monaco-editor": "^0.29.1", "monaco-editor": "^0.29.1",
"n8n-design-system": "~0.18.0", "n8n-design-system": "~0.19.0",
"timeago.js": "^4.0.2", "timeago.js": "^4.0.2",
"v-click-outside": "^3.1.2", "v-click-outside": "^3.1.2",
"vue-fragment": "^1.5.2", "vue-fragment": "^1.5.2",
@ -78,7 +78,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"n8n-workflow": "~0.97.0", "n8n-workflow": "~0.98.0",
"monaco-editor-webpack-plugin": "^5.0.0", "monaco-editor-webpack-plugin": "^5.0.0",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-if="credentialTypesNodeDescriptionDisplayed.length" :class="$style.container"> <div v-if="credentialTypesNodeDescriptionDisplayed.length" :class="['node-credentials', $style.container]">
<div v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed" :key="credentialTypeDescription.name"> <div v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed" :key="credentialTypeDescription.name">
<n8n-input-label <n8n-input-label
:label="$locale.baseText( :label="$locale.baseText(
@ -11,15 +11,20 @@
} }
)" )"
:bold="false" :bold="false"
size="small"
:set="issues = getIssues(credentialTypeDescription.name)" :set="issues = getIssues(credentialTypeDescription.name)"
size="small"
> >
<div v-if="isReadOnly"> <div v-if="isReadOnly">
<n8n-input disabled :value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name" size="small" /> <n8n-input
:value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name"
disabled
size="small"
/>
</div> </div>
<div
<div :class="issues.length ? $style.hasIssues : $style.input" v-else > v-else
:class="issues.length ? $style.hasIssues : $style.input"
>
<n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" :placeholder="$locale.baseText('nodeCredentials.selectCredential')" size="small"> <n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" :placeholder="$locale.baseText('nodeCredentials.selectCredential')" size="small">
<n8n-option <n8n-option
v-for="(item) in credentialOptions[credentialTypeDescription.name]" v-for="(item) in credentialOptions[credentialTypeDescription.name]"
@ -72,6 +77,7 @@ import { showMessage } from '@/components/mixins/showMessage';
import { mapGetters } from "vuex"; import { mapGetters } from "vuex";
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { HTTP_REQUEST_NODE_TYPE } from '@/constants';
export default mixins( export default mixins(
genericHelpers, genericHelpers,
@ -92,7 +98,18 @@ export default mixins(
computed: { computed: {
...mapGetters('credentials', { ...mapGetters('credentials', {
credentialOptions: 'allCredentialsByType', credentialOptions: 'allCredentialsByType',
getCredentialTypeByName: 'getCredentialTypeByName',
}), }),
isProxyAuth(): boolean {
return this.node.type === HTTP_REQUEST_NODE_TYPE
&& this.node.typeVersion === 2
&& this.node.parameters.authenticateWith === 'nodeCredential';
},
isGenericAuth(): boolean {
return this.node.type === HTTP_REQUEST_NODE_TYPE
&& this.node.typeVersion === 2
&& this.node.parameters.authenticateWith === 'genericAuth';
},
credentialTypesNode (): string[] { credentialTypesNode (): string[] {
return this.credentialTypesNodeDescription return this.credentialTypesNodeDescription
.map((credentialTypeDescription) => credentialTypeDescription.name); .map((credentialTypeDescription) => credentialTypeDescription.name);
@ -106,6 +123,18 @@ export default mixins(
credentialTypesNodeDescription (): INodeCredentialDescription[] { credentialTypesNodeDescription (): INodeCredentialDescription[] {
const node = this.node as INodeUi; const node = this.node as INodeUi;
if (this.isGenericAuth) {
const { genericAuthType } = this.node.parameters as { genericAuthType: string };
return [this.getCredentialTypeByName(genericAuthType)];
}
if (this.isProxyAuth) {
const { nodeCredentialType } = this.node.parameters as { nodeCredentialType?: string };
if (nodeCredentialType) return [this.getCredentialTypeByName(nodeCredentialType)];
}
const activeNodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription | null; const activeNodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription | null;
if (activeNodeType && activeNodeType.credentials) { if (activeNodeType && activeNodeType.credentials) {
return activeNodeType.credentials; return activeNodeType.credentials;
@ -295,7 +324,7 @@ export default mixins(
<style lang="scss" module> <style lang="scss" module>
.container { .container {
margin: var(--spacing-xs) 0; margin: 0;
> * { > * {
margin-bottom: var(--spacing-xs); margin-bottom: var(--spacing-xs);

View file

@ -23,12 +23,23 @@
</div> </div>
<div class="node-parameters-wrapper" v-if="node && nodeValid"> <div class="node-parameters-wrapper" v-if="node && nodeValid">
<div v-show="openPanel === 'params'"> <div v-show="openPanel === 'params'">
<node-credentials :node="node" @credentialSelected="credentialSelected"></node-credentials> <node-webhooks
<node-webhooks :node="node" :nodeType="nodeType" /> :node="node"
<parameter-input-list :parameters="parametersNoneSetting" :hideDelete="true" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" /> :nodeType="nodeType"
/>
<parameter-input-list
:parameters="parametersNoneSetting"
:hideDelete="true"
:nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged"
>
<node-credentials
:node="node"
@credentialSelected="credentialSelected"
/>
</parameter-input-list>
<div v-if="parametersNoneSetting.length === 0" class="no-parameters"> <div v-if="parametersNoneSetting.length === 0" class="no-parameters">
<n8n-text> <n8n-text>
{{ $locale.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }} {{ $locale.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }}
</n8n-text> </n8n-text>
</div> </div>
@ -85,6 +96,8 @@ import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import NodeExecuteButton from './NodeExecuteButton.vue'; import NodeExecuteButton from './NodeExecuteButton.vue';
import { HTTP_REQUEST_NODE_TYPE } from '@/constants';
export default mixins( export default mixins(
externalHooks, externalHooks,
genericHelpers, genericHelpers,

View file

@ -1,6 +1,8 @@
<template> <template>
<div class="paramter-input-list-wrapper"> <div class="parameter-input-list-wrapper">
<div v-for="parameter in filteredParameters" :key="parameter.name" :class="{indent}"> <div v-for="(parameter, index) in filteredParameters" :key="parameter.name" :class="{indent}">
<slot v-if="indexToShowSlotAt === index" />
<div <div
v-if="multipleValues(parameter) === true && parameter.type !== 'fixedCollection'" v-if="multipleValues(parameter) === true && parameter.type !== 'fixedCollection'"
class="parameter-item" class="parameter-item"
@ -86,6 +88,7 @@
<script lang="ts"> <script lang="ts">
import { import {
INode,
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
NodeParameterValue, NodeParameterValue,
@ -101,6 +104,7 @@ import ParameterInputFull from '@/components/ParameterInputFull.vue';
import { get, set } from 'lodash'; import { get, set } from 'lodash';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { HTTP_REQUEST_NODE_TYPE } from '@/constants';
export default mixins( export default mixins(
genericHelpers, genericHelpers,
@ -129,6 +133,13 @@ export default mixins(
node (): INodeUi { node (): INodeUi {
return this.$store.getters.activeNode; return this.$store.getters.activeNode;
}, },
indexToShowSlotAt (): number {
if (this.node.type === HTTP_REQUEST_NODE_TYPE && this.node.typeVersion === 2) {
return 2;
}
return 0;
},
}, },
methods: { methods: {
multipleValues (parameter: INodeProperties): boolean { multipleValues (parameter: INodeProperties): boolean {
@ -260,7 +271,11 @@ export default mixins(
</script> </script>
<style lang="scss"> <style lang="scss">
.paramter-input-list-wrapper { .parameter-input-list-wrapper {
display: flex;
flex-direction: column;
padding-top: var(--spacing-xs);
.delete-option { .delete-option {
display: none; display: none;
position: absolute; position: absolute;
@ -293,7 +308,7 @@ export default mixins(
.parameter-item { .parameter-item {
position: relative; position: relative;
margin: var(--spacing-xs) 0; margin: 0 0 var(--spacing-xs);
>.delete-option { >.delete-option {
top: var(--spacing-5xs); top: var(--spacing-5xs);

View file

@ -1,4 +1,5 @@
import { import {
HTTP_REQUEST_NODE_TYPE,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME, PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
} from '@/constants'; } from '@/constants';
@ -32,11 +33,15 @@ import { restApi } from '@/components/mixins/restApi';
import { get } from 'lodash'; import { get } from 'lodash';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { mapGetters } from 'vuex';
export const nodeHelpers = mixins( export const nodeHelpers = mixins(
restApi, restApi,
) )
.extend({ .extend({
computed: {
...mapGetters('credentials', [ 'getCredentialTypeByName', 'getCredentialsByType' ]),
},
methods: { methods: {
// Returns the parameter value // Returns the parameter value
@ -198,6 +203,46 @@ export const nodeHelpers = mixins(
let credentialType: ICredentialType | null; let credentialType: ICredentialType | null;
let credentialDisplayName: string; let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails; let selectedCredentials: INodeCredentialsDetails;
const { authenticateWith, genericAuthType, nodeCredentialType } = node.parameters as {
authenticateWith: 'none' | 'genericAuth' | 'nodeCredential';
genericAuthType: string;
nodeCredentialType: string;
};
if (
isHttpRequestNodeV2(node) &&
authenticateWith === 'genericAuth' &&
selectedCredsAreUnusable(node, genericAuthType)
) {
const credential = this.getCredentialTypeByName(genericAuthType);
return reportUnsetCredential(credential);
}
if (
isHttpRequestNodeV2(node) &&
authenticateWith === 'nodeCredential' &&
nodeCredentialType !== '' &&
node.credentials !== undefined
) {
const stored = this.getCredentialsByType(nodeCredentialType);
if (selectedCredsWereDeleted(node, nodeCredentialType, stored)) {
const credential = this.getCredentialTypeByName(nodeCredentialType);
return reportUnsetCredential(credential);
}
}
if (
isHttpRequestNodeV2(node) &&
authenticateWith === 'nodeCredential' &&
nodeCredentialType !== '' &&
selectedCredsAreUnusable(node, nodeCredentialType)
) {
const credential = this.getCredentialTypeByName(nodeCredentialType);
return reportUnsetCredential(credential);
}
for (const credentialTypeDescription of nodeType!.credentials!) { for (const credentialTypeDescription of nodeType!.credentials!) {
// Check if credentials should be displayed else ignore // Check if credentials should be displayed else ignore
if (this.displayParameter(node.parameters, credentialTypeDescription, '', node) !== true) { if (this.displayParameter(node.parameters, credentialTypeDescription, '', node) !== true) {
@ -394,3 +439,41 @@ export const nodeHelpers = mixins(
}, },
}, },
}); });
function isHttpRequestNodeV2(node: INodeUi) {
return node.type === HTTP_REQUEST_NODE_TYPE && node.typeVersion === 2;
}
/**
* Whether the node has no selected credentials, or none of the node's
* selected credentials are of the specified type.
*/
function selectedCredsAreUnusable(node: INodeUi, credentialType: string) {
return node.credentials === undefined || Object.keys(node.credentials).includes(credentialType) === false;
}
/**
* Whether the node's selected credentials of the specified type
* can no longer be found in the database.
*/
function selectedCredsWereDeleted(
node: INodeUi,
nodeCredentialType: string,
storedCredsByType: ICredentialsResponse[] | null,
) {
if (!node.credentials || !storedCredsByType) return false;
const selectedCredsByType = node.credentials[nodeCredentialType];
if (!selectedCredsByType) return false;
return !storedCredsByType.find((c) => c.id === selectedCredsByType.id);
}
function reportUnsetCredential(credentialType: ICredentialType) {
return {
credentials: {
[credentialType.name]: [`Credentials for "${credentialType.displayName}" are not set.`],
},
};
}

View file

@ -5,6 +5,7 @@ import {
START_NODE_TYPE, START_NODE_TYPE,
WEBHOOK_NODE_TYPE, WEBHOOK_NODE_TYPE,
VIEWS, VIEWS,
HTTP_REQUEST_NODE_TYPE,
} from '@/constants'; } from '@/constants';
import { import {
@ -326,10 +327,17 @@ export const workflowHelpers = mixins(
const nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false, node); const nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false, node);
nodeData.parameters = nodeParameters !== null ? nodeParameters : {}; nodeData.parameters = nodeParameters !== null ? nodeParameters : {};
const fullAccess = node.type === HTTP_REQUEST_NODE_TYPE && node.typeVersion === 2;
// Add the node credentials if there are some set and if they should be displayed // Add the node credentials if there are some set and if they should be displayed
if (node.credentials !== undefined && nodeType.credentials !== undefined) { if (node.credentials !== undefined && nodeType.credentials !== undefined) {
const saveCredenetials: INodeCredentials = {}; const saveCredenetials: INodeCredentials = {};
for (const nodeCredentialTypeName of Object.keys(node.credentials)) { for (const nodeCredentialTypeName of Object.keys(node.credentials)) {
if (fullAccess) {
saveCredenetials[nodeCredentialTypeName] = node.credentials[nodeCredentialTypeName];
continue;
}
const credentialTypeDescription = nodeType.credentials const credentialTypeDescription = nodeType.credentials
.find((credentialTypeDescription) => credentialTypeDescription.name === nodeCredentialTypeName); .find((credentialTypeDescription) => credentialTypeDescription.name === nodeCredentialTypeName);

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-node-dev", "name": "n8n-node-dev",
"version": "0.54.0", "version": "0.55.0",
"description": "CLI to simplify n8n credentials/node development", "description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -61,8 +61,8 @@
"change-case": "^4.1.1", "change-case": "^4.1.1",
"copyfiles": "^2.1.1", "copyfiles": "^2.1.1",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "~0.115.0", "n8n-core": "~0.116.0",
"n8n-workflow": "~0.97.0", "n8n-workflow": "~0.98.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",

View file

@ -52,7 +52,7 @@ export class HttpRequest implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
authenticateWith: [ authenticateWith: [
'basicAuth', 'httpBasicAuth',
], ],
'@version': [ '@version': [
2, 2,
@ -66,7 +66,7 @@ export class HttpRequest implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
authenticateWith: [ authenticateWith: [
'digestAuth', 'httpDigestAuth',
], ],
'@version': [ '@version': [
2, 2,
@ -80,7 +80,7 @@ export class HttpRequest implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
authenticateWith: [ authenticateWith: [
'headerAuth', 'httpHeaderAuth',
], ],
'@version': [ '@version': [
2, 2,
@ -94,7 +94,7 @@ export class HttpRequest implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
authenticateWith: [ authenticateWith: [
'queryAuth', 'httpQueryAuth',
], ],
'@version': [ '@version': [
2, 2,
@ -108,7 +108,7 @@ export class HttpRequest implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
authenticateWith: [ authenticateWith: [
'oAuth1', 'oAuth1Api',
], ],
'@version': [ '@version': [
2, 2,
@ -122,7 +122,7 @@ export class HttpRequest implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
authenticateWith: [ authenticateWith: [
'oAuth2', 'oAuth2Api',
], ],
'@version': [ '@version': [
2, 2,
@ -279,30 +279,30 @@ export class HttpRequest implements INodeType {
options: [ options: [
{ {
name: 'Basic Auth', name: 'Basic Auth',
value: 'basicAuth', value: 'httpBasicAuth',
}, },
{ {
name: 'Digest Auth', name: 'Digest Auth',
value: 'digestAuth', value: 'httpDigestAuth',
}, },
{ {
name: 'Header Auth', name: 'Header Auth',
value: 'headerAuth', value: 'httpHeaderAuth',
}, },
{ {
name: 'Query Auth', name: 'Query Auth',
value: 'queryAuth', value: 'httpQueryAuth',
}, },
{ {
name: 'OAuth1', name: 'OAuth1',
value: 'oAuth1', value: 'oAuth1Api',
}, },
{ {
name: 'OAuth2', name: 'OAuth2',
value: 'oAuth2', value: 'oAuth2Api',
}, },
], ],
default: 'basicAuth', default: 'httpBasicAuth',
displayOptions: { displayOptions: {
show: { show: {
authenticateWith: [ authenticateWith: [

View file

@ -999,11 +999,11 @@ export const contactFields: INodeProperties[] = [
}, },
{ {
name: 'Contains Exactly', name: 'Contains Exactly',
value: 'CONSTAIN_TOKEN', value: 'CONTAINS_TOKEN',
}, },
{ {
name: `Doesn't Contain Exactly`, name: `Doesn't Contain Exactly`,
value: 'NOT_CONSTAIN_TOKEN', value: 'NOT_CONTAINS_TOKEN',
}, },
], ],
default: 'EQ', default: 'EQ',

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-nodes-base", "name": "n8n-nodes-base",
"version": "0.172.0", "version": "0.173.0",
"description": "Base nodes of n8n", "description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -713,7 +713,7 @@
"eslint-plugin-n8n-nodes-base": "^1.0.28", "eslint-plugin-n8n-nodes-base": "^1.0.28",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"jest": "^27.4.7", "jest": "^27.4.7",
"n8n-workflow": "~0.97.0", "n8n-workflow": "~0.98.0",
"nodelinter": "^0.1.9", "nodelinter": "^0.1.9",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"tslint": "^6.1.2", "tslint": "^6.1.2",
@ -755,7 +755,7 @@
"mqtt": "4.2.6", "mqtt": "4.2.6",
"mssql": "^6.2.0", "mssql": "^6.2.0",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.115.0", "n8n-core": "~0.116.0",
"node-html-markdown": "^1.1.3", "node-html-markdown": "^1.1.3",
"node-ssh": "^12.0.0", "node-ssh": "^12.0.0",
"nodemailer": "^6.5.0", "nodemailer": "^6.5.0",

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-workflow", "name": "n8n-workflow",
"version": "0.97.0", "version": "0.98.0",
"description": "Workflow base code of n8n", "description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",