Compare commits

..

3 Commits

Author SHA1 Message Date
Faruk AYDIN
6fcd223496 fix: Do not use context explicitly for docs-change file 2024-03-19 17:47:26 +01:00
Faruk AYDIN
baddf4653c fix: don't import github explicitly for github script 2024-03-19 17:44:11 +01:00
Faruk AYDIN
d1e01d673a docs: Test whether docs-change workflow works or not 2024-03-19 17:39:27 +01:00
254 changed files with 2589 additions and 4450 deletions

View File

@@ -1,116 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create Cloud Firestore document',
key: 'createCloudFirestoreDocument',
description: 'Creates a new document within a Cloud Firestore collection.',
arguments: [
{
label: 'Collection',
key: 'collectionId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirestoreCollections',
},
],
},
},
{
label: 'Convert Numerics',
key: 'convertNumerics',
type: 'dropdown',
required: false,
description:
"If any value represents a valid numerical value, whether it's an integer or a floating-point number, this field directs the database to store it as a numeric data type instead of a string.",
variables: true,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
{
label: 'Document ID',
key: 'documentId',
type: 'string',
required: false,
description:
'The document ID to use for this document. If not specified, an ID will be assigned.',
variables: true,
},
{
label: 'Document Data',
key: 'documentData',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Key',
key: 'key',
type: 'string',
required: false,
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: false,
variables: true,
},
],
},
],
async run($) {
const projectId = $.auth.data.projectId;
const { collectionId, documentId, documentData, convertNumerics } =
$.step.parameters;
const documentDataObject = documentData.reduce((result, entry) => {
const key = entry.key?.toLowerCase();
const value = entry.value;
const isNumber = !isNaN(parseFloat(value));
if (key && value) {
const formattedValue =
convertNumerics && isNumber
? { integerValue: parseFloat(value) }
: { stringValue: value };
return {
...result,
[key]: formattedValue,
};
}
return result;
}, {});
const body = {
fields: documentDataObject,
};
const { data } = await $.http.post(
`/v1/projects/${projectId}/databases/(default)/documents/${collectionId}?documentId=${documentId}`,
body,
{
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,100 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create Firebase Realtime Database Record',
key: 'createFirebaseRealtimeDatabaseRecord',
description: 'Creates a child object within your Firebase Realtime Database.',
arguments: [
{
label: 'Path',
key: 'path',
type: 'string',
required: true,
description:
"Indicate the path to the key of the object where the child objects to be queried are located, for example, 'foo/bar/here'.",
variables: true,
},
{
label: 'Convert Numerics',
key: 'convertNumerics',
type: 'dropdown',
required: false,
description:
"If any value represents a valid numerical value, whether it's an integer or a floating-point number, this field directs the database to store it as a numeric data type instead of a string.",
variables: true,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
{
label: 'New ID',
key: 'newId',
type: 'string',
required: false,
description:
'The key to use for this object, or leave it blank for Firebase to create one automatically.',
variables: true,
},
{
label: 'Data',
key: 'childData',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Key',
key: 'key',
type: 'string',
required: false,
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: false,
variables: true,
},
],
},
],
async run($) {
let path = $.step.parameters.path;
const { convertNumerics, newId, childData } = $.step.parameters;
if (newId) {
path = `${path}/${newId}.json`;
} else {
path = `${path}.json`;
}
const formattedChildObjectData = childData.reduce((result, entry) => {
const key = entry?.key;
const value = entry?.value;
const isNumber = !isNaN(parseFloat(value));
if (isNumber && convertNumerics) {
result[key] = parseFloat(value);
} else {
result[key] = value;
}
return result;
}, {});
const body = formattedChildObjectData;
const { data } = await $.http.post(path, body, {
additionalProperties: {
setFirestoreBaseUrl: false,
},
});
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,53 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Find Cloud Firestore document',
key: 'findCloudFirestoreDocument',
description: 'Finds a document within a collection.',
arguments: [
{
label: 'Collection',
key: 'collectionId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirestoreCollections',
},
],
},
},
{
label: 'Document ID',
key: 'documentId',
type: 'string',
required: true,
description: '',
variables: true,
},
],
async run($) {
const projectId = $.auth.data.projectId;
const { collectionId, documentId } = $.step.parameters;
const { data } = await $.http.get(
`/v1/projects/${projectId}/databases/(default)/documents/${collectionId}/${documentId}`,
{
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,32 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Find Firebase Realtime Database Record',
key: 'findFirebaseRealtimeDatabaseRecord',
description: 'Finds a child object in Firebase Realtime Database.',
arguments: [
{
label: 'Path',
key: 'path',
type: 'string',
required: true,
description:
"Indicate the path to the key of the object where the child objects to be queried are located, for example, 'foo/bar/here'.",
variables: true,
},
],
async run($) {
const { path } = $.step.parameters;
const { data } = await $.http.get(`${path}.json`, {
additionalProperties: {
setFirestoreBaseUrl: false,
},
});
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,11 +0,0 @@
import createCloudFirestoreDocument from './create-cloud-firestore-document/index.js';
import createFirebaseRealtimeDatabaseRecord from './create-firebase-realtime-database-record/index.js';
import findCloudFirestoreDocument from './find-cloud-firestore-document/index.js';
import findFirebaseRealtimeDatabaseRecord from './find-firebase-realtime-database-record/index.js';
export default [
createCloudFirestoreDocument,
createFirebaseRealtimeDatabaseRecord,
findCloudFirestoreDocument,
findFirebaseRealtimeDatabaseRecord,
];

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,23 +0,0 @@
import { URLSearchParams } from 'url';
import authScope from '../common/auth-scope.js';
export default async function generateAuthUrl($) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const searchParams = new URLSearchParams({
client_id: $.auth.data.clientId,
redirect_uri: redirectUri,
prompt: 'select_account',
scope: authScope.join(' '),
response_type: 'code',
access_type: 'offline',
});
const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`;
await $.auth.set({
url,
});
}

View File

@@ -1,71 +0,0 @@
import generateAuthUrl from './generate-auth-url.js';
import verifyCredentials from './verify-credentials.js';
import refreshToken from './refresh-token.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string',
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/firebase/connections/add',
placeholder: null,
description:
'When asked to input a redirect URL in Google Cloud, enter the URL above.',
clickToCopy: true,
},
{
key: 'clientId',
label: 'Client ID',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'clientSecret',
label: 'Client Secret',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'projectId',
label: 'Project ID',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'The project id of your Firebase project',
clickToCopy: false,
},
{
key: 'realtimeDatabaseId',
label: 'Realtime Database Domain',
type: 'string',
required: false,
readOnly: false,
value: null,
placeholder: null,
description:
'If you want to use Realtime Database, please provide the domain of your Realtime Database (https://{{domain}}.firebaseio.com)',
clickToCopy: false,
},
],
generateAuthUrl,
verifyCredentials,
isStillVerified,
refreshToken,
};

View File

@@ -1,8 +0,0 @@
import getCurrentUser from '../common/get-current-user.js';
const isStillVerified = async ($) => {
const currentUser = await getCurrentUser($);
return !!currentUser.resourceName;
};
export default isStillVerified;

View File

@@ -1,31 +0,0 @@
import { URLSearchParams } from 'node:url';
import authScope from '../common/auth-scope.js';
const refreshToken = async ($) => {
const params = new URLSearchParams({
client_id: $.auth.data.clientId,
client_secret: $.auth.data.clientSecret,
grant_type: 'refresh_token',
refresh_token: $.auth.data.refreshToken,
});
const { data } = await $.http.post(
'https://oauth2.googleapis.com/token',
params.toString(),
{
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
await $.auth.set({
accessToken: data.access_token,
expiresIn: data.expires_in,
scope: authScope.join(' '),
tokenType: data.token_type,
});
};
export default refreshToken;

View File

@@ -1,50 +0,0 @@
import getCurrentUser from '../common/get-current-user.js';
const verifyCredentials = async ($) => {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const { data } = await $.http.post(
`https://oauth2.googleapis.com/token`,
{
client_id: $.auth.data.clientId,
client_secret: $.auth.data.clientSecret,
code: $.auth.data.code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
},
{
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
await $.auth.set({
accessToken: data.access_token,
tokenType: data.token_type,
});
const currentUser = await getCurrentUser($);
const { displayName } = currentUser.names.find(
(name) => name.metadata.primary
);
const { value: email } = currentUser.emailAddresses.find(
(emailAddress) => emailAddress.metadata.primary
);
await $.auth.set({
clientId: $.auth.data.clientId,
clientSecret: $.auth.data.clientSecret,
scope: $.auth.data.scope,
idToken: data.id_token,
expiresIn: data.expires_in,
refreshToken: data.refresh_token,
resourceName: currentUser.resourceName,
screenName: `${displayName} - ${email}`,
});
};
export default verifyCredentials;

View File

@@ -1,9 +0,0 @@
const addAuthHeader = ($, requestConfig) => {
if ($.auth.data?.accessToken) {
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -1,9 +0,0 @@
const authScope = [
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/firebase.database',
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
];
export default authScope;

View File

@@ -1,13 +0,0 @@
const getCurrentUser = async ($) => {
const { data: currentUser } = await $.http.get(
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses',
{
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
return currentUser;
};
export default getCurrentUser;

View File

@@ -1,16 +0,0 @@
const setBaseUrl = ($, requestConfig) => {
const realtimeDatabaseId = $.auth.data.realtimeDatabaseId;
if (requestConfig.additionalProperties?.skipAddingAuthHeader)
return requestConfig;
if (requestConfig.additionalProperties?.setFirestoreBaseUrl) {
requestConfig.baseURL = 'https://firestore.googleapis.com';
} else {
requestConfig.baseURL = `https://${realtimeDatabaseId}.firebaseio.com`;
}
return requestConfig;
};
export default setBaseUrl;

View File

@@ -1,3 +0,0 @@
import listFirestoreCollections from './list-firestore-collections/index.js';
export default [listFirestoreCollections];

View File

@@ -1,32 +0,0 @@
export default {
name: 'List firestore collections',
key: 'listFirestoreCollections',
async run($) {
const firestoreCollections = {
data: [],
};
const projectId = $.auth.data.projectId;
const { data } = await $.http.post(
`/v1/projects/${projectId}/databases/(default)/documents:listCollectionIds`,
null,
{
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
if (data.collectionIds?.length) {
for (const collectionId of data.collectionIds) {
firestoreCollections.data.push({
value: collectionId,
name: collectionId,
});
}
}
return firestoreCollections;
},
};

View File

@@ -1,23 +0,0 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import auth from './auth/index.js';
import setBaseUrl from './common/set-base-url.js';
import triggers from './triggers/index.js';
import dynamicData from './dynamic-data/index.js';
import actions from './actions/index.js';
export default defineApp({
name: 'Firebase',
key: 'firebase',
baseUrl: 'https://firebase.google.com',
apiBaseUrl: '',
iconUrl: '{BASE_URL}/apps/firebase/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/firebase/connection',
primaryColor: 'FFA000',
supportsConnections: true,
beforeRequest: [setBaseUrl, addAuthHeader],
auth,
triggers,
dynamicData,
actions,
});

View File

@@ -1,7 +0,0 @@
import newChildObjectInFirebaseRealtimeDatabase from './new-child-object-in-a-firebase-realtime-database/index.js';
import newDocumentsWithinFirestoreCollection from './new-documents-within-firestore-collection/index.js';
export default [
newChildObjectInFirebaseRealtimeDatabase,
newDocumentsWithinFirestoreCollection,
];

View File

@@ -1,75 +0,0 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New child object in a firebase realtime database',
key: 'newChildObjectInFirebaseRealtimeDatabase',
pollInterval: 15,
description:
'Triggers when a new child object is generated within a specific path.',
arguments: [
{
label: 'Path',
key: 'path',
type: 'string',
required: true,
description:
"Indicate the path to the key of the object where the child objects to be queried are located, for example, 'foo/bar/here.json'.",
variables: true,
},
{
label: 'Order',
key: 'order',
type: 'string',
required: false,
description:
'The key or path of the child that should be utilized for comparing objects/records. If unspecified, the order of $key is used.',
variables: true,
},
{
label: 'Location of newest objects',
key: 'locationOfNewestObjects',
type: 'dropdown',
required: false,
description:
'Specifies whether the new 100 records are positioned at the "top" or the "bottom" of the ordering. If left unspecified, the assumption is that the bottom/last result represents the "newest objects" (limitToLast).',
variables: true,
options: [
{ label: 'Top of results', value: 'limitToFirst' },
{ label: 'Bottom of results', value: 'limitToLast' },
],
},
],
async run($) {
const { path, order, locationOfNewestObjects } = $.step.parameters;
const params = {};
if (order) {
params.orderBy = `"${order}"`;
}
if (locationOfNewestObjects) {
params[`${locationOfNewestObjects}`] = 100;
}
const { data } = await $.http.get(path, {
params,
additionalProperties: {
setFirestoreBaseUrl: false,
},
});
if (!data) {
return;
}
$.pushTriggerItem({
raw: data,
meta: {
internalId: Crypto.randomUUID(),
},
});
},
});

View File

@@ -1,66 +0,0 @@
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New documents within a firestore collection',
key: 'newDocumentsWithinFirestoreCollection',
pollInterval: 15,
description:
'Triggers when a new document is added within a Cloud Firestore collection.',
arguments: [
{
label: 'Collection',
key: 'collectionId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFirestoreCollections',
},
],
},
},
],
async run($) {
const projectId = $.auth.data.projectId;
const collectionId = $.step.parameters.collectionId;
const params = {
pageSize: 100,
pageToken: undefined,
};
do {
const { data } = await $.http.get(
`/v1/projects/${projectId}/databases/(default)/documents/${collectionId}`,
{
params,
additionalProperties: {
setFirestoreBaseUrl: true,
},
}
);
params.pageToken = data.nextPageToken;
if (!data?.documents?.length) {
return;
}
for (const document of data.documents) {
const nameParts = document.name.split('/');
const id = nameParts[nameParts.length - 1];
$.pushTriggerItem({
raw: document,
meta: {
internalId: id,
},
});
}
} while (params.pageToken);
},
});

View File

@@ -90,7 +90,7 @@ export default defineAction({
async run($) { async run($) {
const method = $.step.parameters.method; const method = $.step.parameters.method;
const data = $.step.parameters.data || null; const data = $.step.parameters.data;
const url = $.step.parameters.url; const url = $.step.parameters.url;
const headers = $.step.parameters.headers; const headers = $.step.parameters.headers;
@@ -108,17 +108,14 @@ export default defineAction({
return result; return result;
}, {}); }, {});
let expectedResponseContentType = headersObject.accept; let contentType = headersObject['content-type'];
// in case HEAD request is not supported by the URL // in case HEAD request is not supported by the URL
try { try {
const metadataResponse = await $.http.head(url, { const metadataResponse = await $.http.head(url, {
headers: headersObject, headers: headersObject,
}); });
contentType = metadataResponse.headers['content-type'];
if (!expectedResponseContentType) {
expectedResponseContentType = metadataResponse.headers['content-type'];
}
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']); throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
@@ -131,7 +128,7 @@ export default defineAction({
headers: headersObject, headers: headersObject,
}; };
if (!isPossiblyTextBased(expectedResponseContentType)) { if (!isPossiblyTextBased(contentType)) {
requestData.responseType = 'arraybuffer'; requestData.responseType = 'arraybuffer';
} }
@@ -141,7 +138,7 @@ export default defineAction({
let responseData = response.data; let responseData = response.data;
if (!isPossiblyTextBased(expectedResponseContentType)) { if (!isPossiblyTextBased(contentType)) {
responseData = Buffer.from(responseData).toString('base64'); responseData = Buffer.from(responseData).toString('base64');
} }

View File

@@ -64,18 +64,33 @@ export default defineAction({
value: '1', value: '1',
description: description:
'The ID of the stage this deal will be added to. If omitted, the deal will be placed in the first stage of the default pipeline.', 'The ID of the stage this deal will be added to. If omitted, the deal will be placed in the first stage of the default pipeline.',
variables: true, options: [
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{ {
name: 'key', label: 'Qualified (Pipeline)',
value: 'listStages', value: 1,
},
{
label: 'Contact Made (Pipeline)',
value: 2,
},
{
label: 'Prospect Qualified (Pipeline)',
value: 3,
},
{
label: 'Needs Defined (Pipeline)',
value: 4,
},
{
label: 'Proposal Made (Pipeline)',
value: 5,
},
{
label: 'Negotiations Started (Pipeline)',
value: 6,
}, },
], ],
}, },
},
{ {
label: 'Owner', label: 'Owner',
key: 'userId', key: 'userId',

View File

@@ -1,25 +1,23 @@
import listActivityTypes from './list-activity-types/index.js'; import listActivityTypes from './list-activity-types/index.js';
import listCurrencies from './list-currencies/index.js'; import listCurrencies from './list-currencies/index.js';
import listDeals from './list-deals/index.js'; import listDeals from './list-deals/index.js';
import listLeadLabels from './list-lead-labels/index.js';
import listLeads from './list-leads/index.js'; import listLeads from './list-leads/index.js';
import listOrganizationLabelField from './list-organization-label-field/index.js'; import listLeadLabels from './list-lead-labels/index.js';
import listOrganizations from './list-organizations/index.js'; import listOrganizations from './list-organizations/index.js';
import listOrganizationLabelField from './list-organization-label-field/index.js';
import listPersonLabelField from './list-person-label-field/index.js'; import listPersonLabelField from './list-person-label-field/index.js';
import listPersons from './list-persons/index.js'; import listPersons from './list-persons/index.js';
import listStages from './list-stages/index.js';
import listUsers from './list-users/index.js'; import listUsers from './list-users/index.js';
export default [ export default [
listActivityTypes, listActivityTypes,
listCurrencies, listCurrencies,
listDeals, listDeals,
listLeadLabels,
listLeads, listLeads,
listOrganizationLabelField, listLeadLabels,
listOrganizations, listOrganizations,
listOrganizationLabelField,
listPersonLabelField, listPersonLabelField,
listPersons, listPersons,
listStages,
listUsers, listUsers,
]; ];

View File

@@ -1,23 +0,0 @@
export default {
name: 'List stages',
key: 'listStages',
async run($) {
const stages = {
data: [],
};
const { data } = await $.http.get('/api/v1/stages');
if (data.data?.length) {
for (const stage of data.data) {
stages.data.push({
value: stage.id,
name: stage.name,
});
}
}
return stages;
},
};

View File

@@ -14,55 +14,24 @@ export default defineAction({
value: '200', value: '200',
}, },
{ {
label: 'Headers', label: 'JSON body',
key: 'headers', key: 'stringifiedJsonBody',
type: 'dynamic',
required: false,
description: 'Add or remove headers as needed',
fields: [
{
label: 'Key',
key: 'key',
type: 'string', type: 'string',
required: true, required: true,
description: 'Header key', description: 'The content of the JSON body. It must be a valid JSON.',
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: true,
description: 'Header value',
variables: true,
},
],
},
{
label: 'Body',
key: 'body',
type: 'string',
required: true,
description: 'The content of the response body.',
variables: true, variables: true,
}, },
], ],
async run($) { async run($) {
const statusCode = parseInt($.step.parameters.statusCode, 10); const parsedStatusCode = parseInt($.step.parameters.statusCode, 10);
const body = $.step.parameters.body; const stringifiedJsonBody = $.step.parameters.stringifiedJsonBody;
const headers = $.step.parameters.headers.reduce((result, entry) => { const parsedJsonBody = JSON.parse(stringifiedJsonBody);
return {
...result,
[entry.key]: entry.value,
};
}, {});
$.setActionItem({ $.setActionItem({
raw: { raw: {
headers, body: parsedJsonBody,
body, statusCode: parsedStatusCode,
statusCode,
}, },
}); });
}, },

View File

@@ -1,13 +0,0 @@
import User from '../../../../models/user.js';
import { renderObject, renderError } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const { email, password } = request.body;
const token = await User.authenticate(email, password);
if (token) {
return renderObject(response, { token });
}
renderError(response, [{ general: ['Incorrect email or password.'] }]);
};

View File

@@ -1,39 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import { createUser } from '../../../../../test/factories/user';
describe('POST /api/v1/access-tokens', () => {
beforeEach(async () => {
await createUser({
email: 'user@automatisch.io',
password: 'password',
});
});
it('should return the token data with correct credentials', async () => {
const response = await request(app)
.post('/api/v1/access-tokens')
.send({
email: 'user@automatisch.io',
password: 'password',
})
.expect(200);
expect(response.body.data.token.length).toBeGreaterThan(0);
});
it('should return error with incorrect credentials', async () => {
const response = await request(app)
.post('/api/v1/access-tokens')
.send({
email: 'incorrect@email.com',
password: 'incorrectpassword',
})
.expect(422);
expect(response.body.errors.general).toEqual([
'Incorrect email or password.',
]);
});
});

View File

@@ -4,7 +4,6 @@ import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => { export default async (request, response) => {
const appAuthClient = await AppAuthClient.query() const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId) .findById(request.params.appAuthClientId)
.where({ app_key: request.params.appKey })
.throwIfNotFound(); .throwIfNotFound();
renderObject(response, appAuthClient); renderObject(response, appAuthClient);

View File

@@ -0,0 +1,52 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients/:appAuthClientId', () => {
let currentUser, currentUserRole, currentAppAuthClient, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
currentAppAuthClient = await createAppAuthClient();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const response = await request(app)
.get(`/api/v1/admin/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client UUID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/admin/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});
});

View File

@@ -1,55 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createRole } from '../../../../../../test/factories/role.js';
import getAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
let currentUser, adminRole, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
currentAppAuthClient = await createAppAuthClient({
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client', async () => {
const response = await request(app)
.get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client ID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(
`/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`
)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,10 +0,0 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query()
.where({ app_key: request.params.appKey })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);
};

View File

@@ -1,44 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createRole } from '../../../../../../test/factories/role.js';
import getAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-clients.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
let currentUser, adminRole, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/admin/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -4,7 +4,7 @@ import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => { export default async (request, response) => {
const appAuthClient = await AppAuthClient.query() const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId) .findById(request.params.appAuthClientId)
.where({ app_key: request.params.appKey, active: true }) .where({ active: true })
.throwIfNotFound(); .throwIfNotFound();
renderObject(response, appAuthClient); renderObject(response, appAuthClient);

View File

@@ -4,27 +4,25 @@ import Crypto from 'crypto';
import app from '../../../../app.js'; import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js'; import { createUser } from '../../../../../test/factories/user.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js'; import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js'; import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js'; import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => { describe('GET /api/v1/app-auth-clients/:id', () => {
let currentUser, currentAppAuthClient, token; let currentUser, currentAppAuthClient, token;
beforeEach(async () => { beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser(); currentUser = await createUser();
currentAppAuthClient = await createAppAuthClient({ currentAppAuthClient = await createAppAuthClient();
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id); token = createAuthTokenByUserId(currentUser.id);
}); });
it('should return specified app auth client', async () => { it('should return specified app auth client info', async () => {
const response = await request(app) const response = await request(app)
.get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`) .get(`/api/v1/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
@@ -36,14 +34,14 @@ describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID(); const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app) await request(app)
.get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`) .get(`/api/v1/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token) .set('Authorization', token)
.expect(404); .expect(404);
}); });
it('should return bad request response for invalid UUID', async () => { it('should return bad request response for invalid UUID', async () => {
await request(app) await request(app)
.get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID') .get('/api/v1/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token) .set('Authorization', token)
.expect(400); .expect(400);
}); });

View File

@@ -3,9 +3,6 @@ import AppConfig from '../../../../models/app-config.js';
export default async (request, response) => { export default async (request, response) => {
const appConfig = await AppConfig.query() const appConfig = await AppConfig.query()
.withGraphFetched({
appAuthClients: true,
})
.findOne({ .findOne({
key: request.params.appKey, key: request.params.appKey,
}) })

View File

@@ -3,11 +3,11 @@ import request from 'supertest';
import app from '../../../../app.js'; import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js'; import { createUser } from '../../../../../test/factories/user.js';
import getAppConfigMock from '../../../../../test/mocks/rest/api/v1/apps/get-config.js'; import getAppConfigMock from '../../../../../test/mocks/rest/api/v1/app-configs/get-app-config.js';
import { createAppConfig } from '../../../../../test/factories/app-config.js'; import { createAppConfig } from '../../../../../test/factories/app-config.js';
import * as license from '../../../../helpers/license.ee.js'; import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/apps/:appKey/config', () => { describe('GET /api/v1/app-configs/:appKey', () => {
let currentUser, appConfig, token; let currentUser, appConfig, token;
beforeEach(async () => { beforeEach(async () => {
@@ -27,7 +27,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
it('should return specified app config info', async () => { it('should return specified app config info', async () => {
const response = await request(app) const response = await request(app)
.get(`/api/v1/apps/${appConfig.key}/config`) .get(`/api/v1/app-configs/${appConfig.key}`)
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
@@ -37,7 +37,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
it('should return not found response for not existing app key', async () => { it('should return not found response for not existing app key', async () => {
await request(app) await request(app)
.get('/api/v1/apps/not-existing-app-key/config') .get('/api/v1/app-configs/not-existing-app-key')
.set('Authorization', token) .set('Authorization', token)
.expect(404); .expect(404);
}); });

View File

@@ -1,10 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClients = await AppAuthClient.query()
.where({ app_key: request.params.appKey, active: true })
.orderBy('created_at', 'desc');
renderObject(response, appAuthClients);
};

View File

@@ -1,42 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/apps/:appKey/auth-clients', () => {
let currentUser, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const appAuthClientOne = await createAppAuthClient({
appKey: 'deepl',
});
const appAuthClientTwo = await createAppAuthClient({
appKey: 'deepl',
});
const response = await request(app)
.get('/api/v1/apps/deepl/auth-clients')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAuthClientsMock([
appAuthClientTwo,
appAuthClientOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,24 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
import App from '../../../../models/app.js';
export default async (request, response) => {
const app = await App.findOneByKey(request.params.appKey);
const connections = await request.currentUser.authorizedConnections
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': app.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
renderObject(response, connections);
};

View File

@@ -1,101 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
import getConnectionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-connections.js';
describe('GET /api/v1/apps/:appKey/connections', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the connections data of specified app for current user', async () => {
const currentUserConnectionOne = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
const currentUserConnectionTwo = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
currentUserConnectionTwo,
currentUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return the connections data of specified app for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnectionOne = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
const anotherUserConnectionTwo = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get('/api/v1/apps/deepl/connections')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getConnectionsMock([
anotherUserConnectionTwo,
anotherUserConnectionOne,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid connection UUID', async () => {
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.get('/api/v1/connections/invalid-connection-id/connections')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -1,14 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
let connection = await request.currentUser.authorizedConnections
.clone()
.findOne({
id: request.params.connectionId,
})
.throwIfNotFound();
connection = await connection.testAndUpdateConnection();
renderObject(response, connection);
};

View File

@@ -1,123 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import { createPermission } from '../../../../../test/factories/permission.js';
describe('POST /api/v1/connections/:connectionId/test', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should update the connection as not verified for current user', async () => {
const currentUserConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/connections/${currentUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should update the connection as not verified for another user', async () => {
const anotherUser = await createUser();
const anotherUserConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
verified: true,
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/connections/${anotherUserConnection.id}/test`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.verified).toEqual(false);
});
it('should return not found response for not existing connection UUID', async () => {
const notExistingConnectionUUID = Crypto.randomUUID();
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post(`/api/v1/connections/${notExistingConnectionUUID}/test`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.post('/api/v1/connections/invalidConnectionUUID/test')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -42,12 +42,9 @@ describe('GET /api/v1/executions', () => {
const currentUserExecutionTwo = await createExecution({ const currentUserExecutionTwo = await createExecution({
flowId: currentUserFlow.id, flowId: currentUserFlow.id,
deletedAt: new Date().toISOString(),
}); });
await currentUserExecutionTwo
.$query()
.patchAndFetch({ deletedAt: new Date().toISOString() });
await createPermission({ await createPermission({
action: 'read', action: 'read',
subject: 'Execution', subject: 'Execution',
@@ -90,12 +87,9 @@ describe('GET /api/v1/executions', () => {
const anotherUserExecutionTwo = await createExecution({ const anotherUserExecutionTwo = await createExecution({
flowId: anotherUserFlow.id, flowId: anotherUserFlow.id,
deletedAt: new Date().toISOString(),
}); });
await anotherUserExecutionTwo
.$query()
.patchAndFetch({ deletedAt: new Date().toISOString() });
await createPermission({ await createPermission({
action: 'read', action: 'read',
subject: 'Execution', subject: 'Execution',

View File

@@ -1,18 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.where('steps.id', request.params.stepId)
.whereNotNull('steps.app_key')
.whereNotNull('steps.connection_id')
.first()
.throwIfNotFound();
const dynamicData = await step.createDynamicData(
request.body.dynamicDataKey,
request.body.parameters
);
renderObject(response, dynamicData);
};

View File

@@ -1,244 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createConnection } from '../../../../../test/factories/connection';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import listRepos from '../../../../apps/github/dynamic-data/list-repos/index.js';
import HttpError from '../../../../errors/http.js';
describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
describe('should return dynamically created data', () => {
let repositories;
beforeEach(async () => {
repositories = [
{
value: 'automatisch/automatisch',
name: 'automatisch/automatisch',
},
{
value: 'automatisch/sample',
name: 'automatisch/sample',
},
];
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
return {
data: repositories,
};
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
it('of the another users step', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const connection = await createConnection({ userId: anotherUser.id });
const actionStep = await createStep({
flowId: anotherUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.data).toEqual(repositories);
});
});
describe('should return error for dynamically created data', () => {
let errors;
beforeEach(async () => {
errors = {
message: 'Not Found',
documentation_url:
'https://docs.github.com/rest/users/users#get-a-user',
};
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
throw new HttpError({ message: errors });
});
});
it('of the current users step', async () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
const connection = await createConnection({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserFlow.id,
connectionId: connection.id,
type: 'action',
appKey: 'github',
key: 'createIssue',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-data`)
.set('Authorization', token)
.send({
dynamicDataKey: 'listRepos',
parameters: {},
})
.expect(200);
expect(response.body.errors).toEqual(errors);
});
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const step = await createStep({ appKey: null });
await request(app)
.get(`/api/v1/steps/${step.id}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.post('/api/v1/steps/invalidStepUUID/dynamic-fields')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,17 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.where('steps.id', request.params.stepId)
.whereNotNull('steps.app_key')
.first()
.throwIfNotFound();
const dynamicFields = await step.createDynamicFields(
request.body.dynamicFieldsKey,
request.body.parameters
);
renderObject(response, dynamicFields);
};

View File

@@ -1,169 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import createDynamicFieldsMock from '../../../../../test/mocks/rest/api/v1/steps/create-dynamic-fields';
describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return dynamically created fields of the current users step', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const actionStep = await createStep({
flowId: currentUserflow.id,
type: 'action',
appKey: 'slack',
key: 'sendMessageToChannel',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-fields`)
.set('Authorization', token)
.send({
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
parameters: {
sendAsBot: true,
},
})
.expect(200);
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return dynamically created fields of the another users step', async () => {
const anotherUser = await createUser();
const anotherUserflow = await createFlow({ userId: anotherUser.id });
const actionStep = await createStep({
flowId: anotherUserflow.id,
type: 'action',
appKey: 'slack',
key: 'sendMessageToChannel',
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.post(`/api/v1/steps/${actionStep.id}/dynamic-fields`)
.set('Authorization', token)
.send({
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
parameters: {
sendAsBot: true,
},
})
.expect(200);
const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const step = await createStep({ appKey: null });
await request(app)
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.post('/api/v1/steps/invalidStepUUID/dynamic-fields')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,27 +0,0 @@
import { ref } from 'objection';
import ExecutionStep from '../../../../models/execution-step.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.findOne({ 'steps.id': request.params.stepId })
.throwIfNotFound();
const previousSteps = await request.currentUser.authorizedSteps
.clone()
.withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position)
.andWhere(
'executionSteps.created_at',
'=',
ExecutionStep.query()
.max('created_at')
.where('step_id', '=', ref('steps.id'))
.andWhere('status', 'success')
)
.orderBy('steps.position', 'asc');
renderObject(response, previousSteps);
};

View File

@@ -1,173 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import Crypto from 'crypto';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createExecutionStep } from '../../../../../test/factories/execution-step.js';
import { createPermission } from '../../../../../test/factories/permission';
import getPreviousStepsMock from '../../../../../test/mocks/rest/api/v1/steps/get-previous-steps';
describe('GET /api/v1/steps/:stepId/previous-steps', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the previous steps of the specified step of the current user', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const triggerStep = await createStep({
flowId: currentUserflow.id,
type: 'trigger',
});
const actionStepOne = await createStep({
flowId: currentUserflow.id,
type: 'action',
});
const actionStepTwo = await createStep({
flowId: currentUserflow.id,
type: 'action',
});
const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});
const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the previous steps of the specified step of another user', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const triggerStep = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
});
const actionStepOne = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const actionStepTwo = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});
const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingFlowUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/steps/${notExistingFlowUUID}/previous-steps`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.get('/api/v1/steps/invalidFlowUUID/previous-steps')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -1,7 +0,0 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const apps = await request.currentUser.getApps(request.query.name);
renderObject(response, apps, { serializer: 'App' });
};

View File

@@ -1,210 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../../../test/factories/role';
import { createUser } from '../../../../../test/factories/user';
import { createPermission } from '../../../../../test/factories/permission.js';
import { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createConnection } from '../../../../../test/factories/connection.js';
import getAppsMock from '../../../../../test/mocks/rest/api/v1/users/get-apps.js';
describe('GET /api/v1/users/:userId/apps', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return all apps of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const flowOne = await createFlow({ userId: currentUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: currentUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return all apps of the another user', async () => {
const anotherUser = await createUser();
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
});
const flowOne = await createFlow({ userId: anotherUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: anotherUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: anotherUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: anotherUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock();
expect(response.body).toEqual(expectedPayload);
});
it('should return specified app of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'read',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const flowOne = await createFlow({ userId: currentUser.id });
await createStep({
flowId: flowOne.id,
appKey: 'webhook',
});
const flowOneActionStepConnection = await createConnection({
userId: currentUser.id,
key: 'deepl',
draft: false,
});
await createStep({
connectionId: flowOneActionStepConnection.id,
flowId: flowOne.id,
appKey: 'deepl',
});
const flowTwo = await createFlow({ userId: currentUser.id });
const flowTwoTriggerStepConnection = await createConnection({
userId: currentUser.id,
key: 'github',
draft: false,
});
await createStep({
connectionId: flowTwoTriggerStepConnection.id,
flowId: flowTwo.id,
appKey: 'github',
});
await createStep({
flowId: flowTwo.id,
appKey: 'slack',
});
const response = await request(app)
.get(`/api/v1/users/${currentUser.id}/apps?name=deepl`)
.set('Authorization', token)
.expect(200);
expect(response.body.data.length).toEqual(1);
expect(response.body.data[0].key).toEqual('deepl');
});
});

View File

@@ -26,4 +26,6 @@ export default async (request, response) => {
} }
await handlerSync(flowId, request, response); await handlerSync(flowId, request, response);
response.sendStatus(204);
}; };

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_key');
});
}

View File

@@ -1,17 +0,0 @@
export async function up(knex) {
const appAuthClients = await knex('app_auth_clients').select('*');
for (const appAuthClient of appAuthClients) {
const appConfig = await knex('app_configs')
.where('id', appAuthClient.app_config_id)
.first();
await knex('app_auth_clients')
.where('id', appAuthClient.id)
.update({ app_key: appConfig.key });
}
}
export async function down() {
// void
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.dropColumn('app_config_id');
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.uuid('app_config_id').references('id').inTable('app_configs');
});
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').notNullable().alter();
});
}
export async function down(knex) {
await knex.schema.table('app_auth_clients', (table) => {
table.string('app_key').nullable().alter();
});
}

View File

@@ -0,0 +1,24 @@
import AppAuthClient from '../../models/app-auth-client.js';
const getAppAuthClient = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appAuthClient = AppAuthClient.query()
.findById(params.id)
.throwIfNotFound();
if (!canSeeAllClients) {
appAuthClient.where({ active: true });
}
return await appAuthClient;
};
export default getAppAuthClient;

View File

@@ -0,0 +1,33 @@
import AppConfig from '../../models/app-config.js';
const getAppAuthClients = async (_parent, params, context) => {
let canSeeAllClients = false;
try {
context.currentUser.can('read', 'App');
canSeeAllClients = true;
} catch {
// void
}
const appConfig = await AppConfig.query()
.findOne({
key: params.appKey,
})
.throwIfNotFound();
const appAuthClients = appConfig
.$relatedQuery('appAuthClients')
.where({ active: params.active })
.skipUndefined();
if (!canSeeAllClients) {
appAuthClients.where({
active: true,
});
}
return await appAuthClients;
};
export default getAppAuthClients;

View File

@@ -0,0 +1,41 @@
import App from '../../models/app.js';
import Connection from '../../models/connection.js';
const getApp = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const connections = await connectionBaseQuery
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true,
})
.fullOuterJoinRelated('steps')
.where({
'connections.key': params.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
return {
...app,
connections,
};
}
return app;
};
export default getApp;

View File

@@ -0,0 +1,101 @@
import { DateTime } from 'luxon';
import Billing from '../../helpers/billing/index.ee.js';
import ExecutionStep from '../../models/execution-step.js';
const getBillingAndUsage = async (_parent, _params, context) => {
const persistedSubscription = await context.currentUser.$relatedQuery(
'currentSubscription'
);
const subscription = persistedSubscription
? paidSubscription(persistedSubscription)
: freeTrialSubscription();
return {
subscription,
usage: {
task: executionStepCount(context),
},
};
};
const paidSubscription = (subscription) => {
const currentPlan = Billing.paddlePlans.find(
(plan) => plan.productId === subscription.paddlePlanId
);
return {
status: subscription.status,
monthlyQuota: {
title: currentPlan.limit,
action: {
type: 'link',
text: 'Cancel plan',
src: subscription.cancelUrl,
},
},
nextBillAmount: {
title: subscription.nextBillAmount
? '€' + subscription.nextBillAmount
: '---',
action: {
type: 'link',
text: 'Update payment method',
src: subscription.updateUrl,
},
},
nextBillDate: {
title: subscription.nextBillDate ? subscription.nextBillDate : '---',
action: {
type: 'text',
text: '(monthly payment)',
},
},
};
};
const freeTrialSubscription = () => {
return {
status: null,
monthlyQuota: {
title: 'Free Trial',
action: {
type: 'link',
text: 'Upgrade plan',
src: '/settings/billing/upgrade',
},
},
nextBillAmount: {
title: '---',
action: null,
},
nextBillDate: {
title: '---',
action: null,
},
};
};
const executionIds = async (context) => {
return (
await context.currentUser
.$relatedQuery('executions')
.select('executions.id')
).map((execution) => execution.id);
};
const executionStepCount = async (context) => {
const executionStepCount = await ExecutionStep.query()
.whereIn('execution_id', await executionIds(context))
.andWhere(
'created_at',
'>=',
DateTime.now().minus({ days: 30 }).toISODate()
)
.count()
.first();
return executionStepCount.count;
};
export default getBillingAndUsage;

View File

@@ -0,0 +1,32 @@
import appConfig from '../../config/app.js';
import { hasValidLicense } from '../../helpers/license.ee.js';
import Config from '../../models/config.js';
const getConfig = async (_parent, params) => {
if (!(await hasValidLicense())) return {};
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
const configQuery = Config.query();
if (Array.isArray(params.keys)) {
configQuery.whereIn('key', params.keys);
}
const config = await configQuery.orderBy('key', 'asc');
return config.reduce((computedConfig, configEntry) => {
const { key, value } = configEntry;
computedConfig[key] = value?.data;
return computedConfig;
}, defaultConfig);
};
export default getConfig;

View File

@@ -0,0 +1,140 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import { createConfig } from '../../../test/factories/config';
import appConfig from '../../config/app';
import * as license from '../../helpers/license.ee';
describe('graphQL getConfig query', () => {
let configOne, configTwo, configThree, query;
beforeEach(async () => {
configOne = await createConfig({ key: 'configOne' });
configTwo = await createConfig({ key: 'configTwo' });
configThree = await createConfig({ key: 'configThree' });
query = `
query {
getConfig
}
`;
});
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
});
describe('and correct permissions', () => {
it('should return empty config data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = { data: { getConfig: {} } };
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
});
describe('and without providing specific keys', () => {
it('should return all config data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with providing specific keys', () => {
it('should return all config data', async () => {
query = `
query {
getConfig(keys: ["configOne", "configTwo"])
}
`;
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with different defaults', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
});
it('should return custom config', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -0,0 +1,67 @@
import App from '../../models/app.js';
import Flow from '../../models/flow.js';
import Connection from '../../models/connection.js';
const getConnectedApps = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const flowBaseQuery = conditions.isCreator ? userFlows : allFlows;
let apps = await App.findAll(params.name);
const connections = await connectionBaseQuery
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await flowBaseQuery
.clone()
.withGraphJoined('steps')
.orderBy('created_at', 'desc');
const duplicatedUsedApps = flows
.map((flow) => flow.steps.map((step) => step.appKey))
.flat()
.filter(Boolean);
const connectionKeys = connections.map((connection) => connection.key);
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
apps = apps
.filter((app) => {
return usedApps.includes(app.key);
})
.map((app) => {
const connection = connections.find(
(connection) => connection.key === app.key
);
app.connectionCount = connection?.count || 0;
app.flowCount = 0;
flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
if (usedFlow) {
app.flowCount += 1;
}
});
return app;
})
.sort((appA, appB) => appA.name.localeCompare(appB.name));
return apps;
};
export default getConnectedApps;

View File

@@ -0,0 +1,65 @@
import App from '../../models/app.js';
import Step from '../../models/step.js';
import ExecutionStep from '../../models/execution-step.js';
import globalVariable from '../../helpers/global-variable.js';
import computeParameters from '../../helpers/compute-parameters.js';
const getDynamicData = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.withGraphFetched({
connection: true,
flow: true,
})
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!connection || !step.appKey) return null;
const flow = step.flow;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow, step });
const command = app.dynamicData.find((data) => data.key === params.key);
// apply run-time parameters that're not persisted yet
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId
? await ExecutionStep.query().where({
execution_id: lastExecutionId,
})
: [];
// compute variables in parameters
const computedParameters = computeParameters(
$.step.parameters,
priorExecutionSteps
);
$.step.parameters = computedParameters;
const fetchedData = await command.run($);
if (fetchedData.error) {
throw new Error(JSON.stringify(fetchedData.error));
}
return fetchedData.data;
};
export default getDynamicData;

View File

@@ -0,0 +1,40 @@
import App from '../../models/app.js';
import Step from '../../models/step.js';
import globalVariable from '../../helpers/global-variable.js';
const getDynamicFields = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.withGraphFetched({
connection: true,
flow: true,
})
.findById(params.stepId);
if (!step) return null;
const connection = step.connection;
if (!step.appKey) return null;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow: step.flow, step });
const command = app.dynamicFields.find((data) => data.key === params.key);
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const additionalFields = (await command.run($)) || [];
return additionalFields;
};
export default getDynamicFields;

View File

@@ -0,0 +1,19 @@
import Flow from '../../models/flow.js';
const getFlow = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
const flow = await baseQuery
.clone()
.withGraphJoined('[steps.[connection]]')
.orderBy('steps.position', 'asc')
.findOne({ 'flows.id': params.id })
.throwIfNotFound();
return flow;
};
export default getFlow;

View File

@@ -0,0 +1,240 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import appConfig from '../../config/app';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../test/factories/role';
import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
import { createFlow } from '../../../test/factories/flow';
import { createStep } from '../../../test/factories/step';
import { createConnection } from '../../../test/factories/connection';
describe('graphQL getFlow query', () => {
const query = (flowId) => {
return `
query {
getFlow(id: "${flowId}") {
id
name
active
status
steps {
id
type
key
appKey
iconUrl
webhookUrl
status
position
connection {
id
verified
createdAt
}
parameters
}
}
}
`;
};
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const flow = await createFlow();
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(flow.id) })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and with correct permission', () => {
let currentUser, currentUserRole, currentUserFlow;
beforeEach(async () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });
currentUserFlow = await createFlow({ userId: currentUser.id });
});
describe('and with isCreator condition', () => {
it('should return executions data of the current user', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const triggerStep = await createStep({
flowId: currentUserFlow.id,
type: 'trigger',
key: 'catchRawWebhook',
webhookPath: `/webhooks/flows/${currentUserFlow.id}`,
});
const actionConnection = await createConnection({
userId: currentUser.id,
formattedData: {
screenName: 'Test',
authenticationKey: 'test key',
},
});
const actionStep = await createStep({
flowId: currentUserFlow.id,
type: 'action',
connectionId: actionConnection.id,
key: 'translateText',
});
const token = createAuthTokenByUserId(currentUser.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(currentUserFlow.id) })
.expect(200);
const expectedResponsePayload = {
data: {
getFlow: {
active: currentUserFlow.active,
id: currentUserFlow.id,
name: currentUserFlow.name,
status: 'draft',
steps: [
{
appKey: triggerStep.appKey,
connection: null,
iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`,
id: triggerStep.id,
key: 'catchRawWebhook',
parameters: {},
position: 1,
status: triggerStep.status,
type: 'trigger',
webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${currentUserFlow.id}`,
},
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`,
id: actionStep.id,
key: 'translateText',
parameters: {},
position: 1,
status: actionStep.status,
type: 'action',
webhookUrl: 'http://localhost:3000/null',
},
],
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and without isCreator condition', () => {
it('should return executions data of all users', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const triggerStep = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
key: 'catchRawWebhook',
webhookPath: `/webhooks/flows/${anotherUserFlow.id}`,
});
const actionConnection = await createConnection({
userId: anotherUser.id,
formattedData: {
screenName: 'Test',
authenticationKey: 'test key',
},
});
const actionStep = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
connectionId: actionConnection.id,
key: 'translateText',
});
const token = createAuthTokenByUserId(currentUser.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query: query(anotherUserFlow.id) })
.expect(200);
const expectedResponsePayload = {
data: {
getFlow: {
active: anotherUserFlow.active,
id: anotherUserFlow.id,
name: anotherUserFlow.name,
status: 'draft',
steps: [
{
appKey: triggerStep.appKey,
connection: null,
iconUrl: `${appConfig.baseUrl}/apps/${triggerStep.appKey}/assets/favicon.svg`,
id: triggerStep.id,
key: 'catchRawWebhook',
parameters: {},
position: 1,
status: triggerStep.status,
type: 'trigger',
webhookUrl: `${appConfig.baseUrl}/webhooks/flows/${anotherUserFlow.id}`,
},
{
appKey: actionStep.appKey,
connection: {
createdAt: actionConnection.createdAt.getTime().toString(),
id: actionConnection.id,
verified: actionConnection.verified,
},
iconUrl: `${appConfig.baseUrl}/apps/${actionStep.appKey}/assets/favicon.svg`,
id: actionStep.id,
key: 'translateText',
parameters: {},
position: 1,
status: actionStep.status,
type: 'action',
webhookUrl: 'http://localhost:3000/null',
},
],
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -0,0 +1,40 @@
import Flow from '../../models/flow.js';
import paginate from '../../helpers/pagination.js';
const getFlows = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
const flowsQuery = baseQuery
.clone()
.joinRelated({
steps: true,
})
.withGraphFetched({
steps: {
connection: true,
},
})
.where((builder) => {
if (params.connectionId) {
builder.where('steps.connection_id', params.connectionId);
}
if (params.name) {
builder.where('flows.name', 'ilike', `%${params.name}%`);
}
if (params.appKey) {
builder.where('steps.app_key', params.appKey);
}
})
.groupBy('flows.id')
.orderBy('active', 'desc')
.orderBy('updated_at', 'desc');
return paginate(flowsQuery, params.limit, params.offset);
};
export default getFlows;

View File

@@ -0,0 +1,16 @@
import axios from '../../helpers/axios-with-proxy.js';
const NOTIFICATIONS_URL =
'https://notifications.automatisch.io/notifications.json';
const getNotifications = async () => {
try {
const { data: notifications = [] } = await axios.get(NOTIFICATIONS_URL);
return notifications;
} catch (err) {
return [];
}
};
export default getNotifications;

View File

@@ -0,0 +1,34 @@
import { ref } from 'objection';
import ExecutionStep from '../../models/execution-step.js';
import Step from '../../models/step.js';
const getStepWithTestExecutions = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
const step = await stepBaseQuery
.clone()
.findOne({ 'steps.id': params.stepId })
.throwIfNotFound();
const previousStepsWithCurrentStep = await stepBaseQuery
.clone()
.withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position)
.andWhere(
'executionSteps.created_at',
'=',
ExecutionStep.query()
.max('created_at')
.where('step_id', '=', ref('steps.id'))
.andWhere('status', 'success')
)
.orderBy('steps.position', 'asc');
return previousStepsWithCurrentStep;
};
export default getStepWithTestExecutions;

View File

@@ -0,0 +1,19 @@
import paginate from '../../helpers/pagination.js';
import User from '../../models/user.js';
const getUsers = async (_parent, params, context) => {
context.currentUser.can('read', 'User');
const usersQuery = User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.orderBy('full_name', 'asc');
return paginate(usersQuery, params.limit, params.offset);
};
export default getUsers;

View File

@@ -0,0 +1,148 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../test/factories/role';
import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
describe('graphQL getUsers query', () => {
const query = `
query {
getUsers(limit: 10, offset: 0) {
pageInfo {
currentPage
totalPages
}
totalCount
edges {
node {
id
fullName
email
role {
id
name
}
}
}
}
}
`;
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and with correct permissions', () => {
let role, currentUser, anotherUser, token, requestObject;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: role.id,
});
currentUser = await createUser({
roleId: role.id,
fullName: 'Current User',
});
anotherUser = await createUser({
roleId: role.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
requestObject = request(app).post('/graphql').set('Authorization', token);
});
it('should return users data', async () => {
const response = await requestObject.send({ query }).expect(200);
const expectedResponsePayload = {
data: {
getUsers: {
edges: [
{
node: {
email: anotherUser.email,
fullName: anotherUser.fullName,
id: anotherUser.id,
role: {
id: role.id,
name: role.name,
},
},
},
{
node: {
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: {
id: role.id,
name: role.name,
},
},
},
],
pageInfo: {
currentPage: 1,
totalPages: 1,
},
totalCount: 2,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
it('should not return users data with password', async () => {
const query = `
query {
getUsers(limit: 10, offset: 0) {
pageInfo {
currentPage
totalPages
}
totalCount
edges {
node {
id
fullName
password
}
}
}
}
`;
const response = await requestObject.send({ query }).expect(400);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual(
'Cannot query field "password" on type "User".'
);
});
});
});

View File

@@ -0,0 +1,38 @@
import App from '../../models/app.js';
import Connection from '../../models/connection.js';
import globalVariable from '../../helpers/global-variable.js';
const testConnection = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator
? userConnections
: allConnections;
let connection = await connectionBaseQuery
.clone()
.findOne({
id: params.id,
})
.throwIfNotFound();
const app = await App.findOneByKey(connection.key, false);
const $ = await globalVariable({ connection, app });
let isStillVerified;
try {
isStillVerified = !!(await app.auth.isStillVerified($));
} catch {
isStillVerified = false;
}
connection = await connection.$query().patchAndFetch({
formattedData: connection.formattedData,
verified: isStillVerified,
});
return connection;
};
export default testConnection;

View File

@@ -0,0 +1,33 @@
import getApp from './queries/get-app.js';
import getAppAuthClient from './queries/get-app-auth-client.ee.js';
import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
import getConfig from './queries/get-config.ee.js';
import getConnectedApps from './queries/get-connected-apps.js';
import getDynamicData from './queries/get-dynamic-data.js';
import getDynamicFields from './queries/get-dynamic-fields.js';
import getFlow from './queries/get-flow.js';
import getFlows from './queries/get-flows.js';
import getNotifications from './queries/get-notifications.js';
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
import getUsers from './queries/get-users.js';
import testConnection from './queries/test-connection.js';
const queryResolvers = {
getApp,
getAppAuthClient,
getAppAuthClients,
getBillingAndUsage,
getConfig,
getConnectedApps,
getDynamicData,
getDynamicFields,
getFlow,
getFlows,
getNotifications,
getStepWithTestExecutions,
getUsers,
testConnection,
};
export default queryResolvers;

View File

@@ -1,6 +1,8 @@
import mutationResolvers from './mutation-resolvers.js'; import mutationResolvers from './mutation-resolvers.js';
import queryResolvers from './query-resolvers.js';
const resolvers = { const resolvers = {
Query: queryResolvers,
Mutation: mutationResolvers, Mutation: mutationResolvers,
}; };

View File

@@ -1,6 +1,34 @@
type Query { type Query {
placeholderQuery(name: String): Boolean getApp(key: String!): App
getAppAuthClient(id: String!): AppAuthClient
getAppAuthClients(appKey: String!, active: Boolean): [AppAuthClient]
getConnectedApps(name: String): [App]
testConnection(id: String!): Connection
getFlow(id: String!): Flow
getFlows(
limit: Int!
offset: Int!
appKey: String
connectionId: String
name: String
): FlowConnection
getStepWithTestExecutions(stepId: String!): [Step]
getDynamicData(
stepId: String!
key: String!
parameters: JSONObject
): JSONObject
getDynamicFields(
stepId: String!
key: String!
parameters: JSONObject
): [SubstepArgument]
getBillingAndUsage: GetBillingAndUsage
getConfig(keys: [String]): JSONObject
getNotifications: [Notification]
getUsers(limit: Int!, offset: Int!): UserConnection
} }
type Mutation { type Mutation {
createAppConfig(input: CreateAppConfigInput): AppConfig createAppConfig(input: CreateAppConfigInput): AppConfig
createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient
@@ -229,6 +257,15 @@ type Field {
options: [SubstepArgumentOption] options: [SubstepArgumentOption]
} }
type FlowConnection {
edges: [FlowEdge]
pageInfo: PageInfo
}
type FlowEdge {
node: Flow
}
enum FlowStatus { enum FlowStatus {
paused paused
published published
@@ -267,6 +304,16 @@ type SamlAuthProvidersRoleMapping {
remoteRoleName: String remoteRoleName: String
} }
type UserConnection {
edges: [UserEdge]
pageInfo: PageInfo
totalCount: Int
}
type UserEdge {
node: User
}
input CreateConnectionInput { input CreateConnectionInput {
key: String! key: String!
appAuthClientId: String appAuthClientId: String
@@ -550,6 +597,43 @@ type License {
verified: Boolean verified: Boolean
} }
type GetBillingAndUsage {
subscription: Subscription
usage: Usage
}
type MonthlyQuota {
title: String
action: BillingCardAction
}
type NextBillAmount {
title: String
action: BillingCardAction
}
type NextBillDate {
title: String
action: BillingCardAction
}
type BillingCardAction {
type: String
text: String
src: String
}
type Subscription {
status: String
monthlyQuota: MonthlyQuota
nextBillAmount: NextBillAmount
nextBillDate: NextBillDate
}
type Usage {
task: Int
}
type Permission { type Permission {
id: String id: String
action: String action: String
@@ -608,6 +692,13 @@ input UpdateAppAuthClientInput {
active: Boolean active: Boolean
} }
type Notification {
name: String
createdAt: String
documentationUrl: String
description: String
}
schema { schema {
query: Query query: Query
mutation: Mutation mutation: Mutation

View File

@@ -40,6 +40,11 @@ export const authenticateUser = async (request, response, next) => {
const isAuthenticatedRule = rule()(isAuthenticated); const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = { export const authenticationRules = {
Query: {
'*': isAuthenticatedRule,
getConfig: allow,
getNotifications: allow,
},
Mutation: { Mutation: {
'*': isAuthenticatedRule, '*': isAuthenticatedRule,
forgotPassword: allow, forgotPassword: allow,

View File

@@ -42,7 +42,6 @@ describe('authentication rules', () => {
const { queries, mutations } = getQueryAndMutationNames(authenticationRules); const { queries, mutations } = getQueryAndMutationNames(authenticationRules);
if (queries.length) {
describe('for queries', () => { describe('for queries', () => {
queries.forEach((query) => { queries.forEach((query) => {
it(`should apply correct rule for query: ${query}`, () => { it(`should apply correct rule for query: ${query}`, () => {
@@ -56,7 +55,6 @@ describe('authentication rules', () => {
}); });
}); });
}); });
}
describe('for mutations', () => { describe('for mutations', () => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {

View File

@@ -7,10 +7,6 @@ const authorizationList = {
action: 'read', action: 'read',
subject: 'User', subject: 'User',
}, },
'GET /api/v1/users/:userId/apps': {
action: 'read',
subject: 'Connection',
},
'GET /api/v1/flows/:flowId': { 'GET /api/v1/flows/:flowId': {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
@@ -23,34 +19,14 @@ const authorizationList = {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
}, },
'GET /api/v1/steps/:stepId/previous-steps': {
action: 'update',
subject: 'Flow',
},
'POST /api/v1/steps/:stepId/dynamic-fields': {
action: 'update',
subject: 'Flow',
},
'POST /api/v1/steps/:stepId/dynamic-data': {
action: 'update',
subject: 'Flow',
},
'GET /api/v1/connections/:connectionId/flows': { 'GET /api/v1/connections/:connectionId/flows': {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
}, },
'POST /api/v1/connections/:connectionId/test': {
action: 'update',
subject: 'Connection',
},
'GET /api/v1/apps/:appKey/flows': { 'GET /api/v1/apps/:appKey/flows': {
action: 'read', action: 'read',
subject: 'Flow', subject: 'Flow',
}, },
'GET /api/v1/apps/:appKey/connections': {
action: 'read',
subject: 'Connection',
},
'GET /api/v1/executions/:executionId': { 'GET /api/v1/executions/:executionId': {
action: 'read', action: 'read',
subject: 'Execution', subject: 'Execution',

View File

@@ -2,7 +2,7 @@ import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
const config = axios.defaults; const config = {};
const httpProxyUrl = process.env.http_proxy; const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = process.env.https_proxy; const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl; const supportsProxy = httpProxyUrl || httpsProxyUrl;

View File

@@ -2,7 +2,6 @@ import logger from './logger.js';
import objection from 'objection'; import objection from 'objection';
import * as Sentry from './sentry.ee.js'; import * as Sentry from './sentry.ee.js';
const { NotFoundError, DataError } = objection; const { NotFoundError, DataError } = objection;
import HttpError from '../errors/http.js';
// Do not remove `next` argument as the function signature will not fit for an error handler middleware // Do not remove `next` argument as the function signature will not fit for an error handler middleware
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@@ -19,17 +18,6 @@ const errorHandler = (error, request, response, next) => {
response.status(400).end(); response.status(400).end();
} }
if (error instanceof HttpError) {
const httpErrorPayload = {
errors: JSON.parse(error.message),
meta: {
type: 'HttpError',
},
};
response.status(200).json(httpErrorPayload);
}
const statusCode = error.statusCode || 500; const statusCode = error.statusCode || 500;
logger.error(request.method + ' ' + request.url + ' ' + statusCode); logger.error(request.method + ' ' + request.url + ' ' + statusCode);
@@ -49,7 +37,7 @@ const errorHandler = (error, request, response, next) => {
const notFoundAppError = (error) => { const notFoundAppError = (error) => {
return ( return (
error.message.includes('An application with the') && error.message.includes('An application with the') ||
error.message.includes("key couldn't be found.") error.message.includes("key couldn't be found.")
); );
}; };

View File

@@ -9,7 +9,7 @@ const stream = {
const registerGraphQLToken = () => { const registerGraphQLToken = () => {
morgan.token('graphql-query', (req) => { morgan.token('graphql-query', (req) => {
if (req.body.query) { if (req.body.query) {
return `\n GraphQL ${req.body.query}`; return `GraphQL ${req.body.query}`;
} }
}); });
}; };
@@ -17,7 +17,7 @@ const registerGraphQLToken = () => {
registerGraphQLToken(); registerGraphQLToken();
const morganMiddleware = morgan( const morganMiddleware = morgan(
':method :url :status :res[content-length] - :response-time ms :graphql-query', ':method :url :status :res[content-length] - :response-time ms\n:graphql-query',
{ stream } { stream }
); );

View File

@@ -44,22 +44,4 @@ const renderObject = (response, object, options) => {
return response.json(computedPayload); return response.json(computedPayload);
}; };
const renderError = (response, errors, status, type) => { export { renderObject };
const errorStatus = status || 422;
const errorType = type || 'ValidationError';
const payload = {
errors: errors.reduce((acc, error) => {
const key = Object.keys(error)[0];
acc[key] = error[key];
return acc;
}, {}),
meta: {
type: errorType,
},
};
return response.status(errorStatus).send(payload);
};
export { renderObject, renderError };

View File

@@ -75,20 +75,9 @@ export default async (flowId, request, response) => {
}); });
if (actionStep.key === 'respondWith' && !response.headersSent) { if (actionStep.key === 'respondWith' && !response.headersSent) {
const { headers, statusCode, body } = actionExecutionStep.dataOut;
// we set the custom response headers
if (headers) {
for (const [key, value] of Object.entries(headers)) {
if (key) {
response.set(key, value);
}
}
}
// we send the response only if it's not sent yet. This allows us to early respond from the flow. // we send the response only if it's not sent yet. This allows us to early respond from the flow.
response.status(statusCode); response.status(actionExecutionStep.dataOut.statusCode);
response.send(body); response.send(actionExecutionStep.dataOut.body);
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import AES from 'crypto-js/aes.js'; import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js'; import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
import AppConfig from './app-config.js';
import Base from './base.js'; import Base from './base.js';
class AppAuthClient extends Base { class AppAuthClient extends Base {
@@ -8,11 +9,11 @@ class AppAuthClient extends Base {
static jsonSchema = { static jsonSchema = {
type: 'object', type: 'object',
required: ['name', 'appKey', 'formattedAuthDefaults'], required: ['name', 'appConfigId', 'formattedAuthDefaults'],
properties: { properties: {
id: { type: 'string', format: 'uuid' }, id: { type: 'string', format: 'uuid' },
appKey: { type: 'string' }, appConfigId: { type: 'string', format: 'uuid' },
active: { type: 'boolean' }, active: { type: 'boolean' },
authDefaults: { type: ['string', 'null'] }, authDefaults: { type: ['string', 'null'] },
formattedAuthDefaults: { type: 'object' }, formattedAuthDefaults: { type: 'object' },
@@ -21,6 +22,17 @@ class AppAuthClient extends Base {
}, },
}; };
static relationMappings = () => ({
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_config_id',
to: 'app_configs.id',
},
},
});
encryptData() { encryptData() {
if (!this.eligibleForEncryption()) return; if (!this.eligibleForEncryption()) return;

View File

@@ -1,6 +1,6 @@
import App from './app.js'; import App from './app.js';
import AppAuthClient from './app-auth-client.js';
import Base from './base.js'; import Base from './base.js';
import AppAuthClient from './app-auth-client.js';
class AppConfig extends Base { class AppConfig extends Base {
static tableName = 'app_configs'; static tableName = 'app_configs';
@@ -18,21 +18,21 @@ class AppConfig extends Base {
}, },
}; };
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
static relationMappings = () => ({ static relationMappings = () => ({
appAuthClients: { appAuthClients: {
relation: Base.HasManyRelation, relation: Base.HasManyRelation,
modelClass: AppAuthClient, modelClass: AppAuthClient,
join: { join: {
from: 'app_configs.key', from: 'app_configs.id',
to: 'app_auth_clients.app_key', to: 'app_auth_clients.app_config_id',
}, },
}, },
}); });
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
get canCustomConnect() { get canCustomConnect() {
return !this.disabled && this.allowCustomConnection; return !this.disabled && this.allowCustomConnection;
} }

View File

@@ -153,24 +153,6 @@ class Connection extends Base {
return await App.findOneByKey(this.key); return await App.findOneByKey(this.key);
} }
async testAndUpdateConnection() {
const app = await this.getApp();
const $ = await globalVariable({ connection: this, app });
let isStillVerified;
try {
isStillVerified = !!(await app.auth.isStillVerified($));
} catch {
isStillVerified = false;
}
return await this.$query().patchAndFetch({
formattedData: this.formattedData,
verified: isStillVerified,
});
}
async verifyWebhook(request) { async verifyWebhook(request) {
if (!this.key) return true; if (!this.key) return true;

View File

@@ -160,7 +160,7 @@ class Flow extends Base {
} }
async isPaused() { async isPaused() {
const user = await this.$relatedQuery('user').withSoftDeleted(); const user = await this.$relatedQuery('user');
const allowedToRunFlows = await user.isAllowedToRunFlows(); const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true; return allowedToRunFlows ? false : true;
} }

View File

@@ -7,8 +7,6 @@ import Connection from './connection.js';
import ExecutionStep from './execution-step.js'; import ExecutionStep from './execution-step.js';
import Telemetry from '../helpers/telemetry/index.js'; import Telemetry from '../helpers/telemetry/index.js';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
import globalVariable from '../helpers/global-variable.js';
import computeParameters from '../helpers/compute-parameters.js';
class Step extends Base { class Step extends Base {
static tableName = 'steps'; static tableName = 'steps';
@@ -198,59 +196,6 @@ class Step extends Base {
return existingArguments; return existingArguments;
} }
async createDynamicFields(dynamicFieldsKey, parameters) {
const connection = await this.$relatedQuery('connection');
const flow = await this.$relatedQuery('flow');
const app = await this.getApp();
const $ = await globalVariable({ connection, app, flow, step: this });
const command = app.dynamicFields.find(
(data) => data.key === dynamicFieldsKey
);
for (const parameterKey in parameters) {
const parameterValue = parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const dynamicFields = (await command.run($)) || [];
return dynamicFields;
}
async createDynamicData(dynamicDataKey, parameters) {
const connection = await this.$relatedQuery('connection');
const flow = await this.$relatedQuery('flow');
const app = await this.getApp();
const $ = await globalVariable({ connection, app, flow, step: this });
const command = app.dynamicData.find((data) => data.key === dynamicDataKey);
for (const parameterKey in parameters) {
const parameterValue = parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId
? await ExecutionStep.query().where({
execution_id: lastExecutionId,
})
: [];
const computedParameters = computeParameters(
$.step.parameters,
priorExecutionSteps
);
$.step.parameters = computedParameters;
const dynamicData = (await command.run($)).data;
return dynamicData;
}
async updateWebhookUrl() { async updateWebhookUrl() {
if (this.isAction) return this; if (this.isAction) return this;

View File

@@ -5,9 +5,7 @@ import crypto from 'node:crypto';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
import { hasValidLicense } from '../helpers/license.ee.js'; import { hasValidLicense } from '../helpers/license.ee.js';
import userAbility from '../helpers/user-ability.js'; import userAbility from '../helpers/user-ability.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
import Base from './base.js'; import Base from './base.js';
import App from './app.js';
import Connection from './connection.js'; import Connection from './connection.js';
import Execution from './execution.js'; import Execution from './execution.js';
import Flow from './flow.js'; import Flow from './flow.js';
@@ -156,13 +154,6 @@ class User extends Base {
return conditions.isCreator ? this.$relatedQuery('steps') : Step.query(); return conditions.isCreator ? this.$relatedQuery('steps') : Step.query();
} }
get authorizedConnections() {
const conditions = this.can('read', 'Connection');
return conditions.isCreator
? this.$relatedQuery('connections')
: Connection.query();
}
get authorizedExecutions() { get authorizedExecutions() {
const conditions = this.can('read', 'Execution'); const conditions = this.can('read', 'Execution');
return conditions.isCreator return conditions.isCreator
@@ -170,17 +161,6 @@ class User extends Base {
: Execution.query(); : Execution.query();
} }
static async authenticate(email, password) {
const user = await User.query().findOne({
email: email?.toLowerCase() || null,
});
if (user && (await user.login(password))) {
const token = createAuthTokenByUserId(user.id);
return token;
}
}
login(password) { login(password) {
return bcrypt.compare(password, this.password); return bcrypt.compare(password, this.password);
} }
@@ -314,56 +294,6 @@ class User extends Base {
return invoices; return invoices;
} }
async getApps(name) {
const connections = await this.authorizedConnections
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await this.authorizedFlows
.clone()
.withGraphJoined('steps')
.orderBy('created_at', 'desc');
const duplicatedUsedApps = flows
.map((flow) => flow.steps.map((step) => step.appKey))
.flat()
.filter(Boolean);
const connectionKeys = connections.map((connection) => connection.key);
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
let apps = await App.findAll(name);
apps = apps
.filter((app) => {
return usedApps.includes(app.key);
})
.map((app) => {
const connection = connections.find(
(connection) => connection.key === app.key
);
app.connectionCount = connection?.count || 0;
app.flowCount = 0;
flows.forEach((flow) => {
const usedFlow = flow.steps.find((step) => step.appKey === app.key);
if (usedFlow) {
app.flowCount += 1;
}
});
return app;
})
.sort((appA, appB) => appA.name.localeCompare(appB.name));
return apps;
}
async $beforeInsert(queryContext) { async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext); await super.$beforeInsert(queryContext);

View File

@@ -1,9 +0,0 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import createAccessTokenAction from '../../../controllers/api/v1/access-tokens/create-access-token.js';
const router = Router();
router.post('/', asyncHandler(createAccessTokenAction));
export default router;

View File

@@ -3,25 +3,16 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../../helpers/authentication.js'; import { authenticateUser } from '../../../../helpers/authentication.js';
import { authorizeAdmin } from '../../../../helpers/authorization.js'; import { authorizeAdmin } from '../../../../helpers/authorization.js';
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
import getAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-auth-clients.ee.js'; import getAdminAppAuthClientsAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-client.js';
import getAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-auth-client.ee.js';
const router = Router(); const router = Router();
router.get( router.get(
'/:appKey/auth-clients', '/:appAuthClientId',
authenticateUser, authenticateUser,
authorizeAdmin, authorizeAdmin,
checkIsEnterprise, checkIsEnterprise,
asyncHandler(getAuthClientsAction) asyncHandler(getAdminAppAuthClientsAction)
);
router.get(
'/:appKey/auth-clients/:appAuthClientId',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
asyncHandler(getAuthClientAction)
); );
export default router; export default router;

View File

@@ -0,0 +1,16 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAuthClientAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-client.js';
const router = Router();
router.get(
'/:appAuthClientId',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppAuthClientAction)
);
export default router;

View File

@@ -0,0 +1,16 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppConfigAction from '../../../controllers/api/v1/app-configs/get-app-config.ee.js';
const router = Router();
router.get(
'/:appKey',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAppConfigAction)
);
export default router;

View File

@@ -2,14 +2,9 @@ import { Router } from 'express';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js'; import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js'; import { authorizeUser } from '../../../helpers/authorization.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getAppAction from '../../../controllers/api/v1/apps/get-app.js'; import getAppAction from '../../../controllers/api/v1/apps/get-app.js';
import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js'; import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js';
import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js'; import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js';
import getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.js';
import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js';
import getAuthClientsAction from '../../../controllers/api/v1/apps/get-auth-clients.ee.js';
import getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js';
import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js'; import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js';
import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js'; import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js';
import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js'; import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js';
@@ -22,34 +17,6 @@ router.get('/', authenticateUser, asyncHandler(getAppsAction));
router.get('/:appKey', authenticateUser, asyncHandler(getAppAction)); router.get('/:appKey', authenticateUser, asyncHandler(getAppAction));
router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction)); router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction));
router.get(
'/:appKey/connections',
authenticateUser,
authorizeUser,
asyncHandler(getConnectionsAction)
);
router.get(
'/:appKey/config',
authenticateUser,
checkIsEnterprise,
asyncHandler(getConfigAction)
);
router.get(
'/:appKey/auth-clients',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAuthClientsAction)
);
router.get(
'/:appKey/auth-clients/:appAuthClientId',
authenticateUser,
checkIsEnterprise,
asyncHandler(getAuthClientAction)
);
router.get( router.get(
'/:appKey/triggers', '/:appKey/triggers',
authenticateUser, authenticateUser,

View File

@@ -3,7 +3,6 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js'; import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js'; import { authorizeUser } from '../../../helpers/authorization.js';
import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js'; import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js';
import createTestAction from '../../../controllers/api/v1/connections/create-test.js';
const router = Router(); const router = Router();
@@ -14,11 +13,4 @@ router.get(
asyncHandler(getFlowsAction) asyncHandler(getFlowsAction)
); );
router.post(
'/:connectionId/test',
authenticateUser,
authorizeUser,
asyncHandler(createTestAction)
);
export default router; export default router;

View File

@@ -3,9 +3,6 @@ import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js'; import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js'; import { authorizeUser } from '../../../helpers/authorization.js';
import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js';
import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js';
import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js';
import createDynamicDataAction from '../../../controllers/api/v1/steps/create-dynamic-data.js';
const router = Router(); const router = Router();
@@ -16,25 +13,4 @@ router.get(
asyncHandler(getConnectionAction) asyncHandler(getConnectionAction)
); );
router.get(
'/:stepId/previous-steps',
authenticateUser,
authorizeUser,
asyncHandler(getPreviousStepsAction)
);
router.post(
'/:stepId/dynamic-fields',
authenticateUser,
authorizeUser,
asyncHandler(createDynamicFieldsAction)
);
router.post(
'/:stepId/dynamic-data',
authenticateUser,
authorizeUser,
asyncHandler(createDynamicDataAction)
);
export default router; export default router;

View File

@@ -1,11 +1,9 @@
import { Router } from 'express'; import { Router } from 'express';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { authenticateUser } from '../../../helpers/authentication.js'; import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import checkIsCloud from '../../../helpers/check-is-cloud.js'; import checkIsCloud from '../../../helpers/check-is-cloud.js';
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js'; import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
import getAppsAction from '../../../controllers/api/v1/users/get-apps.js';
import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js'; import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js';
import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js'; import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js';
import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js'; import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js';
@@ -13,14 +11,6 @@ import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-an
const router = Router(); const router = Router();
router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction)); router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction));
router.get(
'/:userId/apps',
authenticateUser,
authorizeUser,
asyncHandler(getAppsAction)
);
router.get( router.get(
'/invoices', '/invoices',
authenticateUser, authenticateUser,

View File

@@ -4,20 +4,21 @@ import webhooksRouter from './webhooks.js';
import paddleRouter from './paddle.ee.js'; import paddleRouter from './paddle.ee.js';
import healthcheckRouter from './healthcheck.js'; import healthcheckRouter from './healthcheck.js';
import automatischRouter from './api/v1/automatisch.js'; import automatischRouter from './api/v1/automatisch.js';
import accessTokensRouter from './api/v1/access-tokens.js';
import usersRouter from './api/v1/users.js'; import usersRouter from './api/v1/users.js';
import paymentRouter from './api/v1/payment.ee.js'; import paymentRouter from './api/v1/payment.ee.js';
import appAuthClientsRouter from './api/v1/app-auth-clients.js';
import appConfigsRouter from './api/v1/app-configs.ee.js';
import flowsRouter from './api/v1/flows.js'; import flowsRouter from './api/v1/flows.js';
import stepsRouter from './api/v1/steps.js'; import stepsRouter from './api/v1/steps.js';
import appsRouter from './api/v1/apps.js'; import appsRouter from './api/v1/apps.js';
import connectionsRouter from './api/v1/connections.js'; import connectionsRouter from './api/v1/connections.js';
import executionsRouter from './api/v1/executions.js'; import executionsRouter from './api/v1/executions.js';
import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js'; import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js';
import adminAppsRouter from './api/v1/admin/apps.ee.js';
import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
import rolesRouter from './api/v1/admin/roles.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js';
import permissionsRouter from './api/v1/admin/permissions.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js';
import adminUsersRouter from './api/v1/admin/users.ee.js'; import adminUsersRouter from './api/v1/admin/users.ee.js';
import adminAppAuthClientsRouter from './api/v1/admin/app-auth-clients.js';
const router = Router(); const router = Router();
@@ -26,19 +27,20 @@ router.use('/webhooks', webhooksRouter);
router.use('/paddle', paddleRouter); router.use('/paddle', paddleRouter);
router.use('/healthcheck', healthcheckRouter); router.use('/healthcheck', healthcheckRouter);
router.use('/api/v1/automatisch', automatischRouter); router.use('/api/v1/automatisch', automatischRouter);
router.use('/api/v1/access-tokens', accessTokensRouter);
router.use('/api/v1/users', usersRouter); router.use('/api/v1/users', usersRouter);
router.use('/api/v1/payment', paymentRouter); router.use('/api/v1/payment', paymentRouter);
router.use('/api/v1/apps', appsRouter); router.use('/api/v1/app-auth-clients', appAuthClientsRouter);
router.use('/api/v1/connections', connectionsRouter); router.use('/api/v1/app-configs', appConfigsRouter);
router.use('/api/v1/flows', flowsRouter); router.use('/api/v1/flows', flowsRouter);
router.use('/api/v1/steps', stepsRouter); router.use('/api/v1/steps', stepsRouter);
router.use('/api/v1/apps', appsRouter);
router.use('/api/v1/connections', connectionsRouter);
router.use('/api/v1/executions', executionsRouter); router.use('/api/v1/executions', executionsRouter);
router.use('/api/v1/saml-auth-providers', samlAuthProvidersRouter); router.use('/api/v1/saml-auth-providers', samlAuthProvidersRouter);
router.use('/api/v1/admin/apps', adminAppsRouter); router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
router.use('/api/v1/admin/users', adminUsersRouter);
router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/roles', rolesRouter);
router.use('/api/v1/admin/permissions', permissionsRouter); router.use('/api/v1/admin/permissions', permissionsRouter);
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter); router.use('/api/v1/admin/users', adminUsersRouter);
router.use('/api/v1/admin/app-auth-clients', adminAppAuthClientsRouter);
export default router; export default router;

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