Compare commits

...

79 Commits

Author SHA1 Message Date
Rıdvan Akca
703e434e5c feat(pdf-monkey): add delete document action 2024-02-28 16:29:01 +03:00
Rıdvan Akca
c12dacfa40 feat(pdf-monkey): add generate document action 2024-02-28 15:50:17 +03:00
Rıdvan Akca
71a404063c feat(pdf-monkey): add documents generated trigger 2024-02-27 16:40:51 +03:00
Rıdvan Akca
8bb7b16c0e feat(pdf-monkey): add pdf-monkey integration 2024-02-22 17:32:38 +03:00
Ömer Faruk Aydın
d7893d9a32 Merge pull request #1621 from automatisch/show-saml-auth-provider
feat: Implement API endpoint to show saml auth provider
2024-02-20 13:00:25 +01:00
Ömer Faruk Aydın
9cbdda330c Merge pull request #1620 from automatisch/fix-authorization-middleware
fix: Include http methods for route rules
2024-02-20 12:59:12 +01:00
Ömer Faruk Aydın
42a9bfd099 Merge pull request #1619 from automatisch/get-saml-auth-providers
feat: Implement get saml auth providers API endpoint
2024-02-20 12:59:03 +01:00
Faruk AYDIN
eb15bd01ca feat: Implement API endpoint to show saml auth provider 2024-02-19 23:41:37 +01:00
Faruk AYDIN
9e98aebeb3 fix: Include http methods for route rules 2024-02-19 22:22:04 +01:00
Faruk AYDIN
1361cbc826 chore: Remove get saml auth providers from authorization list 2024-02-19 22:19:37 +01:00
Faruk AYDIN
679d0808a9 refactor: Move saml auth providers endpoint to admin namespace 2024-02-19 22:18:15 +01:00
Faruk AYDIN
6fe9a548ad feat: Implement get saml auth providers API endpoint 2024-02-19 21:48:06 +01:00
Faruk AYDIN
2d6d2430d2 fix: Detect types also for not paginated arrays 2024-02-19 21:46:20 +01:00
Faruk AYDIN
a445538e81 feat: Implement isCheckEnterprise middleware 2024-02-19 21:22:36 +01:00
Faruk AYDIN
50d38ffbd8 chore: Make http log level lower than info 2024-02-19 21:14:54 +01:00
Faruk AYDIN
93bcdfd9c9 feat: Implement saml auth provider serializer 2024-02-19 17:59:18 +01:00
Faruk AYDIN
5be3b101a5 feat: Implement saml auth provider factory 2024-02-19 17:58:52 +01:00
Ömer Faruk Aydın
024c7476c7 Merge pull request #1616 from automatisch/node-18-in-devcontainer
refactor: use node 18 in devcontainer
2024-02-19 11:51:59 +01:00
Ali BARIN
30a7ffe93d refactor: use node 18 in devcontainer 2024-02-19 10:17:16 +00:00
kattoczko
e2d803ebf7 feat: do not let users access notifications page when it's turned off (#1583) 2024-02-16 16:15:01 +01:00
kattoczko
be7e67c940 feat: introduce 404 page (#1600) 2024-02-16 15:54:35 +01:00
kattoczko
ead4b13ba5 feat: Show /login directly on / without valid authentication (#1528) 2024-02-16 15:15:25 +01:00
Ömer Faruk Aydın
e02c42ee18 Merge pull request #1605 from automatisch/test-permission-serializer
test: Add tests for permission serializer
2024-02-16 12:41:09 +01:00
Ömer Faruk Aydın
d39886fdf8 Merge pull request #1604 from automatisch/test-role-serializer
test: Add tests for role serializer
2024-02-16 12:40:59 +01:00
Ömer Faruk Aydın
11a425f1de Merge pull request #1603 from automatisch/test-user-serializer
test: Add tests for user serializer
2024-02-16 12:40:48 +01:00
Ömer Faruk Aydın
f0e194e584 Merge pull request #1606 from automatisch/remove-redundant
chore: Remove redundant npm libraries
2024-02-16 12:37:34 +01:00
Faruk AYDIN
d4b9331cf2 chore: Remove redundant npm libraries 2024-02-16 01:01:50 +01:00
Faruk AYDIN
37e1acc5f1 test: Add tests for permission serializer 2024-02-16 00:19:53 +01:00
Faruk AYDIN
ffaf6a577d test: Add tests for role serializer 2024-02-16 00:18:53 +01:00
Faruk AYDIN
afdaf6ba39 test: Add tests for user serializer 2024-02-16 00:10:37 +01:00
Ömer Faruk Aydın
4c49367910 Merge pull request #1602 from automatisch/introduce-serializers
feat: Introduce serializers
2024-02-15 12:46:20 +01:00
Ömer Faruk Aydın
a506c4411d Merge pull request #1601 from automatisch/rest-get-trial
feat: Implement API endpoint for user trial info
2024-02-15 12:45:54 +01:00
Faruk AYDIN
1859c9854e chore: Add permission serializer to serializers 2024-02-15 02:21:26 +01:00
Faruk AYDIN
6ff29b9ae6 refactor: Use serializer for user model instead of formatJson 2024-02-15 02:19:24 +01:00
Faruk AYDIN
3578f6b849 feat: Extend renderer to use serializers 2024-02-15 02:15:44 +01:00
Faruk AYDIN
0347864fde feat: Introduce serializers 2024-02-15 02:15:19 +01:00
Faruk AYDIN
5f9786a2c7 chore: Adjust get user trial file to have .ee extension 2024-02-14 15:55:02 +01:00
Faruk AYDIN
75aeff1898 test: Cover removed user token for authentication tests 2024-02-14 15:48:49 +01:00
Faruk AYDIN
0afcdce6d3 refactor: Do not expose subscription info for get user trial 2024-02-14 14:23:54 +01:00
Faruk AYDIN
a591d0ea87 test: Add tests for get user trial action 2024-02-14 14:18:42 +01:00
Faruk AYDIN
0e111a3532 feat: Implement API endpoint for user trial info 2024-02-14 14:18:28 +01:00
Faruk AYDIN
b599466ffa feat: Add checkIsCloud middleware for routes 2024-02-14 13:06:58 +01:00
Faruk AYDIN
69727e78df fix: Throw not found error for authentication 2024-02-14 13:06:29 +01:00
Ömer Faruk Aydın
02ae67b147 Merge pull request #1597 from automatisch/rest-api-get-users
feat: Implement api/v1/users API endpoint
2024-02-14 11:38:09 +01:00
Faruk AYDIN
a769f78801 test: Add tests for api/v1/users API endpoint 2024-02-14 01:20:29 +01:00
Faruk AYDIN
d583e42428 refactor: Structure api mock data with folders 2024-02-14 01:02:42 +01:00
Faruk AYDIN
da732becb6 feat: Implement api/v1/users API endpoint 2024-02-14 00:52:17 +01:00
Faruk AYDIN
b89a4d58d9 feat: Add pagination for REST endpoints 2024-02-14 00:51:48 +01:00
Faruk AYDIN
09854147d1 feat: Extend renderer functionality to work with pagination 2024-02-14 00:51:16 +01:00
Ömer Faruk Aydın
3648c2bfe3 Merge pull request #1592 from automatisch/rest-api-get-user
feat: Implement api/v1/users/:userId API endpoint
2024-02-13 12:14:37 +01:00
Ömer Faruk Aydın
3f3ee032f6 Merge pull request #1591 from automatisch/rest-api-get-current-user
feat: Implement users/me API endpoint
2024-02-13 11:34:37 +01:00
Ömer Faruk Aydın
68e5d54331 Merge pull request #1590 from automatisch/rest-api-automatisch-version
feat: Implement automatisch version API endpoint
2024-02-13 10:42:44 +01:00
Faruk AYDIN
824c434b0b feat: Implement api/v1/users/:userId API endpoint 2024-02-13 03:44:44 +01:00
Faruk AYDIN
9f0e0ca656 feat: Implement users/me API endpoint 2024-02-13 02:06:24 +01:00
Faruk AYDIN
95f89ba03e refactor: Use objectionjs instead of knex for factories 2024-02-13 01:49:40 +01:00
Faruk AYDIN
697f72ecf4 refactor: Use api/v1 namespace for routes 2024-02-12 23:30:38 +01:00
Faruk AYDIN
4f03f2ab51 feat: Introduce renderer helper 2024-02-12 23:25:09 +01:00
Faruk AYDIN
c81531cb7a feat: Implement automatisch version API endpoint 2024-02-12 22:59:44 +01:00
Ömer Faruk Aydın
7b6e4aa153 Merge pull request #1589 from automatisch/healthcheck-api-endpoint
feat: Implement healthcheck api endpoint
2024-02-12 22:57:58 +01:00
Faruk AYDIN
f21039d19d feat: Implement healthcheck api endpoint 2024-02-12 22:50:57 +01:00
morihoos
8c936a91be fix(csp): remove illegal characters in directive names (#1585) 2024-02-08 17:41:33 +01:00
Ali BARIN
24451892ff feat: add custom additional drawer link (#1586) 2024-02-08 16:33:12 +01:00
morihoos
6bba2c82fe feat(config): add ability to override apiUrl in environment variables (#1581) 2024-02-07 16:17:03 +01:00
Ali BARIN
3320dc6bc4 Merge pull request #1582 from automatisch/toggle-favicon-and-notifications-page
feat: put favicon and notifications page behind feature flags
2024-02-07 14:46:59 +01:00
Ali BARIN
9d42fd9293 test(queries/get-config): incorporate feature flags
cover disableNotificationsPage and disableFavicon feature flags
2024-02-07 13:11:57 +00:00
Ali BARIN
e6b806616f feat: add DISABLE_FAVICON feature flag 2024-02-07 11:51:17 +00:00
Ali BARIN
6ec5872391 feat: add DISABLE_NOTIFICATIONS_PAGE feature flag 2024-02-07 11:47:44 +00:00
Ali BARIN
a26cf932a1 chore(devcontainer): upgrade node to 20 2024-02-07 11:46:12 +00:00
Ali BARIN
38a3e3ab9f Merge pull request #1570 from automatisch/prevent-public-registrations
fix: prevent registration on non-cloud
2024-01-30 14:11:05 +01:00
Ali BARIN
32b17c1418 fix: prevent registration on non-cloud 2024-01-30 13:03:52 +00:00
Ömer Faruk Aydın
44aa6a1579 Merge pull request #1569 from automatisch/compile-email
fix: Adjust dirname for compile email helper
2024-01-29 13:05:58 +01:00
Faruk AYDIN
2369aacd2a fix: Adjust dirname for compile email helper 2024-01-29 12:48:21 +01:00
Ömer Faruk Aydın
7dafc6364b Merge pull request #1552 from automatisch/dependabot/npm_and_yarn/vite-3.2.8
chore(deps): Bump vite from 3.2.7 to 3.2.8
2024-01-29 12:32:10 +01:00
Ömer Faruk Aydın
3d25fa0aeb Merge branch 'main' into dependabot/npm_and_yarn/vite-3.2.8 2024-01-29 11:14:49 +01:00
Ali BARIN
0297b0f296 Merge pull request #1559 from automatisch/base64-to-text
feat(formatter): add base64 to string action
2024-01-26 10:56:54 +01:00
Rıdvan Akca
4c7d09c3d8 feat(formatter): add base64 to string action 2024-01-26 12:41:05 +03:00
Ali BARIN
48a74826e8 Merge pull request #1557 from automatisch/text-to-base64
feat(formatter): add string to base64 action
2024-01-25 16:56:15 +01:00
Rıdvan Akca
ef34068ac4 feat(formatter): add string to base64 action 2024-01-25 18:52:44 +03:00
dependabot[bot]
3987a8db77 chore(deps): Bump vite from 3.2.7 to 3.2.8
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.7 to 3.2.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.8/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 22:40:35 +00:00
106 changed files with 5159 additions and 334 deletions

View File

@@ -8,7 +8,7 @@
"version": "latest"
},
"ghcr.io/devcontainers/features/node:1": {
"version": 16
"version": 18
},
"ghcr.io/devcontainers/features/common-utils:1": {
"username": "vscode",

View File

@@ -33,7 +33,6 @@
"axios": "1.6.0",
"bcrypt": "^5.0.1",
"bullmq": "^3.0.0",
"copyfiles": "^2.4.1",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
@@ -45,7 +44,6 @@
"graphql-middleware": "^6.1.15",
"graphql-shield": "^7.5.0",
"graphql-tools": "^8.2.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.7",
"http-errors": "~1.6.3",
"http-proxy-agent": "^7.0.0",
@@ -68,7 +66,6 @@
"pluralize": "^8.0.0",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"stripe": "^11.13.0",
"winston": "^3.7.1",
"xmlrpc": "^1.3.2"
},

View File

@@ -1,5 +1,6 @@
import defineAction from '../../../../helpers/define-action.js';
import base64ToString from './transformers/base64-to-string.js';
import capitalize from './transformers/capitalize.js';
import extractEmailAddress from './transformers/extract-email-address.js';
import extractNumber from './transformers/extract-number.js';
@@ -8,10 +9,12 @@ import lowercase from './transformers/lowercase.js';
import markdownToHtml from './transformers/markdown-to-html.js';
import pluralize from './transformers/pluralize.js';
import replace from './transformers/replace.js';
import stringToBase64 from './transformers/string-to-base64.js';
import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js';
const transformers = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -20,6 +23,7 @@ const transformers = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
};
@@ -37,6 +41,7 @@ export default defineAction({
required: true,
variables: true,
options: [
{ label: 'Base64 to String', value: 'base64ToString' },
{ label: 'Capitalize', value: 'capitalize' },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
@@ -45,6 +50,7 @@ export default defineAction({
{ label: 'Lowercase', value: 'lowercase' },
{ label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
],

View File

@@ -0,0 +1,8 @@
const base64ToString = ($) => {
const input = $.step.parameters.input;
const decodedString = Buffer.from(input, 'base64').toString('utf8');
return decodedString;
};
export default base64ToString;

View File

@@ -0,0 +1,8 @@
const stringtoBase64 = ($) => {
const input = $.step.parameters.input;
const base64String = Buffer.from(input).toString('base64');
return base64String;
};
export default stringtoBase64;

View File

@@ -1,3 +1,4 @@
import base64ToString from './text/base64-to-string.js';
import capitalize from './text/capitalize.js';
import extractEmailAddress from './text/extract-email-address.js';
import extractNumber from './text/extract-number.js';
@@ -6,6 +7,7 @@ import lowercase from './text/lowercase.js';
import markdownToHtml from './text/markdown-to-html.js';
import pluralize from './text/pluralize.js';
import replace from './text/replace.js';
import stringToBase64 from './text/string-to-base64.js';
import trimWhitespace from './text/trim-whitespace.js';
import useDefaultValue from './text/use-default-value.js';
import performMathOperation from './numbers/perform-math-operation.js';
@@ -15,6 +17,7 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
import formatDateTime from './date-time/format-date-time.js';
const options = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -23,6 +26,7 @@ const options = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
performMathOperation,

View File

@@ -0,0 +1,12 @@
const base64ToString = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted from Base64 to string.',
variables: true,
},
];
export default base64ToString;

View File

@@ -0,0 +1,12 @@
const stringToBase64 = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted to Base64.',
variables: true,
},
];
export default stringToBase64;

View File

@@ -0,0 +1,29 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Delete document',
key: 'deleteDocument',
description: 'Deletes a document.',
arguments: [
{
label: 'Document ID',
key: 'documentId',
type: 'string',
required: true,
description: '',
variables: true,
},
],
async run($) {
const { documentId } = $.step.parameters;
await $.http.delete(`/v1/documents/${documentId}`);
$.setActionItem({
raw: {
result: 'successful',
},
});
},
});

View File

@@ -0,0 +1,241 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Generate document',
key: 'generateDocument',
description: 'Creates a new document.',
arguments: [
{
label: 'Workspace',
key: 'workspaceId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listWorkspaces',
},
],
},
},
{
label: 'Template',
key: 'templateId',
type: 'dropdown',
required: false,
depensOn: ['parameters.workspaceId'],
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTemplates',
},
{
name: 'parameters.workspaceId',
value: '{parameters.workspaceId}',
},
],
},
},
{
label: 'Use a Custom Json Structure?',
key: 'useCustomJsonStructure',
type: 'dropdown',
required: true,
description:
'Please indicate "yes" if you would rather create a full JSON payload instead of relying on Automatisch mapping for the Document data.',
variables: true,
options: [
{
label: 'Yes',
value: true,
},
{
label: 'No',
value: false,
},
],
additionalFields: {
type: 'query',
name: 'getDynamicFields',
arguments: [
{
name: 'key',
value: 'listDocumentData',
},
{
name: 'parameters.useCustomJsonStructure',
value: '{parameters.useCustomJsonStructure}',
},
],
},
},
{
label: 'Add Line Items?',
key: 'addLineItems',
type: 'dropdown',
required: true,
description:
'Choose "yes" to include information for Line Items (such as in an invoice).',
variables: true,
options: [
{
label: 'Yes',
value: true,
},
{
label: 'No',
value: false,
},
],
additionalFields: {
type: 'query',
name: 'getDynamicFields',
arguments: [
{
name: 'key',
value: 'listLineItems',
},
{
name: 'parameters.addLineItems',
value: '{parameters.addLineItems}',
},
],
},
},
{
label: 'Custom Filename',
key: 'customFilename',
type: 'string',
required: false,
description:
'You have the option to define a custom filename for generated documents. If left blank, a random value will be assigned.',
variables: true,
},
{
label: 'Meta Data',
key: 'metaData',
type: 'dynamic',
required: false,
description:
'Extra information appended to the generated Document but not accessible within its Template.',
fields: [
{
label: 'Key',
key: 'metaDataKey',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Value',
key: 'metaDataValue',
type: 'string',
required: false,
description: '',
variables: true,
},
],
},
],
async run($) {
const {
templateId,
useCustomJsonStructure,
customJsonPayload,
customFilename,
addLineItems,
lineItems,
documentData,
metaData,
} = $.step.parameters;
let payload = {};
let meta = {};
const documentDataObject = documentData.reduce((result, entry) => {
const key = entry.documentDataKey?.toLowerCase();
const value = entry.documentDataValue;
if (key && value) {
return {
...result,
[entry.documentDataKey?.toLowerCase()]: entry.documentDataValue,
};
}
return result;
}, {});
const lineItemsObject = lineItems.reduce((result, entry) => {
const key = entry.lineItemKey?.toLowerCase();
const value = entry.lineItemValue;
if (key && value) {
return {
...result,
[entry.lineItemKey?.toLowerCase()]: entry.lineItemValue,
};
}
return result;
}, {});
const metaDataObject = metaData.reduce((result, entry) => {
const key = entry.metaDataKey?.toLowerCase();
const value = entry.metaDataValue;
if (key && value) {
return {
...result,
[entry.metaDataKey?.toLowerCase()]: entry.metaDataValue,
};
}
return result;
}, {});
if (metaDataObject) {
meta = metaDataObject;
}
if (customFilename) {
meta._filename = customFilename;
}
if (useCustomJsonStructure) {
payload = JSON.parse(customJsonPayload);
} else {
payload = documentDataObject;
}
if (addLineItems) {
payload.lineItems = [lineItemsObject];
}
const body = {
document: {
document_template_id: templateId,
meta: JSON.stringify(meta),
payload: JSON.stringify(payload),
status: 'pending',
},
};
const { data } = await $.http.post('/v1/documents', body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -0,0 +1,4 @@
import deleteDocument from './delete-document/index.js';
import generateDocument from './generate-document/index.js';
export default [deleteDocument, generateDocument];

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,21 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
export default {
fields: [
{
key: 'apiKey',
label: 'API Key',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'PDFMonkey API secret key of your account.',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
};

View File

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

View File

@@ -0,0 +1,15 @@
import getCurrentUser from '../common/get-current-user.js';
const verifyCredentials = async ($) => {
const currentUser = await getCurrentUser($);
const screenName = [currentUser.desired_name, currentUser.email]
.filter(Boolean)
.join(' @ ');
await $.auth.set({
screenName,
apiKey: $.auth.data.apiKey,
});
};
export default verifyCredentials;

View File

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

View File

@@ -0,0 +1,8 @@
const getCurrentUser = async ($) => {
const response = await $.http.get('/v1/current_user');
const currentUser = response.data.current_user;
return currentUser;
};
export default getCurrentUser;

View File

@@ -0,0 +1,4 @@
import listTemplates from './list-templates/index.js';
import listWorkspaces from './list-workspaces/index.js';
export default [listTemplates, listWorkspaces];

View File

@@ -0,0 +1,39 @@
export default {
name: 'List templates',
key: 'listTemplates',
async run($) {
const templates = {
data: [],
};
const workspaceId = $.step.parameters.workspaceId;
let next = false;
const params = {
page: 'all',
'q[workspace_id]': workspaceId,
};
if (!workspaceId) {
return templates;
}
do {
const { data } = await $.http.get('/v1/document_template_cards', params);
next = data.meta.next_page;
if (!data?.document_template_cards?.length) {
return;
}
for (const template of data.document_template_cards) {
templates.data.push({
value: template.id,
name: template.identifier,
});
}
} while (next);
return templates;
},
};

View File

@@ -0,0 +1,29 @@
export default {
name: 'List workspaces',
key: 'listWorkspaces',
async run($) {
const workspaces = {
data: [],
};
let next = false;
do {
const { data } = await $.http.get('/v1/workspace_cards');
next = data.meta.next_page;
if (!data?.workspace_cards?.length) {
return;
}
for (const workspace of data.workspace_cards) {
workspaces.data.push({
value: workspace.id,
name: workspace.identifier,
});
}
} while (next);
return workspaces;
},
};

View File

@@ -0,0 +1,4 @@
import listDocumentData from './list-document-data/index.js';
import listLineItems from './list-line-items/index.js';
export default [listDocumentData, listLineItems];

View File

@@ -0,0 +1,48 @@
export default {
name: 'List document data',
key: 'listDocumentData',
async run($) {
if ($.step.parameters.useCustomJsonStructure) {
return [
{
label: 'Data for the Document (JSON Payload)',
key: 'customJsonPayload',
type: 'string',
required: false,
description:
'Use the JSON format { "firstname": "John", "lastname": "Doe" }.',
variables: true,
},
];
} else {
return [
{
label: 'Data for the Document',
key: 'documentData',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Key',
key: 'documentDataKey',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Value',
key: 'documentDataValue',
type: 'string',
required: false,
description: '',
variables: true,
},
],
},
];
}
},
};

View File

@@ -0,0 +1,37 @@
export default {
name: 'List line items',
key: 'listLineItems',
async run($) {
if ($.step.parameters.addLineItems) {
return [
{
label: 'Line Items',
key: 'lineItems',
type: 'dynamic',
required: false,
description:
'Data for a single item. Available as "lineItems" in your PDFMonkey Template.',
fields: [
{
label: 'Key',
key: 'lineItemKey',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Value',
key: 'lineItemValue',
type: 'string',
required: false,
description: '',
variables: true,
},
],
},
];
}
},
};

View File

@@ -0,0 +1,24 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import auth from './auth/index.js';
import triggers from './triggers/index.js';
import dynamicData from './dynamic-data/index.js';
import actions from './actions/index.js';
import dynamicFields from './dynamic-fields/index.js';
export default defineApp({
name: 'PDFMonkey',
key: 'pdf-monkey',
iconUrl: '{BASE_URL}/apps/pdf-monkey/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/pdf-monkey/connection',
supportsConnections: true,
baseUrl: 'https://pdfmonkey.io',
apiBaseUrl: 'https://api.pdfmonkey.io/api',
primaryColor: '376794',
beforeRequest: [addAuthHeader],
auth,
triggers,
dynamicData,
actions,
dynamicFields,
});

View File

@@ -0,0 +1,99 @@
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'Documents Generated',
key: 'documentsGenerated',
pollInterval: 15,
description:
'Triggers upon the successful completion of document generation.',
arguments: [
{
label: 'Workspace',
key: 'workspaceId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listWorkspaces',
},
],
},
},
{
label: 'Templates',
key: 'templateIds',
type: 'dynamic',
required: false,
description: 'Apply this trigger exclusively for particular templates.',
fields: [
{
label: 'Template',
key: 'templateId',
type: 'dropdown',
required: false,
depensOn: ['parameters.workspaceId'],
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTemplates',
},
{
name: 'parameters.workspaceId',
value: '{parameters.workspaceId}',
},
],
},
},
],
},
],
async run($) {
const workspaceId = $.step.parameters.workspaceId;
const templateIds = $.step.parameters.templateIds;
const allTemplates = templateIds
.map((templateId) => templateId.templateId)
.join(',');
const params = {
'page[size]': 100,
'q[workspace_id]': workspaceId,
'q[status]': 'success',
};
if (!templateIds.length) {
params['q[document_template_id]'] = allTemplates;
}
let next = false;
do {
const { data } = await $.http.get('/v1/document_cards', { params });
if (!data?.document_cards?.length) {
return;
}
next = data.meta.next_page;
for (const document of data.document_cards) {
$.pushTriggerItem({
raw: document,
meta: {
internalId: document.id,
},
});
}
} while (next);
},
});

View File

@@ -0,0 +1,3 @@
import documentsGenerated from './documents-generated/index.js';
export default [documentsGenerated];

View File

@@ -18,7 +18,9 @@ const port = process.env.PORT || '3000';
const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let apiUrl = new URL(`${protocol}://${host}:${port}`).toString();
let apiUrl = new URL(
process.env.API_URL || `${protocol}://${host}:${port}`
).toString();
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
// use apiUrl by default, which has less priority over the following cases
@@ -88,6 +90,10 @@ const appConfig = {
licenseKey: process.env.LICENSE_KEY,
sentryDsn: process.env.SENTRY_DSN,
CI: process.env.CI === 'true',
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
};
if (!appConfig.encryptionKey) {

View File

@@ -0,0 +1,10 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js';
export default async (request, response) => {
const samlAuthProvider = await SamlAuthProvider.query()
.findById(request.params.samlAuthProviderId)
.throwIfNotFound();
renderObject(response, samlAuthProvider);
};

View File

@@ -0,0 +1,34 @@
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 { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
import getSamlAuthProviderMock from '../../../../../../test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-provider.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => {
let samlAuthProvider, currentUser, token;
beforeEach(async () => {
const role = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: role.id });
samlAuthProvider = await createSamlAuthProvider();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return saml auth provider with specified id', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get(`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,11 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js';
export default async (request, response) => {
const samlAuthProviders = await SamlAuthProvider.query().orderBy(
'created_at',
'desc'
);
renderObject(response, samlAuthProviders);
};

View File

@@ -0,0 +1,39 @@
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 { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
import getSamlAuthProvidersMock from '../../../../../../test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/saml-auth-providers', () => {
let samlAuthProviderOne, samlAuthProviderTwo, currentUser, token;
beforeEach(async () => {
const role = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: role.id });
samlAuthProviderOne = await createSamlAuthProvider();
samlAuthProviderTwo = await createSamlAuthProvider();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return saml auth providers', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get('/api/v1/admin/saml-auth-providers')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getSamlAuthProvidersMock([
samlAuthProviderTwo,
samlAuthProviderOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,6 @@
import appConfig from '../../../../config/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, { version: appConfig.version });
};

View File

@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
describe('GET /api/v1/automatisch/version', () => {
it('should return Automatisch version', async () => {
const response = await request(app)
.get('/api/v1/automatisch/version')
.expect(200);
const expectedPayload = {
data: {
version: '0.10.0',
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'Object',
},
};
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,5 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, request.currentUser);
};

View File

@@ -0,0 +1,26 @@
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 { createUser } from '../../../../../test/factories/user';
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
describe('GET /api/v1/users/me', () => {
let role, currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
role = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return current user info', async () => {
const response = await request(app)
.get('/api/v1/users/me')
.set('Authorization', token)
.expect(200);
const expectedPayload = getCurrentUserMock(currentUser, role);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,12 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const inTrial = await request.currentUser.inTrial();
const trialInfo = {
inTrial,
expireAt: request.currentUser.trialExpiryDate,
};
renderObject(response, trialInfo);
};

View File

@@ -0,0 +1,38 @@
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 getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial.js';
import appConfig from '../../../../config/app.js';
import { DateTime } from 'luxon';
import User from '../../../../models/user.js';
describe('GET /api/v1/users/:userId/trial', () => {
let user, token;
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
token = createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});
describe('should return in trial, active subscription and expire at info', () => {
beforeEach(async () => {
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(true);
});
it('should return null', async () => {
const response = await request(app)
.get(`/api/v1/users/${user.id}/trial`)
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUserTrialMock(user);
expect(response.body).toEqual(expectedResponsePayload);
});
});
});

View File

@@ -0,0 +1,16 @@
import { renderObject } from '../../../../helpers/renderer.js';
import User from '../../../../models/user.js';
export default async (request, response) => {
const user = await User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.findById(request.params.userId)
.throwIfNotFound();
renderObject(response, user);
};

View File

@@ -0,0 +1,36 @@
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 { createUser } from '../../../../../test/factories/user';
import { createPermission } from '../../../../../test/factories/permission';
import getUserMock from '../../../../../test/mocks/rest/api/v1/users/get-user';
describe('GET /api/v1/users/:userId', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
anotherUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
anotherUserRole = await anotherUser.$relatedQuery('role');
await createPermission({
roleId: currentUserRole.id,
action: 'read',
subject: 'User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified user info', async () => {
const response = await request(app)
.get(`/api/v1/users/${anotherUser.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,18 @@
import { renderObject } from '../../../../helpers/renderer.js';
import User from '../../../../models/user.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const usersQuery = User.query()
.leftJoinRelated({
role: true,
})
.withGraphFetched({
role: true,
})
.orderBy('full_name', 'asc');
const users = await paginateRest(usersQuery, request.query.page);
renderObject(response, users);
};

View File

@@ -0,0 +1,56 @@
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';
import getUsersMock from '../../../../../test/mocks/rest/api/v1/users/get-users';
describe('GET /api/v1/users', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole({
key: 'currentUser',
name: 'Current user role',
});
await createPermission({
action: 'read',
subject: 'User',
roleId: currentUserRole.id,
});
currentUser = await createUser({
roleId: currentUserRole.id,
fullName: 'Current User',
});
anotherUserRole = await createRole({
key: 'anotherUser',
name: 'Another user role',
});
anotherUser = await createUser({
roleId: anotherUserRole.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return users data', async () => {
const response = await request(app)
.get('/api/v1/users')
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUsersMock(
[anotherUser, currentUser],
[anotherUserRole, currentUserRole]
);
expect(response.body).toEqual(expectedResponsePayload);
});
});

View File

@@ -0,0 +1,3 @@
export default async (request, response) => {
response.status(200).end();
};

View File

@@ -0,0 +1,9 @@
import { describe, it } from 'vitest';
import request from 'supertest';
import app from '../../app.js';
describe('GET /healthcheck', () => {
it('should return 200 response with version data', async () => {
await request(app).get('/healthcheck').expect(200);
});
});

View File

@@ -1,7 +1,10 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js';
import Role from '../../models/role.js';
const registerUser = async (_parent, params) => {
if (!appConfig.isCloud) return;
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({

View File

@@ -1,9 +1,17 @@
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)) {
@@ -18,7 +26,7 @@ const getConfig = async (_parent, params) => {
computedConfig[key] = value?.data;
return computedConfig;
}, {});
}, defaultConfig);
};
export default getConfig;

View File

@@ -2,6 +2,7 @@ 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', () => {
@@ -56,6 +57,10 @@ describe('graphQL getConfig query', () => {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
@@ -82,6 +87,48 @@ describe('graphQL getConfig query', () => {
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',
},
},
};

View File

@@ -20,7 +20,8 @@ export const isAuthenticated = async (_parent, _args, req) => {
.withGraphFetched({
role: true,
permissions: true,
});
})
.throwIfNotFound();
return true;
} catch (error) {
@@ -28,6 +29,14 @@ export const isAuthenticated = async (_parent, _args, req) => {
}
};
export const authenticateUser = async (request, response, next) => {
if (await isAuthenticated(null, null, request)) {
next();
} else {
return response.status(401).end();
}
};
const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {

View File

@@ -1,11 +1,8 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { allow } from 'graphql-shield';
import jwt from 'jsonwebtoken';
import User from '../models/user.js';
import { isAuthenticated, authenticationRules } from './authentication.js';
vi.mock('jsonwebtoken');
vi.mock('../models/user.js');
import { createUser } from '../../test/factories/user.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
describe('isAuthenticated', () => {
it('should return false if no token is provided', async () => {
@@ -14,29 +11,26 @@ describe('isAuthenticated', () => {
});
it('should return false if token is invalid', async () => {
jwt.verify.mockImplementation(() => {
throw new Error('invalid token');
});
const req = { headers: { authorization: 'invalidToken' } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return true if token is valid', async () => {
jwt.verify.mockReturnValue({ userId: '123' });
it('should return true if token is valid and there is a user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
User.query.mockReturnValue({
findById: vi.fn().mockReturnValue({
leftJoinRelated: vi.fn().mockReturnThis(),
withGraphFetched: vi
.fn()
.mockResolvedValue({ id: '123', role: {}, permissions: {} }),
}),
});
const req = { headers: { authorization: 'validToken' } };
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(true);
});
it('should return false if token is valid and but there is no user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
await user.$query().delete();
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
});
describe('authentication rules', () => {

View File

@@ -0,0 +1,33 @@
const authorizationList = {
'GET /api/v1/users/:userId': {
action: 'read',
subject: 'User',
},
'GET /api/v1/users/': {
action: 'read',
subject: 'User',
},
};
export const authorizeUser = async (request, response, next) => {
const currentRoute =
request.method + ' ' + request.baseUrl + request.route.path;
const currentRouteRule = authorizationList[currentRoute];
try {
request.currentUser.can(currentRouteRule.action, currentRouteRule.subject);
next();
} catch (error) {
return response.status(403).end();
}
};
export const authorizeAdmin = async (request, response, next) => {
const role = await request.currentUser.$relatedQuery('role');
if (role?.isAdmin) {
next();
} else {
return response.status(403).end();
}
};

View File

@@ -0,0 +1,11 @@
import appConfig from '../config/app.js';
export const checkIsCloud = async (request, response, next) => {
if (appConfig.isCloud) {
next();
} else {
return response.status(404).end();
}
};
export default checkIsCloud;

View File

@@ -0,0 +1,9 @@
import { hasValidLicense } from './license.ee.js';
export const checkIsEnterprise = async (request, response, next) => {
if (await hasValidLicense()) {
next();
} else {
return response.status(404).end();
}
};

View File

@@ -1,6 +1,9 @@
import * as path from 'path';
import * as fs from 'fs';
import * as handlebars from 'handlebars';
import path from 'path';
import fs from 'fs';
import handlebars from 'handlebars';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compileEmail = (emailPath, replacements = {}) => {
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);

View File

@@ -4,8 +4,8 @@ import appConfig from '../config/app.js';
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
http: 2,
info: 3,
debug: 4,
};

View File

@@ -0,0 +1,25 @@
const paginateRest = async (query, page) => {
const pageSize = 10;
page = parseInt(page, 10);
if (isNaN(page) || page < 1) {
page = 1;
}
const [records, count] = await Promise.all([
query.limit(pageSize).offset((page - 1) * pageSize),
query.resultSize(),
]);
return {
pageInfo: {
currentPage: page,
totalPages: Math.ceil(count / pageSize),
},
totalCount: count,
records,
};
};
export default paginateRest;

View File

@@ -0,0 +1,44 @@
import serializers from '../serializers/index.js';
const isPaginated = (object) =>
object?.pageInfo &&
object?.totalCount !== undefined &&
Array.isArray(object?.records);
const isArray = (object) =>
Array.isArray(object) || Array.isArray(object?.records);
const totalCount = (object) =>
isPaginated(object) ? object.totalCount : isArray(object) ? object.length : 1;
const renderObject = (response, object) => {
let data = isPaginated(object) ? object.records : object;
const type = isPaginated(object)
? object.records[0].constructor.name
: Array.isArray(object)
? object[0].constructor.name
: object.constructor.name;
const serializer = serializers[type];
if (serializer) {
data = Array.isArray(data)
? data.map((item) => serializer(item))
: serializer(data);
}
const computedPayload = {
data,
meta: {
type,
count: totalCount(object),
isArray: isArray(object),
currentPage: isPaginated(object) ? object.pageInfo.currentPage : null,
totalPages: isPaginated(object) ? object.pageInfo.totalPages : null,
},
};
return response.json(computedPayload);
};
export { renderObject };

View File

@@ -15,7 +15,7 @@ const webUIHandler = async (app) => {
app.use(express.static(webBuildPath));
app.get('*', (_req, res) => {
res.set('Content-Security-Policy', 'frame-ancestors: none;');
res.set('Content-Security-Policy', 'frame-ancestors \'none\';');
res.set('X-Frame-Options', 'DENY');
res.sendFile(indexHtml);

View File

@@ -0,0 +1,8 @@
import { Router } from 'express';
import versionAction from '../../../controllers/api/v1/automatisch/version.js';
const router = Router();
router.get('/version', versionAction);
export default router;

View File

@@ -0,0 +1,26 @@
import { Router } from 'express';
import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeAdmin } from '../../../helpers/authorization.js';
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
import getSamlAuthProvidersAction from '../../../controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js';
import getSamlAuthProviderAction from '../../../controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js';
const router = Router();
router.get(
'/',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
getSamlAuthProvidersAction
);
router.get(
'/:samlAuthProviderId',
authenticateUser,
authorizeAdmin,
checkIsEnterprise,
getSamlAuthProviderAction
);
export default router;

View File

@@ -0,0 +1,22 @@
import { Router } from 'express';
import { authenticateUser } from '../../../helpers/authentication.js';
import { authorizeUser } from '../../../helpers/authorization.js';
import checkIsCloud from '../../../helpers/check-is-cloud.js';
import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js';
import getUserAction from '../../../controllers/api/v1/users/get-user.js';
import getUsersAction from '../../../controllers/api/v1/users/get-users.js';
import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js';
const router = Router();
router.get('/', authenticateUser, authorizeUser, getUsersAction);
router.get('/me', authenticateUser, getCurrentUserAction);
router.get('/:userId', authenticateUser, authorizeUser, getUserAction);
router.get(
'/:userId/trial',
authenticateUser,
checkIsCloud,
getUserTrialAction
);
export default router;

View File

@@ -0,0 +1,8 @@
import { Router } from 'express';
import indexAction from '../controllers/healthcheck/index.js';
const router = Router();
router.get('/', indexAction);
export default router;

View File

@@ -2,11 +2,19 @@ import { Router } from 'express';
import graphQLInstance from '../helpers/graphql-instance.js';
import webhooksRouter from './webhooks.js';
import paddleRouter from './paddle.ee.js';
import healthcheckRouter from './healthcheck.js';
import automatischRouter from './api/v1/automatisch.js';
import usersRouter from './api/v1/users.js';
import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js';
const router = Router();
router.use('/graphql', graphQLInstance);
router.use('/webhooks', webhooksRouter);
router.use('/paddle', paddleRouter);
router.use('/healthcheck', healthcheckRouter);
router.use('/api/v1/automatisch', automatischRouter);
router.use('/api/v1/users', usersRouter);
router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter);
export default router;

View File

@@ -0,0 +1,13 @@
import userSerializer from './user.js';
import roleSerializer from './role.js';
import permissionSerializer from './permission.js';
import samlAuthProviderSerializer from './saml-auth-provider.ee.js';
const serializers = {
User: userSerializer,
Role: roleSerializer,
Permission: permissionSerializer,
SamlAuthProvider: samlAuthProviderSerializer,
};
export default serializers;

View File

@@ -0,0 +1,13 @@
const permissionSerializer = (permission) => {
return {
id: permission.id,
roleId: permission.roleId,
action: permission.action,
subject: permission.subject,
conditions: permission.conditions,
createdAt: permission.createdAt,
updatedAt: permission.updatedAt,
};
};
export default permissionSerializer;

View File

@@ -0,0 +1,25 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createPermission } from '../../test/factories/permission';
import permissionSerializer from './permission';
describe('permissionSerializer', () => {
let permission;
beforeEach(async () => {
permission = await createPermission();
});
it('should return permission data', async () => {
const expectedPayload = {
id: permission.id,
roleId: permission.roleId,
action: permission.action,
subject: permission.subject,
conditions: permission.conditions,
createdAt: permission.createdAt,
updatedAt: permission.updatedAt,
};
expect(permissionSerializer(permission)).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,13 @@
const roleSerializer = (role) => {
return {
id: role.id,
name: role.name,
key: role.key,
description: role.description,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isAdmin: role.isAdmin,
};
};
export default roleSerializer;

View File

@@ -0,0 +1,25 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createRole } from '../../test/factories/role';
import roleSerializer from './role';
describe('roleSerializer', () => {
let role;
beforeEach(async () => {
role = await createRole();
});
it('should return role data', async () => {
const expectedPayload = {
id: role.id,
name: role.name,
key: role.key,
description: role.description,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isAdmin: role.isAdmin,
};
expect(roleSerializer(role)).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,18 @@
const samlAuthProviderSerializer = (samlAuthProvider) => {
return {
id: samlAuthProvider.id,
name: samlAuthProvider.name,
certificate: samlAuthProvider.certificate,
signatureAlgorithm: samlAuthProvider.signatureAlgorithm,
issuer: samlAuthProvider.issuer,
entryPoint: samlAuthProvider.entryPoint,
firstnameAttributeName: samlAuthProvider.firstnameAttributeName,
surnameAttributeName: samlAuthProvider.surnameAttributeName,
emailAttributeName: samlAuthProvider.emailAttributeName,
roleAttributeName: samlAuthProvider.roleAttributeName,
active: samlAuthProvider.active,
defaultRoleId: samlAuthProvider.defaultRoleId,
};
};
export default samlAuthProviderSerializer;

View File

@@ -0,0 +1,32 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
import samlAuthProviderSerializer from './saml-auth-provider.ee.js';
describe('samlAuthProviderSerializer', () => {
let samlAuthProvider;
beforeEach(async () => {
samlAuthProvider = await createSamlAuthProvider();
});
it('should return saml auth provider data', async () => {
const expectedPayload = {
id: samlAuthProvider.id,
name: samlAuthProvider.name,
certificate: samlAuthProvider.certificate,
signatureAlgorithm: samlAuthProvider.signatureAlgorithm,
issuer: samlAuthProvider.issuer,
entryPoint: samlAuthProvider.entryPoint,
firstnameAttributeName: samlAuthProvider.firstnameAttributeName,
surnameAttributeName: samlAuthProvider.surnameAttributeName,
emailAttributeName: samlAuthProvider.emailAttributeName,
roleAttributeName: samlAuthProvider.roleAttributeName,
active: samlAuthProvider.active,
defaultRoleId: samlAuthProvider.defaultRoleId,
};
expect(samlAuthProviderSerializer(samlAuthProvider)).toEqual(
expectedPayload
);
});
});

View File

@@ -0,0 +1,32 @@
import roleSerializer from './role.js';
import permissionSerializer from './permission.js';
import appConfig from '../config/app.js';
const userSerializer = (user) => {
let userData = {
id: user.id,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
fullName: user.fullName,
roleId: user.roleId,
};
if (user.role) {
userData.role = roleSerializer(user.role);
}
if (user.permissions) {
userData.permissions = user.permissions.map((permission) =>
permissionSerializer(permission)
);
}
if (appConfig.isCloud && user.trialExpiryDate) {
userData.trialExpiryDate = user.trialExpiryDate;
}
return userData;
};
export default userSerializer;

View File

@@ -0,0 +1,76 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { DateTime } from 'luxon';
import appConfig from '../config/app';
import { createUser } from '../../test/factories/user';
import { createPermission } from '../../test/factories/permission';
import userSerializer from './user';
describe('userSerializer', () => {
let user, role, permissionOne, permissionTwo;
beforeEach(async () => {
user = await createUser();
role = await user.$relatedQuery('role');
permissionOne = await createPermission({
roleId: role.id,
action: 'read',
subject: 'User',
});
permissionTwo = await createPermission({
roleId: role.id,
action: 'read',
subject: 'Role',
});
});
it('should return user data', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
const expectedPayload = {
createdAt: user.createdAt,
email: user.email,
fullName: user.fullName,
id: user.id,
roleId: user.roleId,
updatedAt: user.updatedAt,
};
expect(userSerializer(user)).toEqual(expectedPayload);
});
it('should return user data with the role', async () => {
user.role = role;
const expectedPayload = {
role,
};
expect(userSerializer(user)).toMatchObject(expectedPayload);
});
it('should return user data with the permissions', async () => {
user.permissions = [permissionOne, permissionTwo];
const expectedPayload = {
permissions: [permissionOne, permissionTwo],
};
expect(userSerializer(user)).toMatchObject(expectedPayload);
});
it('should return user data with trial expiry date', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
await user.$query().patch({
trialExpiryDate: DateTime.now().plus({ days: 30 }).toISODate(),
});
const expectedPayload = {
trialExpiryDate: user.trialExpiryDate,
};
expect(userSerializer(user)).toMatchObject(expectedPayload);
});
});

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import Config from '../../src/models/config';
export const createConfig = async (params = {}) => {
const configData = {
@@ -6,10 +7,7 @@ export const createConfig = async (params = {}) => {
value: params?.value || { data: 'sampleConfig' },
};
const [config] = await global.knex
.table('config')
.insert(configData)
.returning('*');
const config = await Config.query().insert(configData).returning('*');
return config;
};

View File

@@ -1,5 +1,6 @@
import appConfig from '../../src/config/app';
import { AES } from 'crypto-js';
import Connection from '../../src/models/connection';
export const createConnection = async (params = {}) => {
params.key = params?.key || 'deepl';
@@ -16,10 +17,7 @@ export const createConnection = async (params = {}) => {
appConfig.encryptionKey
).toString();
const [connection] = await global.knex
.table('connections')
.insert(params)
.returning('*');
const connection = await Connection.query().insert(params).returning('*');
return connection;
};

View File

@@ -1,3 +1,4 @@
import ExecutionStep from '../../src/models/execution-step';
import { createExecution } from './execution';
import { createStep } from './step';
@@ -8,8 +9,7 @@ export const createExecutionStep = async (params = {}) => {
params.dataIn = params?.dataIn || { dataIn: 'dataIn' };
params.dataOut = params?.dataOut || { dataOut: 'dataOut' };
const [executionStep] = await global.knex
.table('executionSteps')
const executionStep = await ExecutionStep.query()
.insert(params)
.returning('*');

View File

@@ -1,3 +1,4 @@
import Execution from '../../src/models/execution';
import { createFlow } from './flow';
export const createExecution = async (params = {}) => {
@@ -6,10 +7,7 @@ export const createExecution = async (params = {}) => {
params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString();
const [execution] = await global.knex
.table('executions')
.insert(params)
.returning('*');
const execution = await Execution.query().insert(params).returning('*');
return execution;
};

View File

@@ -1,3 +1,4 @@
import Flow from '../../src/models/flow';
import { createUser } from './user';
export const createFlow = async (params = {}) => {
@@ -6,7 +7,7 @@ export const createFlow = async (params = {}) => {
params.createdAt = params?.createdAt || new Date().toISOString();
params.updatedAt = params?.updatedAt || new Date().toISOString();
const [flow] = await global.knex.table('flows').insert(params).returning('*');
const flow = await Flow.query().insert(params).returning('*');
return flow;
};

View File

@@ -1,3 +1,4 @@
import Permission from '../../src/models/permission';
import { createRole } from './role';
export const createPermission = async (params = {}) => {
@@ -6,10 +7,7 @@ export const createPermission = async (params = {}) => {
params.subject = params?.subject || 'User';
params.conditions = params?.conditions || ['isCreator'];
const [permission] = await global.knex
.table('permissions')
.insert(params)
.returning('*');
const permission = await Permission.query().insert(params).returning('*');
return permission;
};

View File

@@ -1,8 +1,10 @@
import Role from '../../src/models/role';
export const createRole = async (params = {}) => {
params.name = params?.name || 'Viewer';
params.key = params?.key || 'viewer';
const [role] = await global.knex.table('roles').insert(params).returning('*');
const role = await Role.query().insert(params).returning('*');
return role;
};

View File

@@ -0,0 +1,33 @@
import { createRole } from './role';
import SamlAuthProvider from '../../src/models/saml-auth-provider.ee.js';
export const createSamlAuthProvider = async (params = {}) => {
params.name = params?.name || 'Keycloak SAML';
params.certificate = params?.certificate || 'certificate';
params.signatureAlgorithm = params?.signatureAlgorithm || 'sha512';
params.entryPoint =
params?.entryPoint ||
'https://example.com/auth/realms/automatisch/protocol/saml';
params.issuer = params?.issuer || 'automatisch-client';
params.firstnameAttributeName =
params?.firstnameAttributeName || 'urn:oid:2.1.1.42';
params.surnameAttributeName =
params?.surnameAttributeName || 'urn:oid:2.1.1.4';
params.emailAttributeName =
params?.emailAttributeName || 'urn:oid:1.1.2342.19200300.100.1.1';
params.roleAttributeName = params?.roleAttributeName || 'Role';
params.defaultRoleId = params?.defaultRoleId || (await createRole()).id;
params.active = params?.active || true;
const samlAuthProvider = await SamlAuthProvider.query()
.insert(params)
.returning('*');
return samlAuthProvider;
};

View File

@@ -1,3 +1,4 @@
import Step from '../../src/models/step';
import { createFlow } from './flow';
export const createStep = async (params = {}) => {
@@ -16,7 +17,7 @@ export const createStep = async (params = {}) => {
params.appKey =
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
const [step] = await global.knex.table('steps').insert(params).returning('*');
const step = await Step.query().insert(params).returning('*');
return step;
};

View File

@@ -1,5 +1,6 @@
import { createRole } from './role';
import { faker } from '@faker-js/faker';
import User from '../../src/models/user';
export const createUser = async (params = {}) => {
params.roleId = params?.roleId || (await createRole()).id;
@@ -7,7 +8,7 @@ export const createUser = async (params = {}) => {
params.email = params?.email || faker.internet.email();
params.password = params?.password || faker.internet.password();
const [user] = await global.knex.table('users').insert(params).returning('*');
const user = await User.query().insert(params).returning('*');
return user;
};

View File

@@ -0,0 +1,29 @@
const getSamlAuthProvidersMock = async (samlAuthProvider) => {
const data = {
active: samlAuthProvider.active,
certificate: samlAuthProvider.certificate,
defaultRoleId: samlAuthProvider.defaultRoleId,
emailAttributeName: samlAuthProvider.emailAttributeName,
entryPoint: samlAuthProvider.entryPoint,
firstnameAttributeName: samlAuthProvider.firstnameAttributeName,
id: samlAuthProvider.id,
issuer: samlAuthProvider.issuer,
name: samlAuthProvider.name,
roleAttributeName: samlAuthProvider.roleAttributeName,
signatureAlgorithm: samlAuthProvider.signatureAlgorithm,
surnameAttributeName: samlAuthProvider.surnameAttributeName,
};
return {
data: data,
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'SamlAuthProvider',
},
};
};
export default getSamlAuthProvidersMock;

View File

@@ -0,0 +1,31 @@
const getSamlAuthProvidersMock = async (samlAuthProviders) => {
const data = samlAuthProviders.map((samlAuthProvider) => {
return {
active: samlAuthProvider.active,
certificate: samlAuthProvider.certificate,
defaultRoleId: samlAuthProvider.defaultRoleId,
emailAttributeName: samlAuthProvider.emailAttributeName,
entryPoint: samlAuthProvider.entryPoint,
firstnameAttributeName: samlAuthProvider.firstnameAttributeName,
id: samlAuthProvider.id,
issuer: samlAuthProvider.issuer,
name: samlAuthProvider.name,
roleAttributeName: samlAuthProvider.roleAttributeName,
signatureAlgorithm: samlAuthProvider.signatureAlgorithm,
surnameAttributeName: samlAuthProvider.surnameAttributeName,
};
});
return {
data: data,
meta: {
count: data.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'SamlAuthProvider',
},
};
};
export default getSamlAuthProvidersMock;

View File

@@ -0,0 +1,32 @@
const getCurrentUserMock = (currentUser, role) => {
return {
data: {
createdAt: currentUser.createdAt.toISOString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
permissions: [],
role: {
createdAt: role.createdAt.toISOString(),
description: null,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
},
roleId: role.id,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'User',
},
};
};
export default getCurrentUserMock;

View File

@@ -0,0 +1,17 @@
const getUserTrialMock = async (currentUser) => {
return {
data: {
inTrial: await currentUser.inTrial(),
expireAt: currentUser.trialExpiryDate.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'Object',
},
};
};
export default getUserTrialMock;

View File

@@ -0,0 +1,31 @@
const getUserMock = (currentUser, role) => {
return {
data: {
createdAt: currentUser.createdAt.toISOString(),
email: currentUser.email,
fullName: currentUser.fullName,
id: currentUser.id,
role: {
createdAt: role.createdAt.toISOString(),
description: null,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
},
roleId: role.id,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.toISOString(),
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'User',
},
};
};
export default getUserMock;

View File

@@ -0,0 +1,38 @@
const getUsersMock = async (users, roles) => {
const data = users.map((user) => {
const role = roles.find((r) => r.id === user.roleId);
return {
createdAt: user.createdAt.toISOString(),
email: user.email,
fullName: user.fullName,
id: user.id,
role: role
? {
createdAt: role.createdAt.toISOString(),
description: role.description,
id: role.id,
isAdmin: role.isAdmin,
key: role.key,
name: role.name,
updatedAt: role.updatedAt.toISOString(),
}
: null, // Fallback to null if role not found
roleId: user.roleId,
trialExpiryDate: user.trialExpiryDate.toISOString(),
updatedAt: user.updatedAt.toISOString(),
};
});
return {
data: data,
meta: {
count: data.length,
currentPage: 1,
isArray: true,
totalPages: 1,
type: 'User',
},
};
};
export default getUsersMock;

View File

@@ -252,6 +252,16 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/openai/connection' },
],
},
{
text: 'PDFMonkey',
collapsible: true,
collapsed: true,
items: [
{ text: 'Triggers', link: '/apps/pdf-monkey/triggers' },
{ text: 'Actions', link: '/apps/pdf-monkey/actions' },
{ text: 'Connection', link: '/apps/pdf-monkey/connection' },
],
},
{
text: 'Pipedrive',
collapsible: true,
@@ -305,7 +315,7 @@ export default defineConfig({
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/removebg/actions' },
{ text: 'Connection', link: '/apps/removebg/connection' }
{ text: 'Connection', link: '/apps/removebg/connection' },
],
},
{

View File

@@ -14,31 +14,33 @@ The default values for some environment variables might be different in our deve
Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work.
:::
| Variable Name | Type | Default Value | Description |
| --------------------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
| `HOST` | string | `localhost` | HTTP Host |
| `PROTOCOL` | string | `http` | HTTP Protocol |
| `PORT` | string | `3000` | HTTP Port |
| `APP_ENV` | string | `production` | Automatisch Environment |
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` |
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
| `POSTGRES_PORT` | number | `5432` | Database Port |
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
| `POSTGRES_HOST` | string | `postgres` | Database Host |
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
| `POSTGRES_PASSWORD` | string | | Password of Database User |
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
| `REDIS_HOST` | string | `redis` | Redis Host |
| `REDIS_PORT` | number | `6379` | Redis Port |
| `REDIS_USERNAME` | string | | Redis Username |
| `REDIS_PASSWORD` | string | | Redis Password |
| `REDIS_TLS` | boolean | `false` | Redis TLS |
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
| Variable Name | Type | Default Value | Description |
| ---------------------------- | ------- | ------------------ | ----------------------------------------------------------------------------------- |
| `HOST` | string | `localhost` | HTTP Host |
| `PROTOCOL` | string | `http` | HTTP Protocol |
| `PORT` | string | `3000` | HTTP Port |
| `APP_ENV` | string | `production` | Automatisch Environment |
| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL |
| `WEBHOOK_URL` | string | | Can be used to override webhook URL |
| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` |
| `POSTGRES_DATABASE` | string | `automatisch` | Database Name |
| `POSTGRES_SCHEMA` | string | `public` | Database Schema |
| `POSTGRES_PORT` | number | `5432` | Database Port |
| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database |
| `POSTGRES_HOST` | string | `postgres` | Database Host |
| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User |
| `POSTGRES_PASSWORD` | string | | Password of Database User |
| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials |
| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests |
| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user |
| `REDIS_HOST` | string | `redis` | Redis Host |
| `REDIS_PORT` | number | `6379` | Redis Port |
| `REDIS_USERNAME` | string | | Redis Username |
| `REDIS_PASSWORD` | string | | Redis Password |
| `REDIS_TLS` | boolean | `false` | Redis TLS |
| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry |
| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard |
| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard |
| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard |
| `DISABLE_NOTIFICATIONS_PAGE` | boolean | `false` | Enable/Disable notifications page |
| `DISABLE_FAVICON` | boolean | `false` | Enable/Disable favicon |

View File

@@ -0,0 +1,14 @@
---
favicon: /favicons/pdf-monkey.svg
items:
- name: Generate documents
desc: Creates a new document.
- name: Delete documents
desc: Deletes a document.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -0,0 +1,11 @@
# PDFMonkey
:::info
This page explains the steps you need to follow to set up the PDFMonkey
connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Login to your PDFMonkey account: [https://dashboard.pdfmonkey.io/login](https://dashboard.pdfmonkey.io/login).
2. Go to **My Account** section from your profile.
3. Copy `API SECRET KEY` from the page to the `API Key` field on Automatisch.
4. Now, you can start using the PDFMonkey connection with Automatisch.

View File

@@ -0,0 +1,12 @@
---
favicon: /favicons/pdf-monkey.svg
items:
- name: Documents Generated
desc: Triggers upon the successful completion of document generation.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -26,6 +26,7 @@ The following integrations are currently supported by Automatisch.
- [Ntfy](/apps/ntfy/actions)
- [Odoo](/apps/odoo/actions)
- [OpenAI](/apps/openai/actions)
- [PDFMonkey](/apps/pdf-monkey/actions)
- [Pipedrive](/apps/pipedrive/triggers)
- [Placetel](/apps/placetel/triggers)
- [PostgreSQL](/apps/postgresql/actions)

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0059F7" />
<meta

View File

@@ -2,13 +2,6 @@
"short_name": "automatisch",
"name": "automatisch",
"description": "Build workflow automation without spending time and money. No code is required.",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",

View File

@@ -15,6 +15,7 @@ import { SvgIconComponent } from '@mui/icons-material';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type SettingsLayoutProps = {
@@ -86,19 +87,11 @@ function createDrawerLinks({
return items;
}
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: 'adminSettingsDrawer.goBack',
to: '/',
dataTest: 'go-back-drawer-link',
},
];
export default function SettingsLayout({
children,
}: SettingsLayoutProps): React.ReactElement {
const theme = useTheme();
const formatMessage = useFormatMessage();
const currentUserAbility = useCurrentUserAbility();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -116,6 +109,15 @@ export default function SettingsLayout({
canUpdateApp: currentUserAbility.can('update', 'App'),
});
const drawerBottomLinks = [
{
Icon: ArrowBackIosNewIcon,
primary: formatMessage('adminSettingsDrawer.goBack'),
to: '/',
dataTest: 'go-back-drawer-link',
},
];
return (
<>
<AppBar

View File

@@ -19,6 +19,7 @@ type DrawerLink = {
Icon: React.ElementType;
primary: string;
to: string;
target?: '_blank';
badgeContent?: React.ReactNode;
dataTest?: string;
};
@@ -69,7 +70,7 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<List sx={{ py: 0, mt: 3 }}>
{bottomLinks.map(
({ Icon, badgeContent, primary, to, dataTest }, index) => (
({ Icon, badgeContent, primary, to, dataTest, target }, index) => (
<ListItemLink
key={`${to}-${index}`}
icon={
@@ -77,9 +78,10 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
<Icon htmlColor={theme.palette.primary.main} />
</Badge>
}
primary={formatMessage(primary)}
primary={primary}
to={to}
onClick={closeOnClick}
target={target}
data-test={dataTest}
/>
)

View File

@@ -3,16 +3,19 @@ import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import Stack from '@mui/material/Stack';
import AppsIcon from '@mui/icons-material/Apps';
import SwapCallsIcon from '@mui/icons-material/SwapCalls';
import HistoryIcon from '@mui/icons-material/History';
import NotificationsIcon from '@mui/icons-material/Notifications';
import ArrowBackIosNew from '@mui/icons-material/ArrowBackIosNew';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import useVersion from 'hooks/useVersion';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import useConfig from 'hooks/useConfig';
type PublicLayoutProps = {
children: React.ReactNode;
@@ -40,47 +43,94 @@ const drawerLinks = [
];
type GenerateDrawerBottomLinksOptions = {
isMation: boolean;
loading: boolean;
disableNotificationsPage: boolean;
notificationBadgeContent: number;
additionalDrawerLink?: string;
additionalDrawerLinkText?: string;
additionalDrawerLinkIcon?: string;
formatMessage: ReturnType<typeof useFormatMessage>;
};
const generateDrawerBottomLinks = ({
isMation,
loading,
const generateDrawerBottomLinks = async ({
disableNotificationsPage,
notificationBadgeContent = 0,
additionalDrawerLink,
additionalDrawerLinkText,
formatMessage,
}: GenerateDrawerBottomLinksOptions) => {
if (loading || isMation) {
return [];
const notificationsPageLinkObject = {
Icon: NotificationsIcon,
primary: formatMessage('settingsDrawer.notifications'),
to: URLS.UPDATES,
badgeContent: notificationBadgeContent,
};
const hasAdditionalDrawerLink =
additionalDrawerLink && additionalDrawerLinkText;
const additionalDrawerLinkObject = {
Icon: ArrowBackIosNew,
primary: additionalDrawerLinkText || '',
to: additionalDrawerLink || '',
target: '_blank' as const,
};
const links = [];
if (!disableNotificationsPage) {
links.push(notificationsPageLinkObject);
}
return [
{
Icon: NotificationsIcon,
primary: 'settingsDrawer.notifications',
to: URLS.UPDATES,
badgeContent: notificationBadgeContent,
},
];
if (hasAdditionalDrawerLink) {
links.push(additionalDrawerLinkObject);
}
return links;
};
type Link = {
Icon: React.ElementType;
primary: string;
target?: '_blank';
to: string;
badgeContent?: React.ReactNode;
};
export default function PublicLayout({
children,
}: PublicLayoutProps): React.ReactElement {
const version = useVersion();
const { isMation, loading } = useAutomatischInfo();
const { config, loading } = useConfig([
'disableNotificationsPage',
'additionalDrawerLink',
'additionalDrawerLinkText',
]);
const theme = useTheme();
const formatMessage = useFormatMessage();
const [bottomLinks, setBottomLinks] = React.useState<Link[]>([]);
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
const openDrawer = () => setDrawerOpen(true);
const closeDrawer = () => setDrawerOpen(false);
const drawerBottomLinks = generateDrawerBottomLinks({
notificationBadgeContent: version.newVersionCount,
loading,
isMation,
});
React.useEffect(() => {
async function perform() {
const newBottomLinks = await generateDrawerBottomLinks({
notificationBadgeContent: version.newVersionCount,
disableNotificationsPage: config?.disableNotificationsPage as boolean,
additionalDrawerLink: config?.additionalDrawerLink as string,
additionalDrawerLinkText: config?.additionalDrawerLinkText as string,
formatMessage,
});
setBottomLinks(newBottomLinks);
}
if (loading) return;
perform();
}, [config, loading, version.newVersionCount]);
return (
<>
@@ -90,20 +140,19 @@ export default function PublicLayout({
onDrawerClose={closeDrawer}
/>
<Box sx={{ display: 'flex' }}>
<Box sx={{ display: 'flex', height: '100%' }}>
<Drawer
links={drawerLinks}
bottomLinks={drawerBottomLinks}
bottomLinks={bottomLinks}
open={isDrawerOpen}
onOpen={openDrawer}
onClose={closeDrawer}
/>
<Box sx={{ flex: 1 }}>
<Stack flex={1}>
<Toolbar />
{children}
</Box>
</Stack>
</Box>
</>
);

View File

@@ -9,6 +9,7 @@ type ListItemLinkProps = {
icon: React.ReactNode;
primary: string;
to: string;
target?: '_blank';
onClick?: (event: React.SyntheticEvent) => void;
'data-test'?: string;
};
@@ -16,14 +17,29 @@ type ListItemLinkProps = {
export default function ListItemLink(
props: ListItemLinkProps
): React.ReactElement {
const { icon, primary, to, onClick, 'data-test': dataTest } = props;
const { icon, primary, to, onClick, 'data-test': dataTest, target } = props;
const selected = useMatch({ path: to, end: true });
const CustomLink = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
function InLineLink(linkProps, ref) {
return <Link ref={ref} to={to} {...linkProps} />;
try {
// challenge the link to check if it's absolute URL
new URL(to); // should throw an error if it's not an absolute URL
return (
<a
{...linkProps}
ref={ref}
href={to}
target={target}
rel="noopener noreferrer"
/>
);
} catch {
return <Link ref={ref} {...linkProps} to={to} />;
}
}
),
[to]
@@ -37,6 +53,7 @@ export default function ListItemLink(
selected={!!selected}
onClick={onClick}
data-test={dataTest}
target={target}
>
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
<ListItemText

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