Files
n8n-enterprise-unlocked/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts
Ricardo Espinoza a18081d749 feat: Add n8n Public API (#3064)
*  Inicial setup

*  Add authentication handler

*  Add GET /users route

*  Improvements

* 👕 Fix linting issues

*  Add GET /users/:identifier endpoint

*  Add POST /users endpoint

*  Add DELETE /users/:identifier endpoint

*  Return error using express native functions

* 👕 Fix linting issue

*  Possibility to add custom middleware

*  Refactor POST /users

*  Refactor DELETE /users

*  Improve cleaning function

*  Refactor GET /users and /users/:identifier

*  Add API spec to route

*  Add raw option to response helper

* 🐛 Fix issue adding custom middleware

*  Enable includeRole parameter in GET /users/:identifier

*  Fix linting issues after merge

*  Add missing config variable

*  General improvements

 asasas

*  Add POST /users tests

* Debug public API tests

* Fix both sets of tests

*  Improvements

*  Load api versions dynamically

*  Add endpoints to UM to create/delete an API Key

*  Add index to apiKey column

* 👕 Fix linting issue

*  Clean open api spec

*  Improvements

*  Skip tests

* 🐛 Fix bug with test

*  Fix issue with the open api spec

*  Fix merge issue

*  Move token enpoints from /users to /me

*  Apply feedback to openapi.yml

*  Improvements to api-key endpoints

* 🐛 Fix test to suport API dynamic loading

*  Expose swagger ui in GET /{version}/docs

*  Allow to disable public api via env variable

*  Change handlers structure

* 🚧 WIP create credential, delete credential complete

* 🐛 fix route for creating api key

*  return api key of authenticated user

*  Expose public api activation to the settings

* ⬆️ Update package-lock.json file

*  Add execution resource

*  Fix linting issues

* 🛠 conditional public api endpoints excluding

* ️ create credential complete

*  Added n8n-card component. Added spacing utility classes.

* ♻️ Made use of n8n-card in existing components.

*  Added api key setup view.

*  Added api keys get/create/delete actions.

*  Added public api permissions handling.

* ♻️ Temporarily disabling card tests.

* ♻️ Changed translations. Storing api key only in component.

*  Added utilities storybook entry

* ♻️ Changed default value for generic copy input.

* 🧹 clean up createCredential

*  Add workflow resource to openapi spec

* 🐛 Fix naming with env variable

*  Allow multifile openapi spec

*  Add POST /workflows/:workflowId/activate

* fix up view, fix issues

* remove delete api key modal

* remove unused prop

* clean up store api

* remove getter

* remove unused dispatch

* fix component size to match

* use existing components

* match figma closely

* fix bug when um is disabled in sidebar

* set copy input color

* remove unused import

*  Remove css path

*  Add POST /workflows/:workflowId/desactivate

*  Add POST /workflows

* Revert " Remove css path"

a3d0a71719834ef37c88c23c83dfe662e96185aa

* attempt to fix docker image issue

* revert dockerfile test

* disable public api

* disable api differently

* Revert "disable api differently"

b70e29433e45934975e41ffdc32e288988aba9b0

* Revert "disable public api"

886e5164fb4135c164f77561bdb4427e5cd44ac1

* remove unused box

*  PUT /workflows/:workflowId

*  Refactor workflow endpoints

*  Refactor executions endpoints

*  Fix typo

*  add credentials tests

*  adjust users tests

* update text

* add try it out link

*  Add delete, getAll and get to the workflow resource

* address spacing comments

* ️ apply correct structure

*  Add missing test to user resource and fix some issues

*  Add workflow tests

*  Add missing workflow tests and fix some issues

*  Executions tests

*  finish execution tests

*  Validate credentials data depending on type

* ️ implement review comments

* 👕 fix lint issues

*  Add apiKey to sanatizeUser

*  Fix issues with spec and tests

*  Add new structure

*  Validate credentials type and properties

*  Make all endpoints except /users independent on UM

*  Add instance base path to swagger UI

*  Remove testing endpoints

*  Fix issue with openapi tags

*  Add endpoint GET /credentialTypes/:id/schema

* 🐛 Fix issue adding json middleware to public api

*  Add API playground path to FE

*  Add telemetry and external hooks

* 🐛 Fix issue with user tests

*  Move /credentialTypes under /credentials

*  Add test to GET /credentials/schema/:id

* 🛠 refactor schema naming

*  Add DB migrations
asas

*  add tests for crd apiKey

*  Added API View telemetry events.

*  Remove rsync from the building process as it is missing on alpine base image

*  add missing BE telemetry events

* 🐛 Fix credential tests

*  address outstanding feedback

* 🔨 Remove move:openapi script

* ⬆️ update dependency

* ⬆️ update package-lock.json

* 👕 Fix linting issue

* 🐛 Fix package.json issue

* 🐛 fix migrations and tests

* 🐛 fix typos + naming

* 🚧 WIP fixing tests

*  Add json schema validation

*  Add missing fields to node schema

*  Add limit max upper limit

*  Rename id paths

* 🐛 Fix tests

* Add package-lock.jsonto custom dockerfile

* ⬆️ Update package-lock.json

* 🐛 Fix issue with build

* ✏️ add beta label to api view

* 🔥 Remove user endpoints

*  Add schema examples to GET /credentials/schema/:id

* 🔥 Remove user endpoints tests

* 🐛 Fix tests

* 🎨 adapt points from design review

* 🔥 remove unnecessary text-align

* ️ update UI

* 🐛 Fix issue with executions filter

*  Add tags filter to GET /workflows

*  Add missing error messages

*  add and update public api tests

*  add tests for owner activiating/deactivating non-owned wfs

* 🧪 add tests for filter for tags

* 🧪 add tests for more filter params

* 🐛 fix inclusion of tags

* 🛠 enhance readability

* ️ small refactorings

* 💄 improving readability/naming

*  Set API latest version dinamically

* Add comments to toJsonSchema function

*  Fix issue

*  Make execution data usable

*  Fix validation issue

*  Rename data field and change parameter and options

* 🐛 Fix issue parameter "detailsFieldFormat" not resolving correctly

* Skip executions tests

* skip workflow failing test

* Rename details property to data

*  Add includeData parameter

* 🐛 Fix issue with openapi spec

* 🐛 Fix linting issue

*  Fix execution schema

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2022-06-08 20:53:12 +02:00

250 lines
7.6 KiB
TypeScript

/* eslint-disable no-restricted-syntax */
/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { FindOneOptions } from 'typeorm';
import { UserSettings, Credentials } from 'n8n-core';
import { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
import { Db, ICredentialsDb } from '../../../..';
import { CredentialsEntity } from '../../../../databases/entities/CredentialsEntity';
import { SharedCredentials } from '../../../../databases/entities/SharedCredentials';
import { User } from '../../../../databases/entities/User';
import { externalHooks } from '../../../../Server';
import { IDependency, IJsonSchema } from '../../../types';
export async function getCredentials(
credentialId: number | string,
): Promise<ICredentialsDb | undefined> {
return Db.collections.Credentials.findOne(credentialId);
}
export async function getSharedCredentials(
userId: string,
credentialId: number | string,
relations?: string[],
): Promise<SharedCredentials | undefined> {
const options: FindOneOptions = {
where: {
user: { id: userId },
credentials: { id: credentialId },
},
};
if (relations) {
options.relations = relations;
}
return Db.collections.SharedCredentials.findOne(options);
}
export async function createCredential(
properties: Partial<CredentialsEntity>,
): Promise<CredentialsEntity> {
const newCredential = new CredentialsEntity();
Object.assign(newCredential, properties);
if (!newCredential.nodesAccess || newCredential.nodesAccess.length === 0) {
newCredential.nodesAccess = [
{
nodeType: `n8n-nodes-base.${properties.type?.toLowerCase() ?? 'unknown'}`,
date: new Date(),
},
];
} else {
// Add the added date for node access permissions
newCredential.nodesAccess.forEach((nodeAccess) => {
// eslint-disable-next-line no-param-reassign
nodeAccess.date = new Date();
});
}
return newCredential;
}
export async function saveCredential(
credential: CredentialsEntity,
user: User,
encryptedData: ICredentialsDb,
): Promise<CredentialsEntity> {
const role = await Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'credential',
});
await externalHooks.run('credentials.create', [encryptedData]);
return Db.transaction(async (transactionManager) => {
const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
savedCredential.data = credential.data;
const newSharedCredential = new SharedCredentials();
Object.assign(newSharedCredential, {
role,
user,
credentials: savedCredential,
});
await transactionManager.save<SharedCredentials>(newSharedCredential);
return savedCredential;
});
}
export async function removeCredential(credentials: CredentialsEntity): Promise<ICredentialsDb> {
await externalHooks.run('credentials.delete', [credentials.id]);
return Db.collections.Credentials.remove(credentials);
}
export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> {
const encryptionKey = await UserSettings.getEncryptionKey();
// Encrypt the data
const coreCredential = new Credentials(
{ id: null, name: credential.name },
credential.type,
credential.nodesAccess,
);
// @ts-ignore
coreCredential.setData(credential.data, encryptionKey);
return coreCredential.getDataToSave() as ICredentialsDb;
}
export function sanitizeCredentials(credentials: CredentialsEntity): Partial<CredentialsEntity>;
export function sanitizeCredentials(
credentials: CredentialsEntity[],
): Array<Partial<CredentialsEntity>>;
export function sanitizeCredentials(
credentials: CredentialsEntity | CredentialsEntity[],
): Partial<CredentialsEntity> | Array<Partial<CredentialsEntity>> {
const argIsArray = Array.isArray(credentials);
const credentialsList = argIsArray ? credentials : [credentials];
const sanitizedCredentials = credentialsList.map((credential) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data, nodesAccess, shared, ...rest } = credential;
return rest;
});
return argIsArray ? sanitizedCredentials : sanitizedCredentials[0];
}
/**
* toJsonSchema
* Take an array of crendentials parameter and map it
* to a JSON Schema (see https://json-schema.org/). With
* the JSON Schema defintion we can validate the credential's shape
* @param properties - Credentials properties
* @returns The credentials schema definition.
*/
export function toJsonSchema(properties: INodeProperties[]): IDataObject {
const jsonSchema: IJsonSchema = {
additionalProperties: false,
type: 'object',
properties: {},
allOf: [],
required: [],
};
const optionsValues: { [key: string]: string[] } = {};
const resolveProperties: string[] = [];
// get all posible values of properties type "options"
// so we can later resolve the displayOptions dependencies
properties
.filter((property) => property.type === 'options')
.forEach((property) => {
Object.assign(optionsValues, {
[property.name]: property.options?.map((option: INodePropertyOptions) => option.value),
});
});
let requiredFields: string[] = [];
const propertyRequiredDependencies: { [key: string]: IDependency } = {};
// add all credential's properties to the properties
// object in the JSON Schema definition. This allows us
// to later validate that only this properties are set in
// the credentials sent in the API call.
properties.forEach((property) => {
requiredFields.push(property.name);
if (property.type === 'options') {
// if the property is type options,
// include all possible values in the anum property.
Object.assign(jsonSchema.properties, {
[property.name]: {
type: 'string',
enum: property.options?.map((data: INodePropertyOptions) => data.value),
},
});
} else {
Object.assign(jsonSchema.properties, {
[property.name]: {
type: property.type,
},
});
}
// if the credential property has a dependency
// then add a JSON Schema condition that satisfy each property value
// e.x: If A has value X then required B, else required C
// see https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else
if (property.displayOptions?.show) {
const dependantName = Object.keys(property.displayOptions?.show)[0] || '';
const displayOptionsValues = property.displayOptions.show[dependantName];
let dependantValue: string | number | boolean = '';
if (displayOptionsValues && Array.isArray(displayOptionsValues) && displayOptionsValues[0]) {
// eslint-disable-next-line prefer-destructuring
dependantValue = displayOptionsValues[0];
}
if (propertyRequiredDependencies[dependantName] === undefined) {
propertyRequiredDependencies[dependantName] = {};
}
if (!resolveProperties.includes(dependantName)) {
propertyRequiredDependencies[dependantName] = {
if: {
properties: {
[dependantName]: {
enum: [dependantValue],
},
},
},
then: {
oneOf: [],
},
else: {
allOf: [],
},
};
}
propertyRequiredDependencies[dependantName].then?.oneOf.push({ required: [property.name] });
propertyRequiredDependencies[dependantName].else?.allOf.push({
not: { required: [property.name] },
});
resolveProperties.push(dependantName);
// remove global required
requiredFields = requiredFields.filter((field) => field !== property.name);
}
});
Object.assign(jsonSchema, { required: requiredFields });
jsonSchema.allOf = Object.values(propertyRequiredDependencies);
if (!jsonSchema.allOf.length) {
delete jsonSchema.allOf;
}
return jsonSchema as unknown as IDataObject;
}