🔀 Merge branch 'master' into 'feature/n8n-public-api''

This commit is contained in:
ricardo 2022-05-02 09:48:18 -04:00
commit 55c62b02f1
127 changed files with 4743 additions and 1595 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)
### Bug Fixes

4517
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -28,7 +28,7 @@ export class DbRevertMigrationCommand extends Command {
let connection: Connection | undefined;
try {
await Db.init();
connection = Db.collections.Credentials?.manager.connection;
connection = Db.collections.Credentials.manager.connection;
if (!connection) {
throw new Error(`No database connection available.`);

View file

@ -82,7 +82,7 @@ export class ImportWorkflowsCommand extends Command {
// Make sure the settings exist
await UserSettings.prepareUserSettings();
const credentials = (await Db.collections.Credentials?.find()) ?? [];
const credentials = (await Db.collections.Credentials.find()) ?? [];
let totalImported = 0;

View file

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

View file

@ -70,9 +70,7 @@ export class ActiveExecutions {
const execution = ResponseHelper.flattenExecutionData(fullExecutionData);
const executionResult = await Db.collections.Execution!.save(
execution as IExecutionFlattedDb,
);
const executionResult = await Db.collections.Execution.save(execution as IExecutionFlattedDb);
executionId =
typeof executionResult.id === 'object'
? // @ts-ignore
@ -87,8 +85,7 @@ export class ActiveExecutions {
waitTill: null,
};
// @ts-ignore
await Db.collections.Execution!.update(executionId, execution);
await Db.collections.Execution.update(executionId, execution);
}
// @ts-ignore

View file

@ -83,7 +83,7 @@ export class ActiveWorkflowRunner {
// This is not officially supported but there is no reason
// it should not work.
// Clear up active workflow table
await Db.collections.Webhook?.clear();
await Db.collections.Webhook.clear();
}
this.activeWorkflows = new ActiveWorkflows();
@ -189,7 +189,7 @@ export class ActiveWorkflowRunner {
path = path.slice(0, -1);
}
let webhook = (await Db.collections.Webhook?.findOne({
let webhook = (await Db.collections.Webhook.findOne({
webhookPath: path,
method: httpMethod,
})) as IWebhookDb;
@ -200,7 +200,7 @@ export class ActiveWorkflowRunner {
// check if a dynamic webhook path exists
const pathElements = path.split('/');
webhookId = pathElements.shift();
const dynamicWebhooks = await Db.collections.Webhook?.find({
const dynamicWebhooks = await Db.collections.Webhook.find({
webhookId,
method: httpMethod,
pathLength: pathElements.length,
@ -332,7 +332,7 @@ export class ActiveWorkflowRunner {
* @memberof ActiveWorkflowRunner
*/
async getWebhookMethods(path: string): Promise<string[]> {
const webhooks = await Db.collections.Webhook?.find({ webhookPath: path });
const webhooks = await Db.collections.Webhook.find({ webhookPath: path });
// Gather all request methods in string array
const webhookMethods: string[] = webhooks.map((webhook) => webhook.method);
@ -443,7 +443,7 @@ export class ActiveWorkflowRunner {
try {
// eslint-disable-next-line no-await-in-loop
await Db.collections.Webhook?.insert(webhook);
await Db.collections.Webhook.insert(webhook);
const webhookExists = await workflow.runWebhookMethod(
'checkExists',
webhookData,
@ -556,7 +556,7 @@ export class ActiveWorkflowRunner {
workflowId: workflowData.id,
} as IWebhookDb;
await Db.collections.Webhook?.delete(webhook);
await Db.collections.Webhook.delete(webhook);
}
/**

View file

@ -342,6 +342,7 @@ export class CredentialsHelper extends ICredentialsHelper {
decryptedDataOriginal as INodeParameters,
true,
false,
null,
) as ICredentialDataDecryptedObject;
if (decryptedDataOriginal.oauthTokenData !== undefined) {
@ -436,8 +437,6 @@ export class CredentialsHelper extends ICredentialsHelper {
// Add special database related data
newCredentialsData.updatedAt = new Date();
// TODO: also add user automatically depending on who is logged in, if anybody is logged in
// Save the credentials in DB
const findQuery = {
id: credentials.id,
@ -562,7 +561,9 @@ export class CredentialsHelper extends ICredentialsHelper {
parameters: {},
name: 'Temp-Node',
type: nodeType.description.name,
typeVersion: nodeType.description.version,
typeVersion: Array.isArray(nodeType.description.version)
? nodeType.description.version.slice(-1)[0]
: nodeType.description.version,
position: [0, 0],
};

View file

@ -35,15 +35,12 @@ import { readFile } from 'fs/promises';
import _, { cloneDeep } from 'lodash';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import {
FindConditions,
FindManyOptions,
getConnection,
getConnectionManager,
In,
IsNull,
LessThan,
LessThanOrEqual,
MoreThan,
Not,
Raw,
} from 'typeorm';
@ -61,13 +58,7 @@ import { createHmac, randomBytes } from 'crypto';
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs';
import {
BinaryDataManager,
Credentials,
IBinaryDataConfig,
LoadNodeParameterOptions,
UserSettings,
} from 'n8n-core';
import { BinaryDataManager, Credentials, LoadNodeParameterOptions, UserSettings } from 'n8n-core';
import {
ICredentialType,
@ -154,7 +145,6 @@ import { WEBHOOK_METHODS } from './WebhookHelpers';
import { userManagementRouter } from './UserManagement';
import { resolveJwt } from './UserManagement/auth/jwt';
import { User } from './databases/entities/User';
import { CredentialsEntity } from './databases/entities/CredentialsEntity';
import type {
AuthenticatedRequest,
CredentialRequest,
@ -169,7 +159,11 @@ import { ExecutionEntity } from './databases/entities/ExecutionEntity';
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
import { credentialsController } from './api/credentials.api';
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper';
import {
getInstanceBaseUrl,
isEmailSetUp,
isUserManagementEnabled,
} from './UserManagement/UserManagementHelper';
import { loadPublicApiVersions } from './PublicApi';
require('body-parser-xml')(bodyParser);
@ -315,9 +309,7 @@ class App {
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'),
userManagement: {
enabled:
config.getEnv('userManagement.disabled') === false ||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true,
enabled: isUserManagementEnabled(),
showSetupOnFirstLoad:
config.getEnv('userManagement.disabled') === false &&
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
@ -353,9 +345,7 @@ class App {
getSettingsForFrontend(): IN8nUISettings {
// refresh user management status
Object.assign(this.frontendSettings.userManagement, {
enabled:
config.getEnv('userManagement.disabled') === false ||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true,
enabled: isUserManagementEnabled(),
showSetupOnFirstLoad:
config.getEnv('userManagement.disabled') === false &&
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
@ -601,12 +591,14 @@ class App {
return;
}
try {
const authCookie = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
await resolveJwt(authCookie);
} catch (error) {
res.status(401).send('Unauthorized');
return;
if (isUserManagementEnabled()) {
try {
const authCookie = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
await resolveJwt(authCookie);
} catch (error) {
res.status(401).send('Unauthorized');
return;
}
}
this.push.add(req.query.sessionId as string, req, res);
@ -790,7 +782,7 @@ class App {
const { tags: tagIds } = req.body;
if (tagIds?.length && !config.getEnv('workflowTagsDisabled')) {
newWorkflow.tags = await Db.collections.Tag!.findByIds(tagIds, {
newWorkflow.tags = await Db.collections.Tag.findByIds(tagIds, {
select: ['id', 'name'],
});
}
@ -802,7 +794,7 @@ class App {
await getConnection().transaction(async (transactionManager) => {
savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
const role = await Db.collections.Role!.findOneOrFail({
const role = await Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'workflow',
});
@ -912,13 +904,13 @@ class App {
}
if (req.user.globalRole.name === 'owner') {
workflows = await Db.collections.Workflow!.find(
workflows = await Db.collections.Workflow.find(
Object.assign(query, {
where: filter,
}),
);
} else {
const shared = await Db.collections.SharedWorkflow!.find({
const shared = await Db.collections.SharedWorkflow.find({
relations: ['workflow'],
where: whereClause({
user: req.user,
@ -928,7 +920,7 @@ class App {
if (!shared.length) return [];
workflows = await Db.collections.Workflow!.find(
workflows = await Db.collections.Workflow.find(
Object.assign(query, {
where: {
id: In(shared.map(({ workflow }) => workflow.id)),
@ -971,7 +963,7 @@ class App {
relations = relations.filter((relation) => relation !== 'workflow.tags');
}
const shared = await Db.collections.SharedWorkflow!.findOne({
const shared = await Db.collections.SharedWorkflow.findOne({
relations,
where: whereClause({
user: req.user,
@ -1013,7 +1005,7 @@ class App {
const { tags, ...rest } = req.body;
Object.assign(updateData, rest);
const shared = await Db.collections.SharedWorkflow!.findOne({
const shared = await Db.collections.SharedWorkflow.findOne({
relations: ['workflow'],
where: whereClause({
user: req.user,
@ -1075,7 +1067,7 @@ class App {
await validateEntity(updateData);
}
await Db.collections.Workflow!.update(workflowId, updateData);
await Db.collections.Workflow.update(workflowId, updateData);
if (tags && !config.getEnv('workflowTagsDisabled')) {
const tablePrefix = config.getEnv('database.tablePrefix');
@ -1096,7 +1088,7 @@ class App {
// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
const updatedWorkflow = await Db.collections.Workflow!.findOne(workflowId, options);
const updatedWorkflow = await Db.collections.Workflow.findOne(workflowId, options);
if (updatedWorkflow === undefined) {
throw new ResponseHelper.ResponseError(
@ -1113,7 +1105,6 @@ class App {
}
await this.externalHooks.run('workflow.afterUpdate', [updatedWorkflow]);
// @ts-ignore
void InternalHooksManager.getInstance().onWorkflowSaved(req.user.id, updatedWorkflow);
if (updatedWorkflow.active) {
@ -1127,8 +1118,7 @@ class App {
} catch (error) {
// If workflow could not be activated set it again to inactive
updateData.active = false;
// @ts-ignore
await Db.collections.Workflow!.update(workflowId, updateData);
await Db.collections.Workflow.update(workflowId, updateData);
// Also set it in the returned data
updatedWorkflow.active = false;
@ -1155,7 +1145,7 @@ class App {
await this.externalHooks.run('workflow.delete', [workflowId]);
const shared = await Db.collections.SharedWorkflow!.findOne({
const shared = await Db.collections.SharedWorkflow.findOne({
relations: ['workflow'],
where: whereClause({
user: req.user,
@ -1181,7 +1171,7 @@ class App {
await this.activeWorkflowRunner.remove(workflowId);
}
await Db.collections.Workflow!.delete(workflowId);
await Db.collections.Workflow.delete(workflowId);
void InternalHooksManager.getInstance().onWorkflowDeleted(req.user.id, workflowId);
await this.externalHooks.run('workflow.afterDelete', [workflowId]);
@ -1280,7 +1270,7 @@ class App {
return TagHelpers.getTagsWithCountDb(tablePrefix);
}
return Db.collections.Tag!.find({ select: ['id', 'name'] });
return Db.collections.Tag.find({ select: ['id', 'name'] });
},
),
);
@ -1299,7 +1289,7 @@ class App {
await this.externalHooks.run('tag.beforeCreate', [newTag]);
await validateEntity(newTag);
const tag = await Db.collections.Tag!.save(newTag);
const tag = await Db.collections.Tag.save(newTag);
await this.externalHooks.run('tag.afterCreate', [tag]);
@ -1328,7 +1318,7 @@ class App {
await this.externalHooks.run('tag.beforeUpdate', [newTag]);
await validateEntity(newTag);
const tag = await Db.collections.Tag!.save(newTag);
const tag = await Db.collections.Tag.save(newTag);
await this.externalHooks.run('tag.afterUpdate', [tag]);
@ -1360,7 +1350,7 @@ class App {
await this.externalHooks.run('tag.beforeDelete', [id]);
await Db.collections.Tag!.delete({ id });
await Db.collections.Tag.delete({ id });
await this.externalHooks.run('tag.afterDelete', [id]);
@ -1625,7 +1615,7 @@ class App {
ResponseHelper.send(async (req: WorkflowRequest.GetAllActivationErrors) => {
const { id: workflowId } = req.params;
const shared = await Db.collections.SharedWorkflow!.findOne({
const shared = await Db.collections.SharedWorkflow.findOne({
relations: ['workflow'],
where: whereClause({
user: req.user,
@ -1828,7 +1818,7 @@ class App {
newCredentialsData.updatedAt = this.getCurrentDate();
// Update the credentials in DB
await Db.collections.Credentials!.update(credentialId, newCredentialsData);
await Db.collections.Credentials.update(credentialId, newCredentialsData);
LoggerProxy.verbose('OAuth1 authorization successful for new credential', {
userId: req.user.id,
@ -1945,7 +1935,7 @@ class App {
// Add special database related data
newCredentialsData.updatedAt = this.getCurrentDate();
// Save the credentials in DB
await Db.collections.Credentials!.update(credentialId, newCredentialsData);
await Db.collections.Credentials.update(credentialId, newCredentialsData);
LoggerProxy.verbose('OAuth1 callback successful for new credential', {
userId: req.user?.id,
@ -2060,7 +2050,7 @@ class App {
newCredentialsData.updatedAt = this.getCurrentDate();
// Update the credentials in DB
await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData);
await Db.collections.Credentials.update(req.query.id as string, newCredentialsData);
const authQueryParameters = _.get(oauthCredentials, 'authQueryParameters', '') as string;
let returnUri = oAuthObj.code.getUri();
@ -2247,7 +2237,7 @@ class App {
// Add special database related data
newCredentialsData.updatedAt = this.getCurrentDate();
// Save the credentials in DB
await Db.collections.Credentials!.update(state.cid, newCredentialsData);
await Db.collections.Credentials.update(state.cid, newCredentialsData);
LoggerProxy.verbose('OAuth2 callback successful for new credential', {
userId: req.user?.id,
credentialId: state.cid,
@ -2353,7 +2343,7 @@ class App {
});
}
const executions = await Db.collections.Execution!.find(findOptions);
const executions = await Db.collections.Execution.find(findOptions);
const { count, estimated } = await getExecutionsCount(countFilter, req.user);
@ -2394,7 +2384,7 @@ class App {
if (!sharedWorkflowIds.length) return undefined;
const execution = await Db.collections.Execution!.findOne({
const execution = await Db.collections.Execution.findOne({
where: {
id: executionId,
workflowId: In(sharedWorkflowIds),
@ -2437,7 +2427,7 @@ class App {
if (!sharedWorkflowIds.length) return false;
const execution = await Db.collections.Execution!.findOne({
const execution = await Db.collections.Execution.findOne({
where: {
id: executionId,
workflowId: In(sharedWorkflowIds),
@ -2499,9 +2489,7 @@ class App {
// Loads the currently saved workflow to execute instead of the
// one saved at the time of the execution.
const workflowId = fullExecutionData.workflowData.id;
const workflowData = (await Db.collections.Workflow!.findOne(
workflowId,
)) as IWorkflowBase;
const workflowData = (await Db.collections.Workflow.findOne(workflowId)) as IWorkflowBase;
if (workflowData === undefined) {
throw new Error(
@ -2583,7 +2571,7 @@ class App {
Object.assign(filters, requestFilters);
}
const executions = await Db.collections.Execution!.find({
const executions = await Db.collections.Execution.find({
where: {
workflowId: In(sharedWorkflowIds),
...filters,
@ -2598,7 +2586,7 @@ class App {
idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)),
);
await Db.collections.Execution!.delete({ id: In(idsToDelete) });
await Db.collections.Execution.delete({ id: In(idsToDelete) });
return;
}
@ -2606,7 +2594,7 @@ class App {
// delete executions by IDs, if user may access the underyling worfklows
if (ids) {
const executions = await Db.collections.Execution!.find({
const executions = await Db.collections.Execution.find({
where: {
id: In(ids),
workflowId: In(sharedWorkflowIds),
@ -2627,7 +2615,7 @@ class App {
idsToDelete.map(async (id) => binaryDataManager.deleteBinaryDataByExecutionId(id)),
);
await Db.collections.Execution!.delete(idsToDelete);
await Db.collections.Execution.delete(idsToDelete);
}
}),
);
@ -2678,7 +2666,7 @@ class App {
Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) });
}
const executions = await Db.collections.Execution!.find(findOptions);
const executions = await Db.collections.Execution.find(findOptions);
if (!executions.length) return [];
@ -2739,7 +2727,7 @@ class App {
throw new ResponseHelper.ResponseError('Execution not found', undefined, 404);
}
const execution = await Db.collections.Execution!.findOne({
const execution = await Db.collections.Execution.findOne({
where: {
id: executionId,
workflowId: In(sharedWorkflowIds),
@ -2782,7 +2770,7 @@ class App {
await Queue.getInstance().stopJob(job);
}
const executionDb = (await Db.collections.Execution?.findOne(
const executionDb = (await Db.collections.Execution.findOne(
req.params.id,
)) as IExecutionFlattedDb;
const fullExecutionData = ResponseHelper.unflattenExecutionData(executionDb);
@ -3079,9 +3067,7 @@ export async function start(): Promise<void> {
},
deploymentType: config.getEnv('deployment.type'),
binaryDataMode: binarDataConfig.mode,
n8n_multi_user_allowed:
config.getEnv('userManagement.disabled') === false ||
config.getEnv('userManagement.isInstanceOwnerSetUp') === true,
n8n_multi_user_allowed: isUserManagementEnabled(),
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
};
@ -3117,7 +3103,7 @@ async function getExecutionsCount(
if (dbType !== 'postgresdb' || filteredFields.length > 0 || user.globalRole.name !== 'owner') {
const sharedWorkflowIds = await getSharedWorkflowIds(user);
const count = await Db.collections.Execution!.count({
const count = await Db.collections.Execution.count({
where: {
workflowId: In(sharedWorkflowIds),
...countFilter,
@ -3131,7 +3117,7 @@ async function getExecutionsCount(
// Get an estimate of rows count.
const estimateRowsNumberSql =
"SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';";
const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(
const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution.query(
estimateRowsNumberSql,
);
@ -3148,7 +3134,7 @@ async function getExecutionsCount(
const sharedWorkflowIds = await getSharedWorkflowIds(user);
const count = await Db.collections.Execution!.count({
const count = await Db.collections.Execution.count({
where: {
workflowId: In(sharedWorkflowIds),
},

View file

@ -32,6 +32,20 @@ export function isEmailSetUp(): boolean {
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> {
const ownerRole = await Db.collections.Role.findOneOrFail({
where: {
@ -136,6 +150,10 @@ export async function checkPermissionsForExecution(
// Iterate over all nodes
nodeNames.forEach((nodeName) => {
const node = workflow.nodes[nodeName];
if (node.disabled === true) {
// If a node is disabled there is no need to check its credentials
return;
}
// And check if any of the nodes uses credentials.
if (node.credentials) {
const credentialNames = Object.keys(node.credentials);
@ -146,9 +164,14 @@ export async function checkPermissionsForExecution(
// workflow. Nowaways it should not happen anymore.
// Migrations should handle the case where a credential does
// not have an id.
if (credentialDetail.id === null) {
throw new Error(
`The credential on node '${node.name}' is not valid. Please open the workflow and set it to a valid value.`,
);
}
if (!credentialDetail.id) {
throw new Error(
'Error initializing workflow: credential ID not present. Please open the workflow and save it to fix this error.',
`Error initializing workflow: credential ID not present. Please open the workflow and save it to fix this error. [Node: '${node.name}']`,
);
}
credentialIds.add(credentialDetail.id.toString());

View file

@ -19,7 +19,13 @@ import { usersNamespace } from './users';
import { passwordResetNamespace } from './passwordReset';
import { AuthenticatedRequest } from '../../requests';
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 {
// 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((req: Request, res: Response, next: NextFunction) => {
this.app.use(async (req: Request, res: Response, next: NextFunction) => {
if (
// TODO: refactor me!!!
// skip authentication for preflight requests
@ -73,6 +79,17 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint
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);
});

View file

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

View file

@ -50,7 +50,7 @@ export class WaitingWebhooks {
const executionId = pathParts.shift();
const path = pathParts.join('/');
const execution = await Db.collections.Execution?.findOne(executionId);
const execution = await Db.collections.Execution.findOne(executionId);
if (execution === undefined) {
throw new ResponseHelper.ResponseError(

View file

@ -584,7 +584,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
if (fullRunData.finished === true && this.retryOf !== undefined) {
// If the retry was successful save the reference it on the original execution
// await Db.collections.Execution!.save(executionData as IExecutionFlattedDb);
// await Db.collections.Execution.save(executionData as IExecutionFlattedDb);
await Db.collections.Execution.update(this.retryOf, {
retrySuccessId: this.executionId,
});

View file

@ -479,7 +479,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
credentialsByName[nodeCredentialType] = {};
}
if (credentialsByName[nodeCredentialType][name] === undefined) {
const credentials = await Db.collections.Credentials?.find({
const credentials = await Db.collections.Credentials.find({
name,
type: nodeCredentialType,
});
@ -515,7 +515,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
// check if credentials for ID-type are not yet cached
if (credentialsById[nodeCredentialType][nodeCredentials.id] === undefined) {
// check first if ID-type combination exists
const credentials = await Db.collections.Credentials?.findOne({
const credentials = await Db.collections.Credentials.findOne({
id: nodeCredentials.id,
type: nodeCredentialType,
});
@ -529,7 +529,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
continue;
}
// no credentials found for ID, check if some exist for name
const credsByName = await Db.collections.Credentials?.find({
const credsByName = await Db.collections.Credentials.find({
name: nodeCredentials.name,
type: nodeCredentialType,
});

View file

@ -33,7 +33,7 @@ beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', true);
await Db.collections.Settings!.update(
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(true) },
);
@ -102,7 +102,7 @@ test('GET /login should return cookie if UM is disabled', async () => {
config.set('userManagement.isInstanceOwnerSetUp', false);
await Db.collections.Settings!.update(
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(false) },
);

View file

@ -1,157 +0,0 @@
import express from 'express';
import validator from 'validator';
import { v4 as uuid } from 'uuid';
import * as config from '../../config';
import * as utils from './shared/utils';
import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants';
import { Db } from '../../src';
import { Role } from '../../src/databases/entities/Role';
import { randomEmail, randomValidPassword, randomName } from './shared/random';
import { getGlobalOwnerRole } from './shared/testDb';
import * as testDb from './shared/testDb';
jest.mock('../../src/telemetry');
let globalOwnerRole: Role;
let app: express.Application;
let testDbName = '';
beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true });
const initResult = await testDb.init();
testDbName = initResult.testDbName;
await testDb.truncate(['User'], testDbName);
globalOwnerRole = await getGlobalOwnerRole();
utils.initTestLogger();
utils.initTestTelemetry();
});
beforeEach(async () => {
await testDb.createUser({
id: uuid(),
email: TEST_USER.email,
firstName: TEST_USER.firstName,
lastName: TEST_USER.lastName,
password: TEST_USER.password,
globalRole: globalOwnerRole,
});
config.set('userManagement.isInstanceOwnerSetUp', true);
await Db.collections.Settings!.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(true) },
);
});
afterEach(async () => {
await testDb.truncate(['User'], testDbName);
});
afterAll(async () => {
await testDb.terminate(testDbName);
});
test('POST /login should log user in', async () => {
const authlessAgent = utils.createAgent(app);
await Promise.all(
[
{
email: TEST_USER.email,
password: TEST_USER.password,
},
{
email: TEST_USER.email.toUpperCase(),
password: TEST_USER.password,
},
].map(async (payload) => {
const response = await authlessAgent.post('/login').send(payload);
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(TEST_USER.email);
expect(firstName).toBe(TEST_USER.firstName);
expect(lastName).toBe(TEST_USER.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
}),
);
});
test('GET /login should receive logged in user', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(TEST_USER.email);
expect(firstName).toBe(TEST_USER.firstName);
expect(lastName).toBe(TEST_USER.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(response.headers['set-cookie']).toBeUndefined();
});
test('POST /logout should log user out', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.post('/logout');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
const TEST_USER = {
email: randomEmail(),
password: randomValidPassword(),
firstName: randomName(),
lastName: randomName(),
};

View file

@ -62,14 +62,14 @@ test('POST /credentials should create cred', async () => {
expect(nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(payload.data);
const credential = await Db.collections.Credentials!.findOneOrFail(id);
const credential = await Db.collections.Credentials.findOneOrFail(id);
expect(credential.name).toBe(payload.name);
expect(credential.type).toBe(payload.type);
expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType);
expect(credential.data).not.toBe(payload.data);
const sharedCredential = await Db.collections.SharedCredentials!.findOneOrFail({
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['user', 'credentials'],
where: { credentials: credential },
});
@ -131,11 +131,11 @@ test('DELETE /credentials/:id should delete owned cred for owner', async () => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ data: true });
const deletedCredential = await Db.collections.Credentials!.findOne(savedCredential.id);
const deletedCredential = await Db.collections.Credentials.findOne(savedCredential.id);
expect(deletedCredential).toBeUndefined(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials!.findOne();
const deletedSharedCredential = await Db.collections.SharedCredentials.findOne();
expect(deletedSharedCredential).toBeUndefined(); // deleted
});
@ -151,11 +151,11 @@ test('DELETE /credentials/:id should delete non-owned cred for owner', async ()
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ data: true });
const deletedCredential = await Db.collections.Credentials!.findOne(savedCredential.id);
const deletedCredential = await Db.collections.Credentials.findOne(savedCredential.id);
expect(deletedCredential).toBeUndefined(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials!.findOne();
const deletedSharedCredential = await Db.collections.SharedCredentials.findOne();
expect(deletedSharedCredential).toBeUndefined(); // deleted
});
@ -170,11 +170,11 @@ test('DELETE /credentials/:id should delete owned cred for member', async () =>
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ data: true });
const deletedCredential = await Db.collections.Credentials!.findOne(savedCredential.id);
const deletedCredential = await Db.collections.Credentials.findOne(savedCredential.id);
expect(deletedCredential).toBeUndefined(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials!.findOne();
const deletedSharedCredential = await Db.collections.SharedCredentials.findOne();
expect(deletedSharedCredential).toBeUndefined(); // deleted
});
@ -189,11 +189,11 @@ test('DELETE /credentials/:id should not delete non-owned cred for member', asyn
expect(response.statusCode).toBe(404);
const shellCredential = await Db.collections.Credentials!.findOne(savedCredential.id);
const shellCredential = await Db.collections.Credentials.findOne(savedCredential.id);
expect(shellCredential).toBeDefined(); // not deleted
const deletedSharedCredential = await Db.collections.SharedCredentials!.findOne();
const deletedSharedCredential = await Db.collections.SharedCredentials.findOne();
expect(deletedSharedCredential).toBeDefined(); // not deleted
});
@ -226,14 +226,14 @@ test('PATCH /credentials/:id should update owned cred for owner', async () => {
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials!.findOneOrFail(id);
const credential = await Db.collections.Credentials.findOneOrFail(id);
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials!.findOneOrFail({
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentials: credential },
});
@ -261,14 +261,14 @@ test('PATCH /credentials/:id should update non-owned cred for owner', async () =
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials!.findOneOrFail(id);
const credential = await Db.collections.Credentials.findOneOrFail(id);
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials!.findOneOrFail({
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentials: credential },
});
@ -295,14 +295,14 @@ test('PATCH /credentials/:id should update owned cred for member', async () => {
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials!.findOneOrFail(id);
const credential = await Db.collections.Credentials.findOneOrFail(id);
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials!.findOneOrFail({
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentials: credential },
});
@ -323,7 +323,7 @@ test('PATCH /credentials/:id should not update non-owned cred for member', async
expect(response.statusCode).toBe(404);
const shellCredential = await Db.collections.Credentials!.findOneOrFail(savedCredential.id);
const shellCredential = await Db.collections.Credentials.findOneOrFail(savedCredential.id);
expect(shellCredential.name).not.toBe(patchPayload.name); // not updated
});

View file

@ -101,7 +101,7 @@ describe('Owner shell', () => {
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const storedOwnerShell = await Db.collections.User!.findOneOrFail(id);
const storedOwnerShell = await Db.collections.User.findOneOrFail(id);
expect(storedOwnerShell.email).toBe(validPayload.email.toLowerCase());
expect(storedOwnerShell.firstName).toBe(validPayload.firstName);
@ -117,7 +117,7 @@ describe('Owner shell', () => {
const response = await authOwnerShellAgent.patch('/me').send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedOwnerShell = await Db.collections.User!.findOneOrFail();
const storedOwnerShell = await Db.collections.User.findOneOrFail();
expect(storedOwnerShell.email).toBeNull();
expect(storedOwnerShell.firstName).toBeNull();
expect(storedOwnerShell.lastName).toBeNull();
@ -140,7 +140,7 @@ describe('Owner shell', () => {
const response = await authOwnerShellAgent.patch('/me/password').send(payload);
expect([400, 500].includes(response.statusCode)).toBe(true);
const storedMember = await Db.collections.User!.findOneOrFail();
const storedMember = await Db.collections.User.findOneOrFail();
if (payload.newPassword) {
expect(storedMember.password).not.toBe(payload.newPassword);
@ -152,7 +152,7 @@ describe('Owner shell', () => {
}),
);
const storedOwnerShell = await Db.collections.User!.findOneOrFail();
const storedOwnerShell = await Db.collections.User.findOneOrFail();
expect(storedOwnerShell.password).toBeNull();
});
@ -168,7 +168,7 @@ describe('Owner shell', () => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
const storedShellOwner = await Db.collections.User!.findOneOrFail({
const storedShellOwner = await Db.collections.User.findOneOrFail({
where: { email: IsNull() },
});
@ -181,7 +181,7 @@ describe('Member', () => {
beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', true);
await Db.collections.Settings!.update(
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(true) },
);
@ -255,7 +255,7 @@ describe('Member', () => {
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
const storedMember = await Db.collections.User!.findOneOrFail(id);
const storedMember = await Db.collections.User.findOneOrFail(id);
expect(storedMember.email).toBe(validPayload.email.toLowerCase());
expect(storedMember.firstName).toBe(validPayload.firstName);
@ -271,7 +271,7 @@ describe('Member', () => {
const response = await authMemberAgent.patch('/me').send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedMember = await Db.collections.User!.findOneOrFail();
const storedMember = await Db.collections.User.findOneOrFail();
expect(storedMember.email).toBe(member.email);
expect(storedMember.firstName).toBe(member.firstName);
expect(storedMember.lastName).toBe(member.lastName);
@ -295,7 +295,7 @@ describe('Member', () => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
const storedMember = await Db.collections.User!.findOneOrFail();
const storedMember = await Db.collections.User.findOneOrFail();
expect(storedMember.password).not.toBe(member.password);
expect(storedMember.password).not.toBe(validPayload.newPassword);
});
@ -308,7 +308,7 @@ describe('Member', () => {
const response = await authMemberAgent.patch('/me/password').send(payload);
expect([400, 500].includes(response.statusCode)).toBe(true);
const storedMember = await Db.collections.User!.findOneOrFail();
const storedMember = await Db.collections.User.findOneOrFail();
if (payload.newPassword) {
expect(storedMember.password).not.toBe(payload.newPassword);
@ -330,7 +330,7 @@ describe('Member', () => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
const { personalizationAnswers: storedAnswers } = await Db.collections.User!.findOneOrFail();
const { personalizationAnswers: storedAnswers } = await Db.collections.User.findOneOrFail();
expect(storedAnswers).toEqual(validPayload);
}
@ -410,7 +410,7 @@ describe('Owner', () => {
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const storedOwner = await Db.collections.User!.findOneOrFail(id);
const storedOwner = await Db.collections.User.findOneOrFail(id);
expect(storedOwner.email).toBe(validPayload.email.toLowerCase());
expect(storedOwner.firstName).toBe(validPayload.firstName);

View file

@ -81,7 +81,7 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const storedOwner = await Db.collections.User!.findOneOrFail(id);
const storedOwner = await Db.collections.User.findOneOrFail(id);
expect(storedOwner.password).not.toBe(newOwnerData.password);
expect(storedOwner.email).toBe(newOwnerData.email);
expect(storedOwner.firstName).toBe(newOwnerData.firstName);
@ -113,7 +113,7 @@ test('POST /owner should create owner with lowercased email', async () => {
expect(email).toBe(newOwnerData.email.toLowerCase());
const storedOwner = await Db.collections.User!.findOneOrFail(id);
const storedOwner = await Db.collections.User.findOneOrFail(id);
expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase());
});
@ -140,7 +140,7 @@ test('POST /owner/skip-setup should persist skipping setup to the DB', async ()
const skipConfig = config.getEnv('userManagement.skipInstanceOwnerSetup');
expect(skipConfig).toBe(true);
const { value } = await Db.collections.Settings!.findOneOrFail({
const { value } = await Db.collections.Settings.findOneOrFail({
key: 'userManagement.skipInstanceOwnerSetup',
});
expect(value).toBe('true');

View file

@ -67,7 +67,7 @@ test(
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({});
const user = await Db.collections.User!.findOneOrFail({ email: payload.email });
const user = await Db.collections.User.findOneOrFail({ email: payload.email });
expect(user.resetPasswordToken).toBeDefined();
expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
}),
@ -85,7 +85,7 @@ test('POST /forgot-password should fail if emailing is not set up', async () =>
expect(response.statusCode).toBe(500);
const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email });
const storedOwner = await Db.collections.User.findOneOrFail({ email: owner.email });
expect(storedOwner.resetPasswordToken).toBeNull();
});
@ -109,7 +109,7 @@ test('POST /forgot-password should fail with invalid inputs', async () => {
const response = await authlessAgent.post('/forgot-password').send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email });
const storedOwner = await Db.collections.User.findOneOrFail({ email: owner.email });
expect(storedOwner.resetPasswordToken).toBeNull();
}),
);
@ -133,7 +133,7 @@ test('GET /resolve-password-token should succeed with valid inputs', async () =>
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User!.update(owner.id, {
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
@ -183,7 +183,7 @@ test('GET /resolve-password-token should fail if token is expired', async () =>
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User!.update(owner.id, {
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
@ -205,7 +205,7 @@ test('POST /change-password should succeed with valid inputs', async () => {
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User!.update(owner.id, {
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
@ -223,7 +223,7 @@ test('POST /change-password should succeed with valid inputs', async () => {
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
const { password: storedPassword } = await Db.collections.User!.findOneOrFail(owner.id);
const { password: storedPassword } = await Db.collections.User.findOneOrFail(owner.id);
const comparisonResult = await compare(passwordToStore, storedPassword);
expect(comparisonResult).toBe(true);
@ -238,7 +238,7 @@ test('POST /change-password should fail with invalid inputs', async () => {
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User!.update(owner.id, {
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
@ -267,7 +267,7 @@ test('POST /change-password should fail with invalid inputs', async () => {
const response = await authlessAgent.post('/change-password').query(invalidPayload);
expect(response.statusCode).toBe(400);
const { password: storedPassword } = await Db.collections.User!.findOneOrFail();
const { password: storedPassword } = await Db.collections.User.findOneOrFail();
expect(owner.password).toBe(storedPassword);
}),
);
@ -281,7 +281,7 @@ test('POST /change-password should fail when token has expired', async () => {
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User!.update(owner.id, {
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});

View file

@ -109,7 +109,7 @@ export async function truncate(collections: CollectionName[], testDbName: string
if (dbType === 'sqlite') {
await testDb.query('PRAGMA foreign_keys=OFF');
await Promise.all(collections.map((collection) => Db.collections[collection]!.clear()));
await Promise.all(collections.map((collection) => Db.collections[collection].clear()));
return testDb.query('PRAGMA foreign_keys=ON');
}
@ -182,11 +182,11 @@ export async function saveCredential(
Object.assign(newCredential, encryptedData);
const savedCredential = await Db.collections.Credentials!.save(newCredential);
const savedCredential = await Db.collections.Credentials.save(newCredential);
savedCredential.data = newCredential.data;
await Db.collections.SharedCredentials!.save({
await Db.collections.SharedCredentials.save({
user,
credentials: savedCredential,
role,
@ -214,7 +214,7 @@ export async function createUser(attributes: Partial<User> = {}): Promise<User>
...rest,
};
return Db.collections.User!.save(user);
return Db.collections.User.save(user);
}
export function createUserShell(globalRole: Role): Promise<User> {
@ -228,7 +228,7 @@ export function createUserShell(globalRole: Role): Promise<User> {
shell.email = randomEmail();
}
return Db.collections.User!.save(shell);
return Db.collections.User.save(shell);
}
// ----------------------------------
@ -236,28 +236,28 @@ export function createUserShell(globalRole: Role): Promise<User> {
// ----------------------------------
export function getGlobalOwnerRole() {
return Db.collections.Role!.findOneOrFail({
return Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'global',
});
}
export function getGlobalMemberRole() {
return Db.collections.Role!.findOneOrFail({
return Db.collections.Role.findOneOrFail({
name: 'member',
scope: 'global',
});
}
export function getWorkflowOwnerRole() {
return Db.collections.Role!.findOneOrFail({
return Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'workflow',
});
}
export function getCredentialOwnerRole() {
return Db.collections.Role!.findOneOrFail({
return Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'credential',
});

View file

@ -221,7 +221,7 @@ export function getAuthToken(response: request.Response, authCookieName = AUTH_C
// ----------------------------------
export async function isInstanceOwnerSetUp() {
const { value } = await Db.collections.Settings!.findOneOrFail({
const { value } = await Db.collections.Settings.findOneOrFail({
key: 'userManagement.isInstanceOwnerSetUp',
});

View file

@ -119,9 +119,9 @@ test('DELETE /users/:id should delete the user', async () => {
nodes: [],
});
const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow);
const savedWorkflow = await Db.collections.Workflow.save(newWorkflow);
await Db.collections.SharedWorkflow!.save({
await Db.collections.SharedWorkflow.save({
role: workflowOwnerRole,
user: userToDelete,
workflow: savedWorkflow,
@ -136,9 +136,9 @@ test('DELETE /users/:id should delete the user', async () => {
nodesAccess: [],
});
const savedCredential = await Db.collections.Credentials!.save(newCredential);
const savedCredential = await Db.collections.Credentials.save(newCredential);
await Db.collections.SharedCredentials!.save({
await Db.collections.SharedCredentials.save({
role: credentialOwnerRole,
user: userToDelete,
credentials: savedCredential,
@ -149,27 +149,27 @@ test('DELETE /users/:id should delete the user', async () => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
const user = await Db.collections.User!.findOne(userToDelete.id);
const user = await Db.collections.User.findOne(userToDelete.id);
expect(user).toBeUndefined(); // deleted
const sharedWorkflow = await Db.collections.SharedWorkflow!.findOne({
const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({
relations: ['user'],
where: { user: userToDelete },
});
expect(sharedWorkflow).toBeUndefined(); // deleted
const sharedCredential = await Db.collections.SharedCredentials!.findOne({
const sharedCredential = await Db.collections.SharedCredentials.findOne({
relations: ['user'],
where: { user: userToDelete },
});
expect(sharedCredential).toBeUndefined(); // deleted
const workflow = await Db.collections.Workflow!.findOne(savedWorkflow.id);
const workflow = await Db.collections.Workflow.findOne(savedWorkflow.id);
expect(workflow).toBeUndefined(); // deleted
// TODO: Include active workflow and check whether webhook has been removed
const credential = await Db.collections.Credentials!.findOne(savedCredential.id);
const credential = await Db.collections.Credentials.findOne(savedCredential.id);
expect(credential).toBeUndefined(); // deleted
});
@ -181,7 +181,7 @@ test('DELETE /users/:id should fail to delete self', async () => {
expect(response.statusCode).toBe(400);
const user = await Db.collections.User!.findOne(owner.id);
const user = await Db.collections.User.findOne(owner.id);
expect(user).toBeDefined();
});
@ -197,7 +197,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () =
expect(response.statusCode).toBe(400);
const user = await Db.collections.User!.findOne(idToDelete);
const user = await Db.collections.User.findOne(idToDelete);
expect(user).toBeDefined();
});
@ -205,7 +205,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const userToDelete = await Db.collections.User!.save({
const userToDelete = await Db.collections.User.save({
id: uuid(),
email: randomEmail(),
password: randomValidPassword(),
@ -225,9 +225,9 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
nodes: [],
});
const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow);
const savedWorkflow = await Db.collections.Workflow.save(newWorkflow);
await Db.collections.SharedWorkflow!.save({
await Db.collections.SharedWorkflow.save({
role: workflowOwnerRole,
user: userToDelete,
workflow: savedWorkflow,
@ -242,9 +242,9 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
nodesAccess: [],
});
const savedCredential = await Db.collections.Credentials!.save(newCredential);
const savedCredential = await Db.collections.Credentials.save(newCredential);
await Db.collections.SharedCredentials!.save({
await Db.collections.SharedCredentials.save({
role: credentialOwnerRole,
user: userToDelete,
credentials: savedCredential,
@ -256,17 +256,17 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
expect(response.statusCode).toBe(200);
const sharedWorkflow = await Db.collections.SharedWorkflow!.findOneOrFail({
const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({
relations: ['user'],
where: { user: owner },
});
const sharedCredential = await Db.collections.SharedCredentials!.findOneOrFail({
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['user'],
where: { user: owner },
});
const deletedUser = await Db.collections.User!.findOne(userToDelete);
const deletedUser = await Db.collections.User.findOne(userToDelete);
expect(sharedWorkflow.user.id).toBe(owner.id);
expect(sharedCredential.user.id).toBe(owner.id);
@ -317,7 +317,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => {
.query({ inviteeId });
// cause inconsistent DB state
await Db.collections.User!.update(owner.id, { email: '' });
await Db.collections.User.update(owner.id, { email: '' });
const fifth = await authOwnerAgent
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
@ -369,7 +369,7 @@ test('POST /users/:id should fill out a user shell', async () => {
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
const member = await Db.collections.User!.findOneOrFail(memberShell.id);
const member = await Db.collections.User.findOneOrFail(memberShell.id);
expect(member.firstName).toBe(memberData.firstName);
expect(member.lastName).toBe(memberData.lastName);
expect(member.password).not.toBe(memberData.password);
@ -382,7 +382,7 @@ test('POST /users/:id should fail with invalid inputs', async () => {
const memberShellEmail = randomEmail();
const memberShell = await Db.collections.User!.save({
const memberShell = await Db.collections.User.save({
email: memberShellEmail,
globalRole: globalMemberRole,
});
@ -421,7 +421,7 @@ test('POST /users/:id should fail with invalid inputs', async () => {
const response = await authlessAgent.post(`/users/${memberShell.id}`).send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedUser = await Db.collections.User!.findOneOrFail({
const storedUser = await Db.collections.User.findOneOrFail({
where: { email: memberShellEmail },
});
expect(storedUser.firstName).toBeNull();
@ -448,7 +448,7 @@ test('POST /users/:id should fail with already accepted invite', async () => {
expect(response.statusCode).toBe(400);
const storedMember = await Db.collections.User!.findOneOrFail({
const storedMember = await Db.collections.User.findOneOrFail({
where: { email: member.email },
});
expect(storedMember.firstName).not.toBe(newMemberData.firstName);
@ -517,7 +517,7 @@ test(
expect(error).toBe('Email could not be sent');
}
const storedUser = await Db.collections.User!.findOneOrFail(id);
const storedUser = await Db.collections.User.findOneOrFail(id);
const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } =
storedUser;
@ -552,7 +552,7 @@ test(
const response = await authOwnerAgent.post('/users').send(invalidPayload);
expect(response.statusCode).toBe(400);
const users = await Db.collections.User!.find();
const users = await Db.collections.User.find();
expect(users.length).toBe(1); // DB unaffected
}),
);
@ -576,7 +576,7 @@ test(
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(0);
const users = await Db.collections.User!.find();
const users = await Db.collections.User.find();
expect(users.length).toBe(1);
},
SMTP_TEST_TIMEOUT,
@ -586,7 +586,7 @@ test(
// TODO: UserManagementMailer is a singleton - cannot reinstantiate with wrong creds
// test('POST /users should error for wrong SMTP config', async () => {
// const owner = await Db.collections.User!.findOneOrFail();
// const owner = await Db.collections.User.findOneOrFail();
// const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
// config.set('userManagement.emails.mode', 'smtp');

View file

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

View file

@ -1298,6 +1298,7 @@ export async function getCredentials(
!NodeHelpers.displayParameter(
additionalData.currentNodeParameters || node.parameters,
nodeCredentialDescription,
node,
node.parameters,
)
) {

View file

@ -517,6 +517,65 @@ class NodeTypesClass implements INodeTypes {
},
},
},
'n8n-nodes-base.versionTest': {
sourcePath: '',
type: {
description: {
displayName: 'Version Test',
name: 'versionTest',
group: ['input'],
version: 1,
description: 'Tests if versioning works',
defaults: {
name: 'Version Test',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Display V1',
name: 'versionTest',
type: 'number',
displayOptions: {
show: {
'@version': [1],
},
},
default: 1,
},
{
displayName: 'Display V2',
name: 'versionTest',
type: 'number',
displayOptions: {
show: {
'@version': [2],
},
},
default: 2,
},
],
},
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const newItem: INodeExecutionData = {
json: {
versionFromParameter: this.getNodeParameter('versionTest', itemIndex),
versionFromNode: this.getNode().typeVersion,
},
};
returnData.push(newItem);
}
return this.prepareOutputData(returnData);
},
},
},
'n8n-nodes-base.set': {
sourcePath: '',
type: {

View file

@ -1169,6 +1169,145 @@ describe('WorkflowExecute', () => {
},
},
},
{
description:
'should display the correct parameters and so correct data when simplified node-versioning is used',
input: {
workflowData: {
nodes: [
{
parameters: {},
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [240, 300],
},
{
parameters: {},
name: 'VersionTest1a',
type: 'n8n-nodes-base.versionTest',
typeVersion: 1,
position: [460, 300],
},
{
parameters: {
versionTest: 11,
},
name: 'VersionTest1b',
type: 'n8n-nodes-base.versionTest',
typeVersion: 1,
position: [680, 300],
},
{
parameters: {},
name: 'VersionTest2a',
type: 'n8n-nodes-base.versionTest',
typeVersion: 2,
position: [880, 300],
},
{
parameters: {
versionTest: 22,
},
name: 'VersionTest2b',
type: 'n8n-nodes-base.versionTest',
typeVersion: 2,
position: [1080, 300],
},
],
connections: {
Start: {
main: [
[
{
node: 'VersionTest1a',
type: 'main',
index: 0,
},
],
],
},
VersionTest1a: {
main: [
[
{
node: 'VersionTest1b',
type: 'main',
index: 0,
},
],
],
},
VersionTest1b: {
main: [
[
{
node: 'VersionTest2a',
type: 'main',
index: 0,
},
],
],
},
VersionTest2a: {
main: [
[
{
node: 'VersionTest2b',
type: 'main',
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: [
'Start',
'VersionTest1a',
'VersionTest1b',
'VersionTest2a',
'VersionTest2b',
],
nodeData: {
VersionTest1a: [
[
{
versionFromNode: 1,
versionFromParameter: 1,
},
],
],
VersionTest1b: [
[
{
versionFromNode: 1,
versionFromParameter: 11,
},
],
],
VersionTest2a: [
[
{
versionFromNode: 2,
versionFromParameter: 2,
},
],
],
VersionTest2b: [
[
{
versionFromNode: 2,
versionFromParameter: 22,
},
],
],
},
},
},
];
const fakeLogger = {

View file

@ -35,6 +35,11 @@ module.exports = {
],
});
config.resolve.alias = {
...config.resolve.alias,
"@/": path.resolve(__dirname, "../src/"),
};
return config;
},
};

View file

@ -1,6 +1,6 @@
{
"name": "n8n-design-system",
"version": "0.18.0",
"version": "0.19.0",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
"author": {
@ -14,12 +14,14 @@
},
"scripts": {
"build": "npm run build:theme",
"build:vue": "vue-cli-service build --target lib ./src/main.js --report",
"build:vue": "vite build",
"build:vue:typecheck": "vue-tsc --emitDeclarationOnly",
"dev": "npm run watch:theme",
"test": "npm run test:unit",
"test": "vitest run",
"test:ci": "vitest run --coverage",
"test:dev": "vitest",
"build:storybook": "build-storybook",
"storybook": "start-storybook -p 6006",
"test:unit": "vue-cli-service test:unit --passWithNoTests",
"lint": "tslint -p tsconfig.json -c tslint.json",
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json",
"build:theme": "gulp build:theme",
@ -36,14 +38,15 @@
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^2.0.2",
"core-js": "^3.6.5",
"element-ui": "~2.15.7",
"@storybook/addon-actions": "^6.3.6",
"@storybook/addon-essentials": "^6.3.6",
"@storybook/addon-links": "^6.3.6",
"@storybook/vue": "^6.3.6",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/vue": "^5.8.2",
"@types/jest": "^27.4.0",
"@types/markdown-it": "^12.2.3",
"@types/sanitize-html": "^2.6.2",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"@vue/cli-plugin-babel": "~4.5.0",
@ -55,6 +58,8 @@
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^1.0.3",
"babel-loader": "^8.2.2",
"c8": "7.11.0",
"core-js": "^3.6.5",
"eslint": "^7.32.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.16.0",
@ -62,11 +67,12 @@
"gulp-autoprefixer": "^4.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-dart-sass": "^1.0.2",
"node-notifier": ">=8.0.1",
"jsdom": "19.0.0",
"markdown-it": "^12.3.2",
"markdown-it-emoji": "^2.0.0",
"markdown-it-link-attributes": "^4.0.0",
"markdown-it-task-lists": "^2.1.1",
"node-notifier": ">=8.0.1",
"prettier": "^2.3.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
@ -74,6 +80,9 @@
"storybook-addon-themes": "^6.1.0",
"trim": ">=0.0.3",
"typescript": "~4.6.0",
"vite": "2.9.5",
"vite-plugin-vue2": "1.9.3",
"vitest": "0.9.3",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-loader": "^15.9.7",
@ -81,6 +90,12 @@
"vue-template-compiler": "^2.6.11",
"vue-typed-mixins": "^0.2.0",
"vue2-boring-avatars": "0.3.4",
"vue-tsc": "0.34.8",
"xss": "^1.0.10"
},
"dependencies": {
"element-ui": "~2.15.7",
"sanitize-html": "2.7.0",
"vue2-boring-avatars": "0.3.4"
}
}

View file

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View file

@ -58,7 +58,7 @@ import N8nOption from '../N8nOption';
import N8nInputLabel from '../N8nInputLabel';
import { getValidationError, VALIDATORS } from './validators';
import { Rule, RuleGroup, IValidator } from "../../../../editor-ui/src/Interface";
import { Rule, RuleGroup, IValidator } from "../../types";
import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins';

View file

@ -1,5 +1,5 @@
import { IValidator, RuleGroup } from "../../../../editor-ui/src/Interface";
import { IValidator, RuleGroup } from "../../types";
export const emailRegex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

View file

@ -29,7 +29,7 @@
<script lang="ts">
import Vue from 'vue';
import N8nFormInput from '../N8nFormInput';
import { IFormInputs } from '../../Interface';
import { IFormInputs } from '../../types';
import ResizeObserver from '../ResizeObserver';
export default Vue.extend({

View file

@ -0,0 +1,71 @@
/* tslint:disable:variable-name */
import N8nNotice from './Notice.vue';
import {StoryFn} from "@storybook/vue";
export default {
title: 'Atoms/Notice',
component: N8nNotice,
argTypes: {
theme: {
control: 'select',
options: ['success', 'warning', 'danger', 'info'],
},
},
};
const SlotTemplate: StoryFn = (args, {argTypes}) => ({
props: Object.keys(argTypes),
components: {
N8nNotice,
},
template: `<n8n-notice v-bind="$props">This is a notice! Thread carefully from this point forward.</n8n-notice>`,
});
const PropTemplate: StoryFn = (args, {argTypes}) => ({
props: Object.keys(argTypes),
components: {
N8nNotice,
},
template: `<n8n-notice v-bind="$props"/>`,
});
export const Warning = SlotTemplate.bind({});
Warning.args = {
theme: 'warning',
};
export const Danger = SlotTemplate.bind({});
Danger.args = {
theme: 'danger',
};
export const Success = SlotTemplate.bind({});
Success.args = {
theme: 'success',
};
export const Info = SlotTemplate.bind({});
Info.args = {
theme: 'info',
};
export const Sanitized = PropTemplate.bind({});
Sanitized.args = {
theme: 'warning',
content: '<script>alert(1)</script> This content contains a script tag and is <strong>sanitized</strong>.',
};
export const Truncated = PropTemplate.bind({});
Truncated.args = {
theme: 'warning',
truncate: true,
content: 'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
};
export const HtmlEdgeCase = PropTemplate.bind({});
HtmlEdgeCase.args = {
theme: 'warning',
truncate: true,
content: 'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.',
};

View file

@ -0,0 +1,150 @@
<template>
<div :id="id" :class="classes" role="alert">
<div class="notice-content">
<n8n-text size="small">
<slot>
<span
:class="expanded ? $style['expanded'] : $style['truncated']"
:id="`${id}-content`"
role="region"
v-html="sanitizedContent"
/>
<span v-if="canTruncate">
<a
role="button"
:aria-controls="`${id}-content`"
:aria-expanded="canTruncate && !expanded ? 'false' : 'true'"
@click="toggleExpanded"
>
{{ t(expanded ? 'notice.showLess' : 'notice.showMore') }}
</a>
</span>
</slot>
</n8n-text>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import sanitizeHtml from 'sanitize-html';
import N8nText from "../../components/N8nText";
import Locale from "../../mixins/locale";
import {uid} from "../../utils";
const DEFAULT_TRUNCATION_MAX_LENGTH = 150;
export default Vue.extend({
name: 'n8n-notice',
directives: {},
mixins: [
Locale,
],
props: {
id: {
type: String,
default: () => uid('notice'),
},
theme: {
type: String,
default: 'warning',
},
truncateAt: {
type: Number,
default: 150,
},
truncate: {
type: Boolean,
default: false,
},
content: {
type: String,
default: '',
},
},
components: {
N8nText,
},
data() {
return {
expanded: false,
};
},
computed: {
classes(): string[] {
return [
'notice',
this.$style['notice'],
this.$style[this.theme],
];
},
canTruncate(): boolean {
return this.truncate && this.content.length > this.truncateAt;
},
truncatedContent(): string {
if (!this.canTruncate || this.expanded) {
return this.content;
}
return this.content.slice(0, this.truncateAt as number) + '...';
},
sanitizedContent(): string {
return sanitizeHtml(this.truncatedContent);
},
},
methods: {
toggleExpanded() {
this.expanded = !this.expanded;
},
},
});
</script>
<style lang="scss" module>
.notice {
display: flex;
color: var(--custom-font-black);
margin: 0;
padding: var(--spacing-xs);
background-color: var(--background-color);
border-width: 1px 1px 1px 7px;
border-style: solid;
border-color: var(--border-color);
border-radius: var(--border-radius-small);
a {
font-weight: var(--font-weight-bold);
}
}
.warning {
--border-color: var(--color-warning-tint-1);
--background-color: var(--color-warning-tint-2);
}
.danger {
--border-color: var(--color-danger-tint-1);
--background-color: var(--color-danger-tint-2);
}
.success {
--border-color: var(--color-success-tint-1);
--background-color: var(--color-success-tint-2);
}
.info {
--border-color: var(--color-info-tint-1);
--background-color: var(--color-info-tint-2);
}
.expanded {
+ span {
margin-top: var(--spacing-4xs);
display: block;
}
}
.truncated {
display: inline;
}
</style>

View file

@ -0,0 +1,95 @@
import {fireEvent, render} from '@testing-library/vue';
import N8nNotice from "../Notice.vue";
describe('components', () => {
describe('N8nNotice', () => {
it('should render correctly', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
},
slots: {
default: 'This is a notice.',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
describe('props', () => {
describe('content', () => {
it('should render correctly with content prop', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
content: 'This is a notice.',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should render html', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
content: '<strong>Hello world!</strong> This is a notice.',
},
});
expect(wrapper.container.querySelectorAll('strong')).toHaveLength(1);
expect(wrapper.html()).toMatchSnapshot();
});
it('should sanitize rendered html', () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
content: '<script>alert(1);</script> This is a notice.',
},
});
expect(wrapper.container.querySelector('script')).not.toBeTruthy();
expect(wrapper.html()).toMatchSnapshot();
});
});
});
describe('truncation', () => {
it('should truncate content longer than 150 characters', async () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
truncate: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
},
});
const button = await wrapper.findByRole('button');
const region = await wrapper.findByRole('region');
expect(button).toBeVisible();
expect(button).toHaveTextContent('Show more');
expect(region).toBeVisible();
expect(region.textContent!.endsWith('...')).toBeTruthy();
});
it('should expand truncated text when clicking show more', async () => {
const wrapper = render(N8nNotice, {
props: {
id: 'notice',
truncate: true,
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
},
});
const button = await wrapper.findByRole('button');
const region = await wrapper.findByRole('region');
await fireEvent.click(button);
expect(button).toHaveTextContent('Show less');
expect(region.textContent!.endsWith('...')).not.toBeTruthy();
});
});
});
});

View file

@ -0,0 +1,28 @@
// Vitest Snapshot v1
exports[`components > N8nNotice > props > content > should render correctly with content prop 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\"><span id=\\"notice-content\\" role=\\"region\\" class=\\"_truncated_4m4il_41\\">This is a notice.</span>
<!----></span></div>
</div>"
`;
exports[`components > N8nNotice > props > content > should render html 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\"><span id=\\"notice-content\\" role=\\"region\\" class=\\"_truncated_4m4il_41\\"><strong>Hello world!</strong> This is a notice.</span>
<!----></span></div>
</div>"
`;
exports[`components > N8nNotice > props > content > should sanitize rendered html 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\"><span id=\\"notice-content\\" role=\\"region\\" class=\\"_truncated_4m4il_41\\"> This is a notice.</span>
<!----></span></div>
</div>"
`;
exports[`components > N8nNotice > should render correctly 1`] = `
"<div id=\\"notice\\" role=\\"alert\\" class=\\"notice _notice_4m4il_1 _warning_4m4il_16\\">
<div class=\\"notice-content\\"><span class=\\"_size-small_9dlpz_24 _regular_9dlpz_5\\">This is a notice.</span></div>
</div>"
`;

View file

@ -0,0 +1,3 @@
import N8nNotice from './Notice.vue';
export default N8nNotice;

View file

@ -11,7 +11,7 @@
</template>
<script lang="ts">
import RadioButton from './RadioButton';
import RadioButton from './RadioButton.vue';
export default {
name: 'n8n-radio-buttons',

View file

@ -27,7 +27,7 @@
<script lang="ts">
import Vue from 'vue';
import N8nUserInfo from '../N8nUserInfo';
import { IUser } from '../../Interface';
import { IUser } from '../../types';
import ElSelect from 'element-ui/lib/select';
import ElOption from 'element-ui/lib/option';
import Locale from '../../mixins/locale';

View file

@ -20,7 +20,7 @@
</template>
<script lang="ts">
import { IUser } from '../../Interface';
import { IUser } from '../../types';
import Vue from 'vue';
import N8nActionToggle from '../N8nActionToggle';
import N8nBadge from '../N8nBadge';

View file

@ -1,10 +0,0 @@
import Vue from 'vue';
/** N8n component common definition */
export declare class N8nComponent extends Vue {
/** Install component into Vue */
static install(vue: typeof Vue): void;
}
/** Component size definition for button, input, etc */
export type N8nComponentSize = 'xlarge' | 'large' | 'medium' | 'small' | 'mini';

View file

@ -1 +0,0 @@
declare module './N8nButton';

View file

@ -52,6 +52,7 @@ import N8nLoading from './N8nLoading';
import N8nMarkdown from './N8nMarkdown';
import N8nMenu from './N8nMenu';
import N8nMenuItem from './N8nMenuItem';
import N8nNotice from './N8nNotice';
import N8nLink from './N8nLink';
import N8nOption from './N8nOption';
import N8nRadioButtons from './N8nRadioButtons';
@ -90,6 +91,7 @@ export {
N8nMarkdown,
N8nMenu,
N8nMenuItem,
N8nNotice,
N8nOption,
N8nRadioButtons,
N8nSelect,

View file

@ -5,6 +5,8 @@ export default {
'nds.userSelect.noMatchingUsers': 'No matching users',
'nds.usersList.deleteUser': 'Delete User',
'nds.usersList.reinviteUser': 'Resend invite',
'notice.showMore': 'Show more',
'notice.showLess': 'Show less',
'formInput.validator.fieldRequired': 'This field is required',
'formInput.validator.minCharactersRequired': 'Must be at least {minimum} characters',
'formInput.validator.maxCharactersRequired': 'Must be at most {maximum} characters',

View file

@ -1 +1,16 @@
declare module 'n8n-design-system';
import Vue from 'vue';
import * as locale from './locale';
declare module 'vue/types/vue' {
interface Vue {
$style: Record<string, string>;
}
}
declare module 'n8n-design-system' {
export * from './components';
export { N8nUserSelect, N8nUsersList } from './components'; // Workaround for circular imports, will be removed when migrated to typescript
export { locale };
}
export * from './types';

View file

@ -1,10 +1,15 @@
import * as components from './components';
import * as locale from './locale';
// @TODO Define proper plugin that loads all components
// tslint:disable-next-line:forin
for (const key in components) {
const component = components[key];
component.install = function (Vue) {
Vue.component(component.name, component);
component.install = (app) => {
app.component(component.name, component);
};
}
export { locale };
export * from './components';

View file

@ -0,0 +1 @@
export { default as localeMixin } from './locale';

View file

@ -2,7 +2,7 @@ import { t } from '../locale';
export default {
methods: {
t(...args) {
t(...args: string[]) {
return t.apply(this, args);
},
},

View file

@ -1,14 +1,38 @@
declare module 'element-ui/lib/button';
declare module 'element-ui/lib/col';
declare module 'element-ui/lib/input';
declare module 'element-ui/lib/tooltip';
declare module 'element-ui/lib/input-number';
declare module 'element-ui/lib/drawer';
declare module 'element-ui/lib/dialog';
declare module 'element-ui/lib/dropdown';
declare module 'element-ui/lib/dropdown-menu';
declare module 'element-ui/lib/dropdown-item';
declare module 'element-ui/lib/submenu';
declare module 'element-ui/lib/radio';
declare module 'element-ui/lib/radio-group';
declare module 'element-ui/lib/radio-button';
declare module 'element-ui/lib/checkbox';
declare module 'element-ui/lib/switch';
declare module 'element-ui/lib/select';
declare module 'element-ui/lib/option';
declare module 'element-ui/lib/option-group';
declare module 'element-ui/lib/pagination';
declare module 'element-ui/lib/button-group';
declare module 'element-ui/lib/table';
declare module 'element-ui/lib/table-column';
declare module 'element-ui/lib/date-picker';
declare module 'element-ui/lib/tabs';
declare module 'element-ui/lib/tab-pane';
declare module 'element-ui/lib/tag';
declare module 'element-ui/lib/row';
declare module 'element-ui/lib/col';
declare module 'element-ui/lib/badge';
declare module 'element-ui/lib/card';
declare module 'element-ui/lib/color-picker';
declare module 'element-ui/lib/container';
declare module 'element-ui/lib/loading';
declare module 'element-ui/lib/message-box';
declare module 'element-ui/lib/message';
declare module 'element-ui/lib/menu';
declare module 'element-ui/lib/menu-item';
declare module 'element-ui/lib/row';
declare module 'element-ui/lib/tag';
declare module 'element-ui/lib/skeleton';
declare module 'element-ui/lib/skeleton-item';
declare module 'element-ui/lib/notification';
declare module 'element-ui/lib/popover';
declare module 'element-ui/lib/transitions/collapse-transition';
declare module 'element-ui/lib/tooltip';
declare module 'element-ui/lib/input-number';

View file

@ -0,0 +1,46 @@
export type Rule = { name: string; config?: any}; // tslint:disable-line:no-any
export type RuleGroup = {
rules: Array<Rule | RuleGroup>;
defaultError?: {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IValidator = {
validate: (value: string | number | boolean | null | undefined, config: any) => false | {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IFormInput = {
name: string;
initialValue?: string | number | boolean | null;
properties: {
label?: string;
type?: 'text' | 'email' | 'password' | 'select' | 'multi-select' | 'info';
maxlength?: number;
required?: boolean;
showRequiredAsterisk?: boolean;
validators?: {
[name: string]: IValidator;
};
validationRules?: Array<Rule | RuleGroup>;
validateOnBlur?: boolean;
infoText?: string;
placeholder?: string;
options?: Array<{label: string; value: string}>;
autocomplete?: 'off' | 'new-password' | 'current-password' | 'given-name' | 'family-name' | 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
capitalize?: boolean;
focusInitially?: boolean;
};
shouldDisplay?: (values: {[key: string]: unknown}) => boolean;
};
export type IFormInputs = IFormInput[];
export type IFormBoxConfig = {
title: string;
buttonText?: string;
secondaryButtonText?: string;
inputs: IFormInputs;
redirectLink?: string;
redirectText?: string;
};

View file

@ -0,0 +1,2 @@
export * from './form';
export * from './user';

View file

@ -0,0 +1,8 @@
export interface IUser {
id: string;
firstName?: string;
lastName?: string;
email?: string;
isPending: boolean;
isOwner: boolean;
}

View file

@ -0,0 +1,2 @@
export * from './markdown';
export * from './uid';

View file

@ -0,0 +1,9 @@
/**
* Math.random should be unique because of its seeding algorithm.
* Convert it to base 36 (numbers + letters), and grab the first 9 characters after the decimal.
*
* @param baseId
*/
export function uid (baseId?: string): string {
return `${baseId ? `${baseId}-` : ''}${Math.random().toString(36).substring(2, 11)}`;
}

View file

@ -111,17 +111,21 @@
var(--color-warning-l)
);
--color-warning-tint-1-l: 88%;
--color-warning-tint-1-h: 35;
--color-warning-tint-1-s: 78%;
--color-warning-tint-1-l: 84%;
--color-warning-tint-1: hsl(
var(--color-warning-h),
var(--color-warning-s),
var(--color-warning-tint-1-l)
);
--color-warning-tint-2-h: 34%;
--color-warning-tint-2-s: 80%;
--color-warning-tint-2-l: 96%;
--color-warning-tint-2: hsl(
var(--color-warning-h),
var(--color-warning-s),
var(--color-warning-tint-2-h),
var(--color-warning-tint-2-s),
var(--color-warning-tint-2-l)
);

View file

@ -3,16 +3,26 @@
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"declaration": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env", "jest"],
"outDir": "dist",
"types": [
"webpack-env",
"jest",
"vitest/globals"
],
"typeRoots": [
"@testing-library",
"@types"
],
"paths": {
"@/*": ["src/*"]
},
@ -21,9 +31,7 @@
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
"src/**/*.vue"
],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,42 @@
import { createVuePlugin } from 'vite-plugin-vue2';
import { resolve } from 'path';
export default {
plugins: [
createVuePlugin(),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'vue2-boring-avatars': resolve(__dirname, '..', '..', 'node_modules', 'vue2-boring-avatars', 'dist', 'vue-2-boring-avatars.umd.js'),
// 'vue2-boring-avatars': 'vue2-boring-avatars/dist/vue-2-boring-avatars.umd.js',
},
},
build: {
lib: {
entry: resolve(__dirname, 'src', 'main.js'),
name: 'N8nDesignSystem',
fileName: (format) => `n8n-design-system.${format}.js`,
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
output: {
exports: 'named',
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
},
},
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: [
'./src/__tests__/setup.ts',
],
},
};

View file

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

View file

@ -23,6 +23,8 @@ import {
WorkflowExecuteMode,
} from 'n8n-workflow';
export * from 'n8n-design-system/src/types';
declare module 'jsplumb' {
interface PaintStyle {
stroke?: string;
@ -477,12 +479,6 @@ export interface IPushDataConsoleMessage {
messages: string[];
}
export interface IVersionNotificationSettings {
enabled: boolean;
endpoint: string;
infoUrl: string;
}
export type IPersonalizationSurveyAnswersV1 = {
codingSkill?: string | null;
companyIndustry?: string[] | null;
@ -505,6 +501,34 @@ export type IPersonalizationSurveyAnswersV2 = {
otherCompanyIndustryExtended?: string[] | null;
};
export type IRole = 'default' | 'owner' | 'member';
export interface IUserResponse {
id: string;
firstName?: string;
lastName?: string;
email?: string;
globalRole?: {
name: IRole;
id: string;
};
personalizationAnswers?: IPersonalizationSurveyAnswersV1 | IPersonalizationSurveyAnswersV2 | null;
isPending: boolean;
}
export interface IUser extends IUserResponse {
isDefaultUser: boolean;
isPendingUser: boolean;
isOwner: boolean;
fullName?: string;
}
export interface IVersionNotificationSettings {
enabled: boolean;
endpoint: string;
infoUrl: string;
}
export interface IN8nPrompts {
message: string;
title: string;
@ -888,21 +912,6 @@ export interface IBounds {
export type ILogInStatus = 'LoggedIn' | 'LoggedOut';
export type IRole = 'default' | 'owner' | 'member';
export interface IUserResponse {
id: string;
firstName?: string;
lastName?: string;
email?: string;
globalRole?: {
name: IRole;
id: string;
};
personalizationAnswers?: IPersonalizationSurveyAnswersV1 | IPersonalizationSurveyAnswersV2 | null;
isPending: boolean;
}
export interface IInviteResponse {
user: {
id: string;
@ -911,59 +920,6 @@ export interface IInviteResponse {
error?: string;
}
export interface IUser extends IUserResponse {
isDefaultUser: boolean;
isPendingUser: boolean;
isOwner: boolean;
fullName?: string;
}
export type Rule = { name: string; config?: any}; // tslint:disable-line:no-any
export type RuleGroup = {
rules: Array<Rule | RuleGroup>;
defaultError?: {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IValidator = {
validate: (value: string | number | boolean | null | undefined, config: any) => false | {messageKey: string, options?: any}; // tslint:disable-line:no-any
};
export type IFormInput = {
name: string;
initialValue?: string | number | boolean | null;
properties: {
label?: string;
type?: 'text' | 'email' | 'password' | 'select' | 'multi-select' | 'info';
maxlength?: number;
required?: boolean;
showRequiredAsterisk?: boolean;
validators?: {
[name: string]: IValidator;
};
validationRules?: Array<Rule | RuleGroup>;
validateOnBlur?: boolean;
infoText?: string;
placeholder?: string;
options?: Array<{label: string; value: string}>;
autocomplete?: 'off' | 'new-password' | 'current-password' | 'given-name' | 'family-name' | 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
capitalize?: boolean;
focusInitially?: boolean;
};
shouldDisplay?: (values: {[key: string]: unknown}) => boolean;
};
export type IFormInputs = IFormInput[];
export type IFormBoxConfig = {
title: string;
buttonText?: string;
secondaryButtonText?: string;
inputs: IFormInputs;
redirectLink?: string;
redirectText?: string;
};
export interface ITab {
value: string | number;
label?: string;

View file

@ -32,6 +32,7 @@
<script lang="ts">
import {
INodeUi,
IUpdateInformation,
} from '@/Interface';
@ -87,6 +88,9 @@ export default mixins(
return this.displayNodeParameter(option as INodeProperties);
});
},
node (): INodeUi {
return this.$store.getters.activeNode;
},
// Returns all the options which did not get added already
parameterOptions (): Array<INodePropertyOptions | INodeProperties> {
return (this.filteredOptions as Array<INodePropertyOptions | INodeProperties>).filter((option) => {
@ -127,7 +131,7 @@ export default mixins(
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.nodeValues, parameter, this.path);
return this.displayParameter(this.nodeValues, parameter, this.path, this.node);
},
optionSelected (optionName: string) {
const options = this.getOptionProperties(optionName);

View file

@ -395,6 +395,7 @@ export default mixins(showMessage, nodeHelpers).extend({
this.credentialData as INodeParameters,
parameter,
'',
null,
);
},
getCredentialProperties(name: string): INodeProperties[] {
@ -598,6 +599,7 @@ export default mixins(showMessage, nodeHelpers).extend({
this.credentialData as INodeParameters,
false,
false,
null,
);
const credentialDetails: ICredentialsDecrypted = {

View file

@ -251,7 +251,7 @@ export default mixins(
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.node.parameters, credentialTypeDescription, '');
return this.displayParameter(this.node.parameters, credentialTypeDescription, '', this.node);
},
getIssues (credentialTypeName: string): string[] {

Some files were not shown because too many files have changed in this diff Show more