Compare commits

..

112 Commits

Author SHA1 Message Date
Ali BARIN
424c251dde test: ntfy 2024-07-27 13:07:54 +00:00
Ali BARIN
be4493710f feat(http-client): support async beforeRequest interceptors 2024-07-27 13:05:04 +00:00
Ömer Faruk Aydın
52c0c5e0c5 Merge pull request #1992 from automatisch/fix-proxy-configuration
fix(axios): update order of interceptors
2024-07-26 20:16:38 +02:00
Ali BARIN
4c639f170e fix(axios): update order of interceptors 2024-07-26 13:52:41 +00:00
Ali BARIN
509a414151 Merge pull request #1986 from automatisch/aut-1126
refactor(http-client): inherit interceptors from parent instance
2024-07-26 11:45:02 +02:00
Ali BARIN
8f3c793a69 Merge pull request #1988 from automatisch/aut-1130
feat(formatter/text): add regex support in replace transfomer
2024-07-26 11:21:12 +02:00
Ömer Faruk Aydın
a2dd9cf1b8 Merge pull request #1989 from automatisch/fix-typos-in-appwrite-docs
docs(appwrite): fix typos
2024-07-26 10:14:15 +02:00
Ömer Faruk Aydın
42285a5879 Merge pull request #1987 from automatisch/aut-1129
fix(webhook): add missing filter coverage
2024-07-26 10:13:40 +02:00
Ali BARIN
5d63fce6f0 docs(appwrite): fix typos 2024-07-25 09:18:25 +00:00
Ali BARIN
46a4c8faec feat(formatter/text): add regex support in replace transfomer 2024-07-24 17:34:14 +00:00
Ali BARIN
ba14481151 fix(webhook): add missing filter coverage 2024-07-24 16:01:28 +00:00
Ali BARIN
bdd8da98c4 Merge pull request #1985 from automatisch/aut-1061
feat: use dynamic DOCS URL
2024-07-24 14:20:03 +02:00
Ali BARIN
5a4207414d refactor(http-client): inherit interceptors from parent instance 2024-07-23 15:01:12 +00:00
Ali BARIN
730fdd32b1 feat: use dynamic DOCS URL 2024-07-23 09:48:20 +00:00
Ali BARIN
aa7f6694fc Merge pull request #1974 from kuba618/AUT-1115
test: add admin account setup tests
2024-07-22 17:03:55 +02:00
Jakub P.
8fba6704df Merge remote-tracking branch 'upstream/main' into AUT-1115 2024-07-22 16:32:09 +02:00
Jakub P.
37d02eba02 test: add teardown dependency to clean db after tests 2024-07-22 15:42:21 +02:00
Ali BARIN
75a87cf070 Merge pull request #1965 from automatisch/add-cryptography-app
feat: add cryptography app with createHmac and createSignature actions
2024-07-22 14:22:14 +02:00
Ömer Faruk Aydın
8acdc5853d Merge pull request #1978 from automatisch/AUT-1105
feat: refactor delete user mutation with the REST API endpoint
2024-07-22 14:11:35 +02:00
kasia.oczkowska
1aa1f441b3 feat: refactor delete user mutation with the REST API endpoint 2024-07-22 13:27:57 +02:00
Ömer Faruk Aydın
0e26032ac3 Merge pull request #1977 from automatisch/add-create-uuid-text-formatter
feat(formatter/text): add create uuid action
2024-07-22 13:24:14 +02:00
Ömer Faruk Aydın
871f25c6d9 Merge pull request #1975 from automatisch/add-get-current-timestamp
feat(formatter/date-time): add get current timestamp action
2024-07-22 13:23:06 +02:00
Ömer Faruk Aydın
d8199e7ba7 Merge pull request #1983 from automatisch/strict-installation-redirection
fix: safeguard redirect to /installation
2024-07-22 13:20:25 +02:00
Ali BARIN
52b938eabe Merge pull request #1980 from kuba618/AUT-1119
test: add removed user invitation test
2024-07-19 11:42:29 +02:00
Ali BARIN
94fddf3d9b Merge pull request #1979 from kuba618/AUT-1118
chore: add linter to tests
2024-07-19 11:41:34 +02:00
Ali BARIN
02c98c1ece Merge pull request #1982 from automatisch/remove-deleted-connection
fix(AppConnectionRow): invalidate app connections upon deletion
2024-07-19 11:38:31 +02:00
Ali BARIN
6ba77667e9 Merge pull request #1981 from automatisch/AUT-1103
feat: use create access token api endpoint instead of login mutation
2024-07-19 11:38:17 +02:00
Ali BARIN
e201a5b806 fix: safeguard redirect to /installation 2024-07-19 09:31:58 +00:00
Ali BARIN
adac68c407 fix(AppConnectionRow): invalidate app connections upon deletion 2024-07-19 08:43:49 +00:00
kasia.oczkowska
d051275e54 feat: use create access token api endpoint instead of login mutation 2024-07-18 13:05:59 +01:00
Jakub P.
2afc00364a test: add removed user invitation test 2024-07-18 12:40:48 +02:00
Jakub P.
0a1461231b chore: add linter to tests 2024-07-18 12:39:04 +02:00
Jakub P.
129327f40d test: add admin account setup tests 2024-07-18 12:29:35 +02:00
Ali BARIN
58819aad94 feat(cryptography/actions): add output encoding field 2024-07-17 18:44:00 +02:00
Ali BARIN
949a2543f5 feat(cryptography): add create signature action 2024-07-17 18:44:00 +02:00
Ali BARIN
0f9d732667 feat: add cryptography app with createHmac action 2024-07-17 18:44:00 +02:00
Ali BARIN
b7df175950 feat(formatter/text): add create uuid action 2024-07-17 16:07:23 +00:00
Ali BARIN
4f7ce9874f feat(formatter/date-time): add get current timestamp action 2024-07-17 15:56:26 +00:00
Ömer Faruk Aydın
f63cc80383 Merge pull request #1973 from automatisch/AUT-1109
feat: refactor reset password mutation with the REST API endpoint
2024-07-17 17:24:55 +02:00
kasia.oczkowska
dd0a1328e8 feat: refactor reset password mutation with the REST API endpoint 2024-07-17 15:05:46 +01:00
Ali BARIN
27610c002c Merge pull request #1972 from automatisch/AUT-1107
feat: refactor forgot password mutation with the REST API endpoint
2024-07-17 15:32:37 +02:00
kasia.oczkowska
46ec9b5229 feat: refactor forgot password mutation with the REST API endpoint 2024-07-17 13:23:45 +01:00
Ali BARIN
778559d537 Merge pull request #1971 from automatisch/fix-email-worker
fix(email): correct sandbox safeguard check
2024-07-17 13:53:12 +02:00
Ali BARIN
1e64b8a903 fix(email): correct sandbox safeguard check 2024-07-17 10:25:01 +02:00
Ali BARIN
0e000693d6 Merge pull request #1970 from automatisch/rest-reset-password
feat: Implement reset password rest API endpoint
2024-07-16 16:45:20 +02:00
Faruk AYDIN
cf9e09ea7a feat: Implement reset password rest API endpoint 2024-07-16 16:22:10 +02:00
Ali BARIN
eac2f729a5 Merge pull request #1969 from automatisch/fix-migrations-rollback
fix: Remove step constraint down migration
2024-07-16 16:04:07 +02:00
Faruk AYDIN
9578cd27dd fix: Remove step contraint down migration 2024-07-16 15:52:24 +02:00
Ali BARIN
9304ffffc9 Merge pull request #1968 from automatisch/rest-forgot-password
feat: Implement forgot password rest API endpoint
2024-07-16 15:52:07 +02:00
Faruk AYDIN
3fd628cb05 feat: Implement forgot password rest API endpoint 2024-07-16 15:43:15 +02:00
Ömer Faruk Aydın
fe68b70bd9 Merge pull request #1967 from automatisch/rest-delete-user
feat: Implement rest API endpoint to delete user
2024-07-16 15:42:01 +02:00
Faruk AYDIN
ec2863d218 feat: Implement rest API endpoint to delete user 2024-07-16 15:18:01 +02:00
Ali BARIN
dc56e7f883 Merge pull request #1966 from kuba618/AUT-1099
test: add accept invitation invalid token tests
2024-07-16 13:53:32 +02:00
Jakub P.
2f1f537e00 test: add accept invitation invalid token tests 2024-07-16 12:08:50 +02:00
Ali BARIN
b7b3a3025b Merge pull request #1964 from automatisch/AUT-1095
feat: create onboarding UX flow
2024-07-15 16:11:25 +02:00
Ali BARIN
edb9526538 feat(InstallationForm): rephrase submit button 2024-07-15 12:24:35 +00:00
Ali BARIN
62117ece06 Merge pull request #1939 from automatisch/aut-1035
feat(salesforce): add find partially matching record action
2024-07-15 14:01:21 +02:00
Ali BARIN
913a2a0ac1 Merge pull request #1953 from automatisch/add-security-icon-in-additional-drawer
feat(Layout): support security icon in additional drawer link
2024-07-15 14:01:10 +02:00
Ali BARIN
92ec3d07a3 feat(salesforce/find-partially-matching-record): sanitize user inputs 2024-07-15 09:04:16 +00:00
Ali BARIN
67ee7899fd feat(salesforce): add find partially matching record action 2024-07-15 09:04:16 +00:00
Ali BARIN
b0ceb3fe7e feat(InstallationForm): use thinner font weight in success alert 2024-07-15 09:00:13 +00:00
kasia.oczkowska
ba6b4c6854 feat: create onboarding UX flow 2024-07-12 14:55:30 +01:00
Ali BARIN
61e90aed60 chore: upgrade @playwright/test with 1.45.1 2024-07-12 13:40:51 +00:00
Ali BARIN
45b7a399f2 feat(Layout): support security icon in additional drawer link 2024-07-12 13:40:22 +00:00
Ali BARIN
673ed25598 Merge pull request #1963 from automatisch/add-parse-stringified-json-action
feat(formatter): add parse stringified json action
2024-07-12 12:44:49 +02:00
Ali BARIN
14fc460174 Merge pull request #1962 from automatisch/missing-power-input-label
fix: bring back the label for PowerInput
2024-07-12 12:38:54 +02:00
Ali BARIN
211b4537f9 Merge pull request #1961 from automatisch/fix-undefined-datastore-entry-throwing
fix(datastore): stop undefined datastore entry throwing
2024-07-12 12:37:53 +02:00
Ali BARIN
a683e059aa feat(formatter): add parse stringified json action 2024-07-12 10:37:16 +00:00
kasia.oczkowska
d2d2cea567 fix: return Item in renderItemFactory 2024-07-12 11:31:43 +01:00
kasia.oczkowska
e5bda65a35 fix: bring back the label for PowerInput 2024-07-12 11:27:52 +01:00
Ali BARIN
6aaef9df4b fix(datastore): stop undefined datastore entry throwing 2024-07-12 08:58:39 +00:00
Ali BARIN
7fba4d72b0 Merge pull request #1958 from automatisch/AUT-1091
fix: introduce fix for logo being stretched
2024-07-11 15:35:48 +02:00
Ali BARIN
a6dc2ed4ba Merge pull request #1956 from automatisch/AUT-1007
feat: limit vertical scroll to a reasonable boundary
2024-07-11 15:34:04 +02:00
Ali BARIN
20a40eb2f6 Merge pull request #1959 from automatisch/AUT-1088
fix: introduce fix for undefined id on dialog close when adding a connection
2024-07-11 13:23:59 +02:00
kasia.oczkowska
ec4ac9d075 fix: introduce fix for undefined id on dialog close when adding a connection 2024-07-11 12:15:51 +01:00
kasia.oczkowska
b2c14f4226 fix: introduce fix for logo being stretched 2024-07-11 11:28:45 +01:00
Ömer Faruk Aydın
099dfbd0b0 Merge pull request #1954 from automatisch/user-invitation
feat: Implement user invitation functionality
2024-07-11 12:18:20 +02:00
Ali BARIN
a9f5736c12 test: cover accept invitation flow 2024-07-11 11:38:23 +02:00
Ali BARIN
2bd4dd3ab0 test(e2e-tests): cover password field removal 2024-07-11 11:38:23 +02:00
Ali BARIN
186850c256 test: cover user status in user serializer 2024-07-11 11:38:23 +02:00
Ali BARIN
9922033d33 feat: add accept-invitation page 2024-07-11 11:38:23 +02:00
Faruk AYDIN
3c3e6e4144 feat: Implement user invitation backend functionality 2024-07-11 11:38:23 +02:00
Faruk AYDIN
0e4ac3b7f3 feat: Add status column to user model 2024-07-11 11:38:23 +02:00
Ali BARIN
c9813e0316 Merge pull request #1950 from automatisch/AUT-1074
fix: fix error for only Create role when publishing flow
2024-07-11 11:02:08 +02:00
Ali BARIN
577c0cfab9 Merge pull request #1957 from automatisch/AUT-677
feat: add missing propTypes
2024-07-11 10:54:17 +02:00
kasia.oczkowska
0966f9d715 feat: disable publish button when no flow or permissions 2024-07-11 09:39:31 +01:00
kasia.oczkowska
3f5df118a0 feat: add missing propTypes 2024-07-11 08:54:32 +01:00
kasia.oczkowska
e05c1b26f1 feat: limit vertical scroll to a reasonable boundary 2024-07-10 14:52:07 +01:00
Ömer Faruk Aydın
725b38c697 Merge pull request #1955 from automatisch/repository-structure
docs: Adjust repository to the new packages structure
2024-07-09 13:14:27 +02:00
Faruk AYDIN
402a0fdf3b docs: Adjust repository to the new packages structure 2024-07-08 21:11:25 +02:00
Ali BARIN
078364ffa1 Merge pull request #1951 from automatisch/AUT-1069
fix: set default locale for luxon's  DateTime
2024-07-08 15:19:14 +02:00
kasia.oczkowska
f64d5ec4fc fix: set default locale for luxon's DateTime 2024-07-04 15:14:28 +01:00
kasia.oczkowska
0666174501 fix: fix error for only Create role when publishing flow 2024-07-04 14:08:35 +01:00
Ali BARIN
12194a50e1 Merge pull request #1948 from automatisch/AUT-1076
feat: block access to flow creation for users without permissions
2024-07-04 10:44:15 +02:00
kasia.oczkowska
82ee592699 feat: block access to flow creation for users without permissions 2024-07-04 08:57:03 +01:00
Ali BARIN
1b4fb2ce6e Merge pull request #1947 from automatisch/AUT-1070
feat: use try-catch block for updating flow status
2024-07-03 15:37:02 +02:00
kasia.oczkowska
ebea8d12d1 feat: use try-catch block for updating flow status 2024-07-03 14:04:11 +01:00
Ali BARIN
f842dd77df Merge pull request #1945 from automatisch/remove-access-tokens-in-perm-user-deletion
fix: delete access token before hard deleting user
2024-07-03 13:16:20 +02:00
Ali BARIN
a6ec7a6c99 Merge pull request #1944 from automatisch/fix-reset-password
fix(user): make resetPasswordToken relevant fields nullable
2024-07-03 13:16:12 +02:00
Ali BARIN
369c72282c fix: delete access token before hard deleting user 2024-07-01 11:41:11 +00:00
Ali BARIN
6f30c1a509 fix(user): make resetPasswordToken relevant fields nullable 2024-07-01 10:26:17 +00:00
Ali BARIN
abfd1116c7 Merge pull request #1941 from automatisch/AUT-1038
fix: persist value in ControlledCustomAutocomplete when it depends on other fields
2024-06-21 14:32:09 +02:00
kasia.oczkowska
017854955d fix: persist value in ControlledCustomAutocomplete when it depends on other fields 2024-06-21 10:37:39 +01:00
Ali BARIN
1405cddea1 Merge pull request #1940 from automatisch/AUT-1039
Revert "feat: persist parameters values in FlowSubstep (#1505)"
2024-06-20 15:57:47 +02:00
kasia.oczkowska
00dd3164c9 Revert "feat: persist parameters values in FlowSubstep (#1505)"
This reverts commit 017a881494.
2024-06-20 14:48:51 +01:00
Ali BARIN
d5cbc0f611 Merge pull request #1578 from automatisch/AUT-620
feat: improve UI display depending on user permissions
2024-06-20 12:04:31 +02:00
kasia.oczkowska
5d2e9ccc67 feat: improve UI display depending on user persmissions 2024-06-20 09:52:00 +00:00
kattoczko
017a881494 feat: persist parameters values in FlowSubstep (#1505)
* feat: persist parameters values in FlowSubstep

* feat: add missing import

* feat: use usePrevious hook
2024-06-20 11:13:45 +02:00
Alex Maslakov
52994970e6 fix(DynamicField): display long variables better
* Fix issue #1933

Problem with rendering in DynamicField component with long variables #1933

* Fix issue #1933 (2)

* Fix issue #1933 (3)
2024-06-20 09:55:08 +02:00
Ali BARIN
ebae629e5c Merge pull request #1931 from automatisch/AUT-1010
feat: improve nodes and edges state update
2024-06-19 16:01:24 +02:00
kasia.oczkowska
4d79220b0c refactor: fix spelling and wording errors 2024-06-14 12:39:42 +01:00
kasia.oczkowska
96fba7fbb8 feat: improve nodes and edges state update 2024-06-14 12:39:42 +01:00
243 changed files with 3661 additions and 2007 deletions

View File

@@ -71,9 +71,6 @@ jobs:
- name: Migrate database
working-directory: ./packages/backend
run: yarn db:migrate
- name: Seed user
working-directory: ./packages/backend
run: yarn db:seed:user &
- name: Install certutils
run: sudo apt install -y libnss3-tools
- name: Install mkcert

View File

@@ -0,0 +1,64 @@
import { createHmac } from 'node:crypto';
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create HMAC',
key: 'createHmac',
description: 'Create a Hash-based Message Authentication Code (HMAC) using the specified algorithm, secret key, and message.',
arguments: [
{
label: 'Algorithm',
key: 'algorithm',
type: 'dropdown',
required: true,
value: 'sha256',
description: 'Specifies the cryptographic hash function to use for HMAC generation.',
options: [
{ label: 'SHA-256', value: 'sha256' },
],
variables: true,
},
{
label: 'Message',
key: 'message',
type: 'string',
required: true,
description: 'The input message to be hashed. This is the value that will be processed to generate the HMAC.',
variables: true,
},
{
label: 'Secret Key',
key: 'secretKey',
type: 'string',
required: true,
description: 'The secret key used to create the HMAC.',
variables: true,
},
{
label: 'Output Encoding',
key: 'outputEncoding',
type: 'dropdown',
required: true,
value: 'hex',
description: 'Specifies the encoding format for the HMAC digest output.',
options: [
{ label: 'base64', value: 'base64' },
{ label: 'base64url', value: 'base64url' },
{ label: 'hex', value: 'hex' },
],
variables: true,
},
],
async run($) {
const hash = createHmac($.step.parameters.algorithm, $.step.parameters.secretKey)
.update($.step.parameters.message)
.digest($.step.parameters.outputEncoding);
$.setActionItem({
raw: {
hash
},
});
},
});

View File

@@ -0,0 +1,65 @@
import crypto from 'node:crypto';
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create Signature',
key: 'createSignature',
description: 'Create a digital signature using the specified algorithm, secret key, and message.',
arguments: [
{
label: 'Algorithm',
key: 'algorithm',
type: 'dropdown',
required: true,
value: 'RSA-SHA256',
description: 'Specifies the cryptographic hash function to use for HMAC generation.',
options: [
{ label: 'RSA-SHA256', value: 'RSA-SHA256' },
],
variables: true,
},
{
label: 'Message',
key: 'message',
type: 'string',
required: true,
description: 'The input message to be signed.',
variables: true,
},
{
label: 'Private Key',
key: 'privateKey',
type: 'string',
required: true,
description: 'The RSA private key in PEM format used for signing.',
variables: true,
},
{
label: 'Output Encoding',
key: 'outputEncoding',
type: 'dropdown',
required: true,
value: 'hex',
description: 'Specifies the encoding format for the digital signature output. This determines how the generated signature will be represented as a string.',
options: [
{ label: 'base64', value: 'base64' },
{ label: 'base64url', value: 'base64url' },
{ label: 'hex', value: 'hex' },
],
variables: true,
},
],
async run($) {
const signer = crypto.createSign($.step.parameters.algorithm);
signer.update($.step.parameters.message);
signer.end();
const signature = signer.sign($.step.parameters.privateKey, $.step.parameters.outputEncoding);
$.setActionItem({
raw: {
signature
},
});
},
});

View File

@@ -0,0 +1,4 @@
import createHmac from './create-hmac/index.js';
import createRsaSha256Signature from './create-rsa-sha256-signature/index.js';
export default [createHmac, createRsaSha256Signature];

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100pt" height="100pt" version="1.1" viewBox="0 0 100 100">
<path d="m66.012 33h-3.0117v-11c0-7.1719-5.8281-13-13-13s-13 5.8281-13 13v11h-3.0117c-2.75 0-4.9883 2.2383-4.9883 4.9883v28.012c0 2.75 2.2383 4.9883 4.9883 4.9883h32.012c2.75 0 4.9883-2.2383 4.9883-4.9883v-28.012c0.011719-2.75-2.2266-4.9883-4.9766-4.9883zm-27.012-11c0-6.0703 4.9297-11 11-11s11 4.9297 11 11v11h-22zm30 44.012c0 1.6484-1.3398 2.9883-2.9883 2.9883h-32.023c-1.6484 0-2.9883-1.3398-2.9883-2.9883v-28.023c0-1.6484 1.3398-2.9883 2.9883-2.9883h32.023c1.6484 0 2.9883 1.3398 2.9883 2.9883zm-18 9.9883v14c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-14c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm20 8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm-32-8v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm-14-26c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm0-12c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm0 24c0 0.55078-0.44922 1-1 1h-7v7c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm66-12c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm-16-12c0-0.55078 0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1zm10 24v8c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-7h-7c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm-35-17c-2.7617 0-5 2.2383-5 5 0 2.4102 1.7188 4.4297 4 4.8984v5.1016c0 0.55078 0.44922 1 1 1s1-0.44922 1-1v-5.1016c2.2812-0.46094 4-2.4805 4-4.8984 0-2.7617-2.2383-5-5-5zm0 8c-1.6484 0-3-1.3516-3-3s1.3516-3 3-3 3 1.3516 3 3-1.3516 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,14 @@
import defineApp from '../../helpers/define-app.js';
import actions from './actions/index.js';
export default defineApp({
name: 'Cryptography',
key: 'cryptography',
iconUrl: '{BASE_URL}/apps/cryptography/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/cryptography/connection',
supportsConnections: false,
baseUrl: '',
apiBaseUrl: '',
primaryColor: '001F52',
actions,
});

View File

@@ -1,157 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
import isEmpty from 'lodash/isEmpty.js';
import omitBy from 'lodash/omitBy.js';
export default defineAction({
name: 'Create event',
key: 'createEvent',
description: 'Creates a new event.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Summary',
key: 'summary',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Event Start',
key: 'eventStart',
type: 'string',
required: true,
description: 'e.g. 2018-05-12T02:00:00Z',
variables: true,
},
{
label: 'Event End',
key: 'eventEnd',
type: 'string',
required: true,
description: 'e.g. 2018-05-12T02:00:00Z',
variables: true,
},
{
label: 'Venue',
key: 'venueId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listVenues',
},
{
name: 'parameters.organizationId',
value: '{parameters.organizationId}',
},
],
},
},
{
label: 'Currency',
key: 'currencyId',
type: 'dropdown',
required: true,
description: 'The ISO 4217 currency code.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listCurrency',
},
],
},
},
{
label: 'Listed?',
key: 'listed',
type: 'dropdown',
required: false,
description: 'Can this event be found on Eventbrite?',
variables: true,
options: [
{ label: 'Yes', value: 'true' },
{ label: 'No', value: 'false' },
],
},
],
async run($) {
const {
organizationId,
name,
summary,
eventStart,
eventEnd,
venueId,
currencyId,
listed,
} = $.step.parameters;
const fields = {
name: {
html: name,
},
summary,
start: {
timezone: 'UTC',
utc: eventStart,
},
end: {
timezone: 'UTC',
utc: eventEnd,
},
currency: currencyId,
venue_id: venueId,
listed,
};
const filteredFields = omitBy(fields, isEmpty);
const { data } = await $.http.post(
`/v3/organizations/${organizationId}/events/`,
{
event: filteredFields,
}
);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,4 +0,0 @@
import createEvent from './create-event/index.js';
import publishEvent from './publish-event/index.js';
export default [createEvent, publishEvent];

View File

@@ -1,63 +0,0 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Publish event',
key: 'publishEvent',
description: 'Publishes a draft event.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
{
label: 'Event',
key: 'eventId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listEvents',
},
{
name: 'parameters.organizationId',
value: '{parameters.organizationId}',
},
{
name: 'parameters.unpublished',
value: 'true',
},
],
},
},
],
async run($) {
const { eventId } = $.step.parameters;
const { data } = await $.http.post(`/v3/events/${eventId}/publish/`);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<circle fill="#F05537" cx="128" cy="128" r="128"></circle>
<path d="M117.475323,82.7290398 C136.772428,78.4407943 156.069532,86.3025777 166.790146,101.311437 L81.5017079,120.608542 C84.3605382,102.26438 98.1782181,87.0172853 117.475323,82.7290398 Z M167.266618,153.48509 C160.596014,163.252761 150.351872,170.161601 138.678314,172.782195 C119.38121,177.070441 99.8458692,169.208657 89.1252554,153.961562 L174.651929,134.664457 L188.469609,131.567391 L215.152026,125.611495 C214.91379,119.893834 214.199082,114.176173 213.007903,108.696749 C202.287289,62.7172275 155.354825,33.8906884 108.42236,44.6113021 C61.4898956,55.3319159 32.1868848,101.073201 43.1457344,147.290958 C54.1045839,193.508715 100.798813,222.097018 147.731277,211.376404 C175.366637,205.182272 196.807864,186.599875 207.766714,163.014525 L167.266618,153.48509 L167.266618,153.48509 Z" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,19 +0,0 @@
import { URLSearchParams } from 'url';
export default async function generateAuthUrl($) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value;
const searchParams = new URLSearchParams({
response_type: 'code',
client_id: $.auth.data.clientId,
redirect_uri: redirectUri,
});
const url = `https://www.eventbrite.com/oauth/authorize?${searchParams.toString()}`;
await $.auth.set({
url,
});
}

View File

@@ -1,46 +0,0 @@
import generateAuthUrl from './generate-auth-url.js';
import verifyCredentials from './verify-credentials.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/eventbrite/connections/add',
placeholder: null,
description:
'When asked to input a redirect URL in Eventbrite, enter the URL above.',
clickToCopy: true,
},
{
key: 'clientId',
label: 'API Key',
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,
},
],
generateAuthUrl,
verifyCredentials,
isStillVerified,
};

View File

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

View File

@@ -1,42 +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://www.eventbrite.com/oauth/token',
{
grant_type: 'authorization_code',
client_id: $.auth.data.clientId,
client_secret: $.auth.data.clientSecret,
code: $.auth.data.code,
redirect_uri: redirectUri,
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
await $.auth.set({
accessToken: data.access_token,
tokenType: data.token_type,
});
const currentUser = await getCurrentUser($);
const screenName = [currentUser.name, currentUser.emails[0].email]
.filter(Boolean)
.join(' @ ');
await $.auth.set({
clientId: $.auth.data.clientId,
clientSecret: $.auth.data.clientSecret,
screenName,
});
};
export default verifyCredentials;

View File

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

View File

@@ -1,6 +0,0 @@
const getCurrentUser = async ($) => {
const { data: currentUser } = await $.http.get('/v3/users/me');
return currentUser;
};
export default getCurrentUser;

View File

@@ -1,5 +0,0 @@
import listEvents from './list-events/index.js';
import listOrganizations from './list-organizations/index.js';
import listVenues from './list-venues/index.js';
export default [listEvents, listOrganizations, listVenues];

View File

@@ -1,49 +0,0 @@
export default {
name: 'List events',
key: 'listEvents',
async run($) {
const events = {
data: [],
};
const organizationId = $.step.parameters.organizationId;
const unpublished = $.step.parameters.unpublished === 'true';
if (!organizationId) {
return events;
}
const params = {
continuation: undefined,
order_by: 'created_desc',
};
if (unpublished) {
params.status = 'draft';
}
do {
const { data } = await $.http.get(
`/v3/organizations/${organizationId}/events/`,
{
params,
}
);
if (data.pagination.has_more_items) {
params.continuation = data.pagination.continuation;
}
if (data.events) {
for (const event of data.events) {
events.data.push({
value: event.id,
name: `${event.name.text} (${event.status})`,
});
}
}
} while (params.continuation);
return events;
},
};

View File

@@ -1,35 +0,0 @@
export default {
name: 'List organizations',
key: 'listOrganizations',
async run($) {
const organizations = {
data: [],
};
const params = {
continuation: undefined,
};
do {
const { data } = await $.http.get('/v3/users/me/organizations', {
params,
});
if (data.pagination.has_more_items) {
params.continuation = data.pagination.continuation;
}
if (data.organizations) {
for (const organization of data.organizations) {
organizations.data.push({
value: organization.id,
name: organization.name,
});
}
}
} while (params.continuation);
return organizations;
},
};

View File

@@ -1,43 +0,0 @@
export default {
name: 'List venues',
key: 'listVenues',
async run($) {
const venues = {
data: [],
};
const organizationId = $.step.parameters.organizationId;
if (!organizationId) {
return venues;
}
const params = {
continuation: undefined,
};
do {
const { data } = await $.http.get(
`/v3/organizations/${organizationId}/venues/`,
{
params,
}
);
if (data.pagination.has_more_items) {
params.continuation = data.pagination.continuation;
}
if (data.venues) {
for (const venue of data.venues) {
venues.data.push({
value: venue.id,
name: venue.name,
});
}
}
} while (params.continuation);
return venues;
},
};

View File

@@ -1,22 +0,0 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import auth from './auth/index.js';
import dynamicData from './dynamic-data/index.js';
import triggers from './triggers/index.js';
import actions from './actions/index.js';
export default defineApp({
name: 'Eventbrite',
key: 'eventbrite',
baseUrl: 'https://www.eventbrite.com',
apiBaseUrl: 'https://www.eventbriteapi.com',
iconUrl: '{BASE_URL}/apps/eventbrite/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/eventbrite/connection',
primaryColor: 'F05537',
supportsConnections: true,
beforeRequest: [addAuthHeader],
auth,
dynamicData,
triggers,
actions,
});

View File

@@ -1,11 +0,0 @@
import newAttendeeCheckIn from './new-attendee-check-in/index.js';
import newAttendeeRegistered from './new-attendee-registered/index.js';
import newEvents from './new-events/index.js';
import updatedAttendee from './updated-attendee/index.js';
export default [
updatedAttendee,
newAttendeeCheckIn,
newAttendeeRegistered,
newEvents,
];

View File

@@ -1,120 +0,0 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New attendee check in',
key: 'newAttendeeCheckIn',
type: 'webhook',
description: "Triggers when an attendee's barcode is scanned in.",
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
{
label: 'Event',
key: 'eventId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listEvents',
},
{
name: 'parameters.organizationId',
value: '{parameters.organizationId}',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const eventId = $.step.parameters.eventId;
const organizationId = $.step.parameters.organizationId;
const params = {
event_id: eventId,
};
const {
data: { orders },
} = await $.http.get(`/v3/events/${eventId}/orders/`, params);
if (orders.length === 0) {
return;
}
const computedWebhookEvent = {
config: {
action: 'barcode.checked_in',
user_id: organizationId,
webhook_id: '11111111',
endpoint_url: $.webhookUrl,
},
api_url: orders[0].resource_uri,
};
const dataItem = {
raw: computedWebhookEvent,
meta: {
internalId: computedWebhookEvent.user_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const eventId = $.step.parameters.eventId;
const payload = {
endpoint_url: $.webhookUrl,
actions: 'attendee.checked_in',
event_id: eventId,
};
const { data } = await $.http.post(
`/v3/organizations/${organizationId}/webhooks/`,
payload
);
await $.flow.setRemoteWebhookId(data.id);
},
async unregisterHook($) {
await $.http.delete(`/v3/webhooks/${$.flow.remoteWebhookId}/`);
},
});

View File

@@ -1,120 +0,0 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New attendee registered',
key: 'newAttendeeRegistered',
type: 'webhook',
description: 'Triggers when an attendee orders a ticket for an event.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
{
label: 'Event',
key: 'eventId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listEvents',
},
{
name: 'parameters.organizationId',
value: '{parameters.organizationId}',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const eventId = $.step.parameters.eventId;
const organizationId = $.step.parameters.organizationId;
const params = {
event_id: eventId,
};
const {
data: { orders },
} = await $.http.get(`/v3/events/${eventId}/orders/`, params);
if (orders.length === 0) {
return;
}
const computedWebhookEvent = {
config: {
action: 'order.placed',
user_id: organizationId,
webhook_id: '11111111',
endpoint_url: $.webhookUrl,
},
api_url: orders[0].resource_uri,
};
const dataItem = {
raw: computedWebhookEvent,
meta: {
internalId: computedWebhookEvent.user_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const eventId = $.step.parameters.eventId;
const payload = {
endpoint_url: $.webhookUrl,
actions: 'order.placed',
event_id: eventId,
};
const { data } = await $.http.post(
`/v3/organizations/${organizationId}/webhooks/`,
payload
);
await $.flow.setRemoteWebhookId(data.id);
},
async unregisterHook($) {
await $.http.delete(`/v3/webhooks/${$.flow.remoteWebhookId}/`);
},
});

View File

@@ -1,98 +0,0 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'New events',
key: 'newEvents',
type: 'webhook',
description:
'Triggers when a new event is published and live within an organization.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const organizationId = $.step.parameters.organizationId;
const params = {
orderBy: 'created_desc',
status: 'all',
};
const {
data: { events },
} = await $.http.get(`/v3/organizations/${organizationId}/events/`, params);
if (events.length === 0) {
return;
}
const computedWebhookEvent = {
config: {
action: 'event.published',
user_id: events[0].organization_id,
webhook_id: '11111111',
endpoint_url: $.webhookUrl,
},
api_url: events[0].resource_uri,
};
const dataItem = {
raw: computedWebhookEvent,
meta: {
internalId: computedWebhookEvent.user_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const payload = {
endpoint_url: $.webhookUrl,
actions: 'event.published',
event_id: '',
};
const { data } = await $.http.post(
`/v3/organizations/${organizationId}/webhooks/`,
payload
);
await $.flow.setRemoteWebhookId(data.id);
},
async unregisterHook($) {
await $.http.delete(`/v3/webhooks/${$.flow.remoteWebhookId}/`);
},
});

View File

@@ -1,120 +0,0 @@
import Crypto from 'crypto';
import defineTrigger from '../../../../helpers/define-trigger.js';
export default defineTrigger({
name: 'Updated Attendee',
key: 'updatedAttendee',
type: 'webhook',
description: 'Triggers when attendee data is updated.',
arguments: [
{
label: 'Organization',
key: 'organizationId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listOrganizations',
},
],
},
},
{
label: 'Event',
key: 'eventId',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listEvents',
},
{
name: 'parameters.organizationId',
value: '{parameters.organizationId}',
},
],
},
},
],
async run($) {
const dataItem = {
raw: $.request.body,
meta: {
internalId: Crypto.randomUUID(),
},
};
$.pushTriggerItem(dataItem);
},
async testRun($) {
const eventId = $.step.parameters.eventId;
const organizationId = $.step.parameters.organizationId;
const params = {
event_id: eventId,
};
const {
data: { attendees },
} = await $.http.get(`/v3/events/${eventId}/attendees/`, params);
if (attendees.length === 0) {
return;
}
const computedWebhookEvent = {
config: {
action: 'attendee.updated',
user_id: organizationId,
webhook_id: '11111111',
endpoint_url: $.webhookUrl,
},
api_url: attendees[0].resource_uri,
};
const dataItem = {
raw: computedWebhookEvent,
meta: {
internalId: computedWebhookEvent.user_id,
},
};
$.pushTriggerItem(dataItem);
},
async registerHook($) {
const organizationId = $.step.parameters.organizationId;
const eventId = $.step.parameters.eventId;
const payload = {
endpoint_url: $.webhookUrl,
actions: 'attendee.updated',
event_id: eventId,
};
const { data } = await $.http.post(
`/v3/organizations/${organizationId}/webhooks/`,
payload
);
await $.flow.setRemoteWebhookId(data.id);
},
async unregisterHook($) {
await $.http.delete(`/v3/webhooks/${$.flow.remoteWebhookId}/`);
},
});

View File

@@ -1,8 +1,10 @@
import defineAction from '../../../../helpers/define-action.js';
import formatDateTime from './transformers/format-date-time.js';
import getCurrentTimestamp from './transformers/get-current-timestamp.js';
const transformers = {
formatDateTime,
getCurrentTimestamp,
};
export default defineAction({
@@ -16,7 +18,16 @@ export default defineAction({
type: 'dropdown',
required: true,
variables: true,
options: [{ label: 'Format Date / Time', value: 'formatDateTime' }],
options: [
{
label: 'Get current timestamp',
value: 'getCurrentTimestamp',
},
{
label: 'Format Date / Time',
value: 'formatDateTime',
},
],
additionalFields: {
type: 'query',
name: 'getDynamicFields',

View File

@@ -0,0 +1,5 @@
const getCurrentTimestamp = () => {
return Date.now();
};
export default getCurrentTimestamp;

View File

@@ -14,6 +14,8 @@ import stringToBase64 from './transformers/string-to-base64.js';
import encodeUri from './transformers/encode-uri.js';
import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js';
import parseStringifiedJson from './transformers/parse-stringified-json.js';
import createUuid from './transformers/create-uuid.js';
const transformers = {
base64ToString,
@@ -30,6 +32,8 @@ const transformers = {
encodeUri,
trimWhitespace,
useDefaultValue,
parseStringifiedJson,
createUuid,
};
export default defineAction({
@@ -47,19 +51,21 @@ export default defineAction({
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' },
{ label: 'Create UUID', value: 'createUuid' },
{ label: 'Encode URI', value: 'encodeUri' },
{
label: 'Encode URI Component',
value: 'encodeUriComponent',
},
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
{ label: 'Extract Email Address', value: 'extractEmailAddress' },
{ label: 'Extract Number', value: 'extractNumber' },
{ label: 'Lowercase', value: 'lowercase' },
{ label: 'Parse stringified JSON', value: 'parseStringifiedJson' },
{ label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Encode URI', value: 'encodeUri' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
],

View File

@@ -0,0 +1,7 @@
import { v4 as uuidv4 } from 'uuid';
const createUuidV4 = () => {
return uuidv4();
};
export default createUuidV4;

View File

@@ -0,0 +1,7 @@
const parseStringifiedJson = ($) => {
const input = $.step.parameters.input;
return JSON.parse(input);
};
export default parseStringifiedJson;

View File

@@ -1,8 +1,26 @@
const replace = ($) => {
const input = $.step.parameters.input;
const find = $.step.parameters.find;
const replace = $.step.parameters.replace;
const useRegex = $.step.parameters.useRegex;
if (useRegex) {
const ignoreCase = $.step.parameters.ignoreCase;
const flags = [ignoreCase && 'i', 'g'].filter(Boolean).join('');
const timeoutId = setTimeout(() => {
$.execution.exit();
}, 100);
const regex = new RegExp(find, flags);
const replacedValue = input.replaceAll(regex, replace);
clearTimeout(timeoutId);
return replacedValue;
}
return input.replaceAll(find, replace);
};

View File

@@ -1,3 +1,4 @@
import listTransformOptions from './list-transform-options/index.js';
import listReplaceRegexOptions from './list-replace-regex-options/index.js';
export default [listTransformOptions];
export default [listTransformOptions, listReplaceRegexOptions];

View File

@@ -0,0 +1,23 @@
export default {
name: 'List replace regex options',
key: 'listReplaceRegexOptions',
async run($) {
if (!$.step.parameters.useRegex) return [];
return [
{
label: 'Ignore case',
key: 'ignoreCase',
type: 'dropdown',
required: true,
description: 'Ignore case sensitivity.',
variables: true,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
];
},
};

View File

@@ -12,6 +12,7 @@ import stringToBase64 from './text/string-to-base64.js';
import encodeUri from './text/encode-uri.js';
import trimWhitespace from './text/trim-whitespace.js';
import useDefaultValue from './text/use-default-value.js';
import parseStringifiedJson from './text/parse-stringified-json.js';
import performMathOperation from './numbers/perform-math-operation.js';
import randomNumber from './numbers/random-number.js';
import formatNumber from './numbers/format-number.js';
@@ -38,6 +39,7 @@ const options = {
formatNumber,
formatPhoneNumber,
formatDateTime,
parseStringifiedJson,
};
export default {

View File

@@ -0,0 +1,12 @@
const useDefaultValue = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Stringified JSON you want to parse.',
variables: true,
},
];
export default useDefaultValue;

View File

@@ -23,6 +23,33 @@ const replace = [
description: 'Text that will replace the found text.',
variables: true,
},
{
label: 'Use Regular Expression',
key: 'useRegex',
type: 'dropdown',
required: true,
description: 'Use regex to search values.',
variables: true,
value: false,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
additionalFields: {
type: 'query',
name: 'getDynamicFields',
arguments: [
{
name: 'key',
value: 'listReplaceRegexOptions',
},
{
name: 'parameters.useRegex',
value: '{parameters.useRegex}',
},
],
},
},
];
export default replace;

View File

@@ -89,6 +89,8 @@ export default defineAction({
const response = await $.http.post('/', payload);
console.log(response.config.additionalProperties.extraData);
$.setActionItem({
raw: response.data,
});

View File

@@ -1,4 +1,7 @@
const addAuthHeader = ($, requestConfig) => {
console.log('requestConfig', requestConfig)
if (requestConfig.additionalProperties?.skip) return requestConfig;
if ($.auth.data.serverUrl) {
requestConfig.baseURL = $.auth.data.serverUrl;
}

View File

@@ -0,0 +1,23 @@
const asyncBeforeRequest = async ($, requestConfig) => {
if (requestConfig.additionalProperties?.skip)
return requestConfig;
const response = await $.http.post(
'http://localhost:3000/webhooks/flows/8a040f4e-817f-4076-80ba-3c1c0af7e65e/sync',
null,
{
additionalProperties: {
skip: true,
},
}
);
console.log(response);
requestConfig.additionalProperties = {
extraData: response.data
}
return requestConfig;
};
export default asyncBeforeRequest;

View File

@@ -1,5 +1,6 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import asyncBeforeRequest from './common/async-before-request.js';
import auth from './auth/index.js';
import actions from './actions/index.js';
@@ -12,7 +13,7 @@ export default defineApp({
baseUrl: 'https://ntfy.sh',
apiBaseUrl: 'https://ntfy.sh',
primaryColor: '56bda8',
beforeRequest: [addAuthHeader],
beforeRequest: [asyncBeforeRequest, addAuthHeader],
auth,
actions,
});

View File

@@ -0,0 +1,101 @@
import defineAction from '../../../../helpers/define-action.js';
import listObjects from '../../dynamic-data/list-objects/index.js';
import listFields from '../../dynamic-data/list-fields/index.js';
export default defineAction({
name: 'Find partially matching record',
key: 'findPartiallyMatchingRecord',
description: 'Finds a record of a specified object by a field containing a value.',
arguments: [
{
label: 'Object',
key: 'object',
type: 'dropdown',
required: true,
variables: true,
description: 'Pick which type of object you want to search for.',
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listObjects',
},
],
},
},
{
label: 'Field',
key: 'field',
type: 'dropdown',
description: 'Pick which field to search by',
required: true,
variables: true,
dependsOn: ['parameters.object'],
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listFields',
},
{
name: 'parameters.object',
value: '{parameters.object}',
},
],
},
},
{
label: 'Search value to contain',
key: 'searchValue',
type: 'string',
required: true,
variables: true,
description: 'The value to search for in the field.',
},
],
async run($) {
const sanitizedSearchValue = $.step.parameters.searchValue.replaceAll(`'`, `\\'`);
// validate given object
const objects = await listObjects.run($);
const validObject = objects.data.find((object) => object.value === $.step.parameters.object);
if (!validObject) {
throw new Error(`The "${$.step.parameters.object}" object does not exist.`);
}
// validate given object field
const fields = await listFields.run($);
const validField = fields.data.find((field) => field.value === $.step.parameters.field);
if (!validField) {
throw new Error(`The "${$.step.parameters.field}" field does not exist on the "${$.step.parameters.object}" object.`);
}
const query = `
SELECT
FIELDS(ALL)
FROM
${$.step.parameters.object}
WHERE
${$.step.parameters.field} LIKE '%${sanitizedSearchValue}%'
LIMIT 1
`;
const options = {
params: {
q: query,
},
};
const { data } = await $.http.get('/services/data/v61.0/query', options);
const record = data.records[0];
$.setActionItem({ raw: record });
},
});

View File

@@ -1,5 +1,6 @@
import createAttachment from './create-attachment/index.js';
import executeQuery from './execute-query/index.js';
import findRecord from './find-record/index.js';
import findPartiallyMatchingRecord from './find-partially-matching-record/index.js';
export default [findRecord, createAttachment, executeQuery];
export default [findRecord, findPartiallyMatchingRecord, createAttachment, executeQuery];

View File

@@ -97,8 +97,12 @@ const appConfig = {
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkIcon: process.env.ADDITIONAL_DRAWER_LINK_ICON,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
disableSeedUser: process.env.DISABLE_SEED_USER === 'true',
httpProxy: process.env.http_proxy,
httpsProxy: process.env.https_proxy,
noProxy: process.env.no_proxy,
};
if (!appConfig.encryptionKey) {

View File

@@ -0,0 +1,10 @@
import User from '../../../../../models/user.js';
export default async (request, response) => {
const id = request.params.userId;
const user = await User.query().findById(id).throwIfNotFound();
await user.softRemove();
response.status(204).end();
};

View File

@@ -0,0 +1,43 @@
import { describe, it, 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 { createRole } from '../../../../../../test/factories/role';
describe('DELETE /api/v1/admin/users/:userId', () => {
let currentUser, currentUserRole, anotherUser, token;
beforeEach(async () => {
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
anotherUser = await createUser();
token = await createAuthTokenByUserId(currentUser.id);
});
it('should soft delete user and respond with no content', async () => {
await request(app)
.delete(`/api/v1/admin/users/${anotherUser.id}`)
.set('Authorization', token)
.expect(204);
});
it('should return not found response for not existing user UUID', async () => {
const notExistingUserUUID = Crypto.randomUUID();
await request(app)
.delete(`/api/v1/admin/users/${notExistingUserUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.delete('/api/v1/admin/users/invalidUserUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -7,6 +7,7 @@ export default async (request, response) => {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkIcon: appConfig.additionalDrawerLinkIcon,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};

View File

@@ -4,6 +4,7 @@ import { createConfig } from '../../../../../test/factories/config.js';
import app from '../../../../app.js';
import configMock from '../../../../../test/mocks/rest/api/v1/automatisch/config.js';
import * as license from '../../../../helpers/license.ee.js';
import appConfig from '../../../../config/app.js';
describe('GET /api/v1/automatisch/config', () => {
it('should return Automatisch config', async () => {
@@ -48,4 +49,18 @@ describe('GET /api/v1/automatisch/config', () => {
expect(response.body).toEqual(expectedPayload);
});
it('should return additional environment variables', async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue('link');
vi.spyOn(appConfig, 'additionalDrawerLinkIcon', 'get').mockReturnValue('icon');
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue('text');
expect(appConfig.disableNotificationsPage).toEqual(true);
expect(appConfig.disableFavicon).toEqual(true);
expect(appConfig.additionalDrawerLink).toEqual('link');
expect(appConfig.additionalDrawerLinkIcon).toEqual('icon');
expect(appConfig.additionalDrawerLinkText).toEqual('text');
});
});

View File

@@ -7,6 +7,7 @@ export default async (request, response) => {
isCloud: appConfig.isCloud,
isMation: appConfig.isMation,
isEnterprise: await hasValidLicense(),
docsUrl: appConfig.docsUrl,
};
renderObject(response, info);

View File

@@ -10,6 +10,7 @@ describe('GET /api/v1/automatisch/info', () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false);
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
vi.spyOn(appConfig, 'docsUrl', 'get').mockReturnValue('https://automatisch.io/docs');
const response = await request(app)
.get('/api/v1/automatisch/info')

View File

@@ -0,0 +1,21 @@
import User from '../../../../models/user.js';
export default async (request, response) => {
const { token, password } = request.body;
if (!token) {
throw new Error('Invitation token is required!');
}
const user = await User.query()
.findOne({ invitation_token: token })
.throwIfNotFound();
if (!user.isInvitationTokenValid()) {
return response.status(422).end();
}
await user.acceptInvitation(password);
response.status(204).end();
};

View File

@@ -0,0 +1,13 @@
import User from '../../../../models/user.js';
export default async (request, response) => {
const { email } = request.body;
const user = await User.query()
.findOne({ email: email.toLowerCase() })
.throwIfNotFound();
await user.sendResetPasswordEmail();
response.status(204).end();
};

View File

@@ -0,0 +1,30 @@
import { describe, it, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import { createUser } from '../../../../../test/factories/user';
describe('POST /api/v1/users/forgot-password', () => {
let currentUser;
beforeEach(async () => {
currentUser = await createUser();
});
it('should respond with no content', async () => {
await request(app)
.post('/api/v1/users/forgot-password')
.send({
email: currentUser.email,
})
.expect(204);
});
it('should return not found response for not existing user UUID', async () => {
await request(app)
.post('/api/v1/users/forgot-password')
.send({
email: 'nonexisting@automatisch.io',
})
.expect(404);
});
});

View File

@@ -0,0 +1,23 @@
import User from '../../../../models/user.js';
import { renderError } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const { token, password } = request.body;
const user = await User.query()
.findOne({
reset_password_token: token,
})
.throwIfNotFound();
if (!user.isResetPasswordTokenValid()) {
return renderError(response, [{ general: [invalidTokenErrorMessage] }]);
}
await user.resetPassword(password);
response.status(204).end();
};
const invalidTokenErrorMessage =
'Reset password link is not valid or expired. Try generating a new link.';

View File

@@ -0,0 +1,49 @@
import { describe, it, beforeEach } from 'vitest';
import request from 'supertest';
import { DateTime } from 'luxon';
import app from '../../../../app.js';
import { createUser } from '../../../../../test/factories/user';
describe('POST /api/v1/users/reset-password', () => {
let currentUser;
beforeEach(async () => {
currentUser = await createUser({
resetPasswordToken: 'sampleResetPasswordToken',
resetPasswordTokenSentAt: DateTime.now().toISO(),
});
});
it('should respond with no content', async () => {
await request(app)
.post('/api/v1/users/reset-password')
.send({
token: currentUser.resetPasswordToken,
password: 'newPassword',
})
.expect(204);
});
it('should return not found response for not existing user', async () => {
await request(app)
.post('/api/v1/users/reset-password')
.send({
token: 'nonExistingResetPasswordToken',
})
.expect(404);
});
it('should return unprocessable entity for existing user with expired reset password token', async () => {
const user = await createUser({
resetPasswordToken: 'anotherResetPasswordToken',
resetPasswordTokenSentAt: DateTime.now().minus({ days: 2 }).toISO(),
});
await request(app)
.post('/api/v1/users/reset-password')
.send({
token: user.resetPasswordToken,
})
.expect(422);
});
});

View File

@@ -5,9 +5,11 @@ export async function up(knex) {
});
}
export async function down(knex) {
return knex.schema.alterTable('steps', (table) => {
table.string('key').notNullable().alter();
table.string('app_key').notNullable().alter();
});
export async function down() {
// We can't use down migration here since there are null values which needs to be set!
// We don't want to set those values by default key and app key since it will mislead users.
// return knex.schema.alterTable('steps', (table) => {
// table.string('key').notNullable().alter();
// table.string('app_key').notNullable().alter();
// });
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
return knex.schema.table('users', (table) => {
table.string('status').defaultTo('active');
});
}
export async function down(knex) {
return knex.schema.table('users', (table) => {
table.dropColumn('status');
});
}

View File

@@ -0,0 +1,13 @@
export async function up(knex) {
return knex.schema.table('users', (table) => {
table.string('invitation_token');
table.timestamp('invitation_token_sent_at');
});
}
export async function down(knex) {
return knex.schema.table('users', (table) => {
table.dropColumn('invitation_token');
table.dropColumn('invitation_token_sent_at');
});
}

View File

@@ -10,15 +10,11 @@ import deleteCurrentUser from './mutations/delete-current-user.ee.js';
import deleteFlow from './mutations/delete-flow.js';
import deleteRole from './mutations/delete-role.ee.js';
import deleteStep from './mutations/delete-step.js';
import deleteUser from './mutations/delete-user.ee.js';
import duplicateFlow from './mutations/duplicate-flow.js';
import executeFlow from './mutations/execute-flow.js';
import forgotPassword from './mutations/forgot-password.ee.js';
import generateAuthUrl from './mutations/generate-auth-url.js';
import login from './mutations/login.js';
import registerUser from './mutations/register-user.ee.js';
import resetConnection from './mutations/reset-connection.js';
import resetPassword from './mutations/reset-password.ee.js';
import updateAppAuthClient from './mutations/update-app-auth-client.ee.js';
import updateAppConfig from './mutations/update-app-config.ee.js';
import updateConfig from './mutations/update-config.ee.js';
@@ -46,15 +42,11 @@ const mutationResolvers = {
deleteFlow,
deleteRole,
deleteStep,
deleteUser,
duplicateFlow,
executeFlow,
forgotPassword,
generateAuthUrl,
login,
registerUser,
resetConnection,
resetPassword,
updateAppAuthClient,
updateAppConfig,
updateConfig,

View File

@@ -1,10 +1,16 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js';
import Role from '../../models/role.js';
import emailQueue from '../../queues/email.js';
import {
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../../helpers/remove-job-configuration.js';
const createUser = async (_parent, params, context) => {
context.currentUser.can('create', 'User');
const { fullName, email, password } = params.input;
const { fullName, email } = params.input;
const existingUser = await User.query().findOne({
email: email.toLowerCase(),
@@ -17,7 +23,7 @@ const createUser = async (_parent, params, context) => {
const userPayload = {
fullName,
email,
password,
status: 'invited',
};
try {
@@ -32,7 +38,29 @@ const createUser = async (_parent, params, context) => {
const user = await User.query().insert(userPayload);
return user;
await user.generateInvitationToken();
const jobName = `Invitation Email - ${user.id}`;
const acceptInvitationUrl = `${appConfig.webAppUrl}/accept-invitation?token=${user.invitationToken}`;
const jobPayload = {
email: user.email,
subject: 'You are invited!',
template: 'invitation-instructions',
params: {
fullName: user.fullName,
acceptInvitationUrl,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
await emailQueue.add(jobName, jobPayload, jobOptions);
return { user, acceptInvitationUrl };
};
export default createUser;

View File

@@ -1,24 +0,0 @@
import { Duration } from 'luxon';
import User from '../../models/user.js';
import deleteUserQueue from '../../queues/delete-user.ee.js';
const deleteUser = async (_parent, params, context) => {
context.currentUser.can('delete', 'User');
const id = params.input.id;
await User.query().deleteById(id);
const jobName = `Delete user - ${id}`;
const jobPayload = { id };
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobOptions = {
delay: millisecondsFor30Days,
};
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
return true;
};
export default deleteUser;

View File

@@ -1,43 +0,0 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js';
import emailQueue from '../../queues/email.js';
import {
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../../helpers/remove-job-configuration.js';
const forgotPassword = async (_parent, params) => {
const { email } = params.input;
const user = await User.query().findOne({ email: email.toLowerCase() });
if (!user) {
throw new Error('Email address not found!');
}
await user.generateResetPasswordToken();
const jobName = `Reset Password Email - ${user.id}`;
const jobPayload = {
email: user.email,
subject: 'Reset Password',
template: 'reset-password-instructions',
params: {
token: user.resetPasswordToken,
webAppUrl: appConfig.webAppUrl,
fullName: user.fullName,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
await emailQueue.add(jobName, jobPayload, jobOptions);
return true;
};
export default forgotPassword;

View File

@@ -1,17 +0,0 @@
import User from '../../models/user.js';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id.js';
const login = async (_parent, params) => {
const user = await User.query().findOne({
email: params.input.email.toLowerCase(),
});
if (user && (await user.login(params.input.password))) {
const token = await createAuthTokenByUserId(user.id);
return { token, user };
}
throw new Error('User could not be found.');
};
export default login;

View File

@@ -1,23 +0,0 @@
import User from '../../models/user.js';
const resetPassword = async (_parent, params) => {
const { token, password } = params.input;
if (!token) {
throw new Error('Reset password token is required!');
}
const user = await User.query().findOne({ reset_password_token: token });
if (!user || !user.isResetPasswordTokenValid()) {
throw new Error(
'Reset password link is not valid or expired. Try generating a new link.'
);
}
await user.resetPassword(password);
return true;
};
export default resetPassword;

View File

@@ -8,21 +8,17 @@ type Mutation {
createFlow(input: CreateFlowInput): Flow
createRole(input: CreateRoleInput): Role
createStep(input: CreateStepInput): Step
createUser(input: CreateUserInput): User
createUser(input: CreateUserInput): UserWithAcceptInvitationUrl
deleteConnection(input: DeleteConnectionInput): Boolean
deleteCurrentUser: Boolean
deleteFlow(input: DeleteFlowInput): Boolean
deleteRole(input: DeleteRoleInput): Boolean
deleteStep(input: DeleteStepInput): Step
deleteUser(input: DeleteUserInput): Boolean
duplicateFlow(input: DuplicateFlowInput): Flow
executeFlow(input: ExecuteFlowInput): executeFlowType
forgotPassword(input: ForgotPasswordInput): Boolean
generateAuthUrl(input: GenerateAuthUrlInput): AuthLink
login(input: LoginInput): Auth
registerUser(input: RegisterUserInput): User
resetConnection(input: ResetConnectionInput): Connection
resetPassword(input: ResetPasswordInput): Boolean
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
updateAppConfig(input: UpdateAppConfigInput): AppConfig
updateConfig(input: JSONObject): JSONObject
@@ -154,11 +150,6 @@ enum ArgumentEnumType {
string
}
type Auth {
user: User
token: String
}
type AuthenticationStep {
type: String
name: String
@@ -375,7 +366,6 @@ input DeleteStepInput {
input CreateUserInput {
fullName: String!
email: String!
password: String!
role: UserRoleInput!
}
@@ -390,10 +380,6 @@ input UpdateUserInput {
role: UserRoleInput
}
input DeleteUserInput {
id: String!
}
input RegisterUserInput {
fullName: String!
email: String!
@@ -406,20 +392,6 @@ input UpdateCurrentUserInput {
fullName: String
}
input ForgotPasswordInput {
email: String!
}
input ResetPasswordInput {
token: String!
password: String!
}
input LoginInput {
email: String!
password: String!
}
input PermissionInput {
action: String!
subject: String!
@@ -520,6 +492,11 @@ type User {
updatedAt: String
}
type UserWithAcceptInvitationUrl {
user: User
acceptInvitationUrl: String
}
type Role {
id: String
name: String

View File

@@ -53,10 +53,7 @@ const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {
Mutation: {
'*': isAuthenticatedRule,
forgotPassword: allow,
login: allow,
registerUser: allow,
resetPassword: allow,
},
};

View File

@@ -1,43 +1,102 @@
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent';
import appConfig from '../config/app.js';
const config = axios.defaults;
const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;
const noProxyEnv = process.env.no_proxy;
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : [];
export function createInstance(customConfig = {}, { requestInterceptor, responseErrorInterceptor } = {}) {
const config = {
...axios.defaults,
...customConfig
};
const httpProxyUrl = appConfig.httpProxy;
const httpsProxyUrl = appConfig.httpsProxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;
const noProxyEnv = appConfig.noProxy;
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : [];
if (supportsProxy) {
if (httpProxyUrl) {
config.httpAgent = new HttpProxyAgent(httpProxyUrl);
if (supportsProxy) {
if (httpProxyUrl) {
config.httpAgent = new HttpProxyAgent(httpProxyUrl);
}
if (httpsProxyUrl) {
config.httpsAgent = new HttpsProxyAgent(httpsProxyUrl);
}
config.proxy = false;
}
if (httpsProxyUrl) {
config.httpsAgent = new HttpsProxyAgent(httpsProxyUrl);
const instance = axios.create(config);
function shouldSkipProxy(hostname) {
return noProxyHosts.some(noProxyHost => {
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
});
};
/**
* The interceptors are executed in the reverse order they are added.
*/
instance.interceptors.request.use(
function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.baseURL).hostname;
if (supportsProxy && shouldSkipProxy(hostname)) {
requestConfig.httpAgent = undefined;
requestConfig.httpsAgent = undefined;
}
return requestConfig;
},
(error) => Promise.reject(error)
);
// not always we have custom request interceptors
if (requestInterceptor) {
instance.interceptors.request.use(
async function customInterceptor(requestConfig) {
let newRequestConfig = requestConfig;
for (const interceptor of requestInterceptor) {
newRequestConfig = await interceptor(newRequestConfig);
}
return newRequestConfig;
}
);
}
config.proxy = false;
instance.interceptors.request.use(
function removeBaseUrlForAbsoluteUrls(requestConfig) {
/**
* If the URL is an absolute URL, we remove its origin out of the URL
* and set it as baseURL. This lets us streamlines the requests made by Automatisch
* and requests made by app integrations.
*/
try {
const url = new URL(requestConfig.url);
requestConfig.baseURL = url.origin;
requestConfig.url = url.pathname + url.search;
return requestConfig;
} catch (err) {
return requestConfig;
}
},
(error) => Promise.reject(error)
);
// not always we have custom response error interceptor
if (responseErrorInterceptor) {
instance.interceptors.response.use(
(response) => response,
responseErrorInterceptor
);
}
return instance;
}
const axiosWithProxyInstance = axios.create(config);
const defaultInstance = createInstance();
function shouldSkipProxy(hostname) {
return noProxyHosts.some(noProxyHost => {
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
});
};
axiosWithProxyInstance.interceptors.request.use(function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.url).hostname;
if (supportsProxy && shouldSkipProxy(hostname)) {
requestConfig.httpAgent = undefined;
requestConfig.httpsAgent = undefined;
}
return requestConfig;
});
export default axiosWithProxyInstance;
export default defaultInstance;

View File

@@ -0,0 +1,169 @@
import { beforeEach, describe, it, expect, vi } from 'vitest';
describe('Custom default axios with proxy', () => {
beforeEach(() => {
vi.resetModules();
});
it('should have two interceptors by default', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
const requestInterceptors = axios.interceptors.request.handlers;
expect(requestInterceptors.length).toBe(2);
});
it('should have default interceptors in a certain order', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
const requestInterceptors = axios.interceptors.request.handlers;
const firstRequestInterceptor = requestInterceptors[0];
const secondRequestInterceptor = requestInterceptors[1];
expect(firstRequestInterceptor.fulfilled.name).toBe('skipProxyIfInNoProxy');
expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls');
});
it('should throw with invalid url (consisting of path alone)', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
await expect(() => axios('/just-a-path')).rejects.toThrowError('Invalid URL');
});
describe('with skipProxyIfInNoProxy interceptor', () => {
let appConfig, axios;
beforeEach(async() => {
appConfig = (await import('../config/app.js')).default;
vi.spyOn(appConfig, 'httpProxy', 'get').mockReturnValue('http://proxy.automatisch.io');
vi.spyOn(appConfig, 'httpsProxy', 'get').mockReturnValue('http://proxy.automatisch.io');
vi.spyOn(appConfig, 'noProxy', 'get').mockReturnValue('name.tld,automatisch.io');
axios = (await import('./axios-with-proxy.js')).default;
});
it('should skip proxy for hosts in no_proxy environment variable', async () => {
const skipProxyIfInNoProxy = axios.interceptors.request.handlers[0].fulfilled;
const mockRequestConfig = {
...axios.defaults,
baseURL: 'https://automatisch.io'
};
const interceptedRequestConfig = skipProxyIfInNoProxy(mockRequestConfig);
expect(interceptedRequestConfig.httpAgent).toBeUndefined();
expect(interceptedRequestConfig.httpsAgent).toBeUndefined();
expect(interceptedRequestConfig.proxy).toBe(false);
});
it('should not skip proxy for hosts not in no_proxy environment variable', async () => {
const skipProxyIfInNoProxy = axios.interceptors.request.handlers[0].fulfilled;
const mockRequestConfig = {
...axios.defaults,
// beware the intentional typo!
baseURL: 'https://automatish.io'
};
const interceptedRequestConfig = skipProxyIfInNoProxy(mockRequestConfig);
expect(interceptedRequestConfig.httpAgent).toBeDefined();
expect(interceptedRequestConfig.httpsAgent).toBeDefined();
expect(interceptedRequestConfig.proxy).toBe(false);
});
});
describe('with removeBaseUrlForAbsoluteUrls interceptor', () => {
let axios;
beforeEach(async() => {
axios = (await import('./axios-with-proxy.js')).default;
});
it('should trim the baseUrl from absolute urls', async () => {
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
const mockRequestConfig = {
...axios.defaults,
url: 'https://automatisch.io/path'
};
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
expect(interceptedRequestConfig.url).toBe('/path');
});
it('should not mutate separate baseURL and urls', async () => {
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
const mockRequestConfig = {
...axios.defaults,
baseURL: 'https://automatisch.io',
url: '/path?query=1'
};
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
expect(interceptedRequestConfig.url).toBe('/path?query=1');
});
it('should not strip querystring from url', async () => {
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
const mockRequestConfig = {
...axios.defaults,
url: 'https://automatisch.io/path?query=1'
};
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
expect(interceptedRequestConfig.url).toBe('/path?query=1');
});
});
describe('with extra requestInterceptors', () => {
it('should apply extra request interceptors in the middle', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.test = true;
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
const requestInterceptors = instance.interceptors.request.handlers;
const customInterceptor = requestInterceptors[1].fulfilled;
expect(requestInterceptors.length).toBe(3);
await expect(customInterceptor({})).resolves.toStrictEqual({ test: true });
});
it('should work with a custom interceptor setting a baseURL and a request to path', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.baseURL = 'http://localhost';
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
try {
await instance.get('/just-a-path');
} catch (error) {
expect(error.config.baseURL).toBe('http://localhost');
expect(error.config.url).toBe('/just-a-path');
}
})
});
});

View File

@@ -6,7 +6,7 @@ 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`);
const filePath = path.join(__dirname, `../views/emails/${emailPath}.hbs`);
const source = fs.readFileSync(filePath, 'utf-8').toString();
const template = handlebars.compile(source);
return template(replacements);

View File

@@ -98,9 +98,9 @@ const globalVariable = async (options) => {
});
return {
key: datastore.key,
value: datastore.value,
[datastore.key]: datastore.value,
key: key,
value: datastore?.value ?? null,
[key]: datastore?.value ?? null,
};
},
set: async ({ key, value }) => {

View File

@@ -1,68 +1,43 @@
import { URL } from 'node:url';
import HttpError from '../../errors/http.js';
import axios from '../axios-with-proxy.js';
const removeBaseUrlForAbsoluteUrls = (requestConfig) => {
try {
const url = new URL(requestConfig.url);
requestConfig.baseURL = url.origin;
requestConfig.url = url.pathname + url.search;
return requestConfig;
} catch {
return requestConfig;
}
};
import { createInstance } from '../axios-with-proxy.js';
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
const instance = axios.create({
baseURL,
});
async function interceptResponseError(error) {
const { config, response } = error;
// Do not destructure `status` from `error.response` because it might not exist
const status = response?.status;
instance.interceptors.request.use((requestConfig) => {
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
if (
// TODO: provide a `shouldRefreshToken` function in the app
(status === 401 || status === 403) &&
$.app.auth &&
$.app.auth.refreshToken &&
!$.app.auth.isRefreshTokenRequested
) {
$.app.auth.isRefreshTokenRequested = true;
await $.app.auth.refreshToken($);
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
return beforeRequestFunc($, newConfig);
}, newRequestConfig);
// retry the previous request before the expired token error
const newResponse = await instance.request(config);
$.app.auth.isRefreshTokenRequested = false;
/**
* axios seems to want InternalAxiosRequestConfig returned not AxioRequestConfig
* anymore even though requests do require AxiosRequestConfig.
*
* Since both interfaces are very similar (InternalAxiosRequestConfig
* extends AxiosRequestConfig), we can utilize an assertion below
**/
return result;
});
instance.interceptors.response.use(
(response) => response,
async (error) => {
const { config, response } = error;
// Do not destructure `status` from `error.response` because it might not exist
const status = response?.status;
if (
// TODO: provide a `shouldRefreshToken` function in the app
(status === 401 || status === 403) &&
$.app.auth &&
$.app.auth.refreshToken &&
!$.app.auth.isRefreshTokenRequested
) {
$.app.auth.isRefreshTokenRequested = true;
await $.app.auth.refreshToken($);
// retry the previous request before the expired token error
const newResponse = await instance.request(config);
$.app.auth.isRefreshTokenRequested = false;
return newResponse;
}
throw new HttpError(error);
return newResponse;
}
);
throw new HttpError(error);
};
const instance = createInstance(
{
baseURL,
},
{
requestInterceptor: beforeRequest.map((originalBeforeRequest) => {
return async (requestConfig) => await originalBeforeRequest($, requestConfig);
}),
responseErrorInterceptor: interceptResponseError,
}
)
return instance;
}

View File

@@ -63,6 +63,8 @@ export default async (flowId, request, response) => {
});
if (testRun) {
response.status(204).end();
// in case of testing, we do not process the whole process.
continue;
}
@@ -74,6 +76,12 @@ export default async (flowId, request, response) => {
executionId,
});
if (actionStep.appKey === 'filter' && !actionExecutionStep.dataOut) {
response.status(422).end();
break;
}
if (actionStep.key === 'respondWith' && !response.headersSent) {
const { headers, statusCode, body } = actionExecutionStep.dataOut;

View File

@@ -1,5 +1,5 @@
import bcrypt from 'bcrypt';
import { DateTime } from 'luxon';
import { DateTime, Duration } from 'luxon';
import crypto from 'node:crypto';
import appConfig from '../config/app.js';
@@ -21,6 +21,13 @@ import Subscription from './subscription.ee.js';
import UsageData from './usage-data.ee.js';
import Billing from '../helpers/billing/index.ee.js';
import deleteUserQueue from '../queues/delete-user.ee.js';
import emailQueue from '../queues/email.js';
import {
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../helpers/remove-job-configuration.js';
class User extends Base {
static tableName = 'users';
@@ -33,8 +40,21 @@ class User extends Base {
fullName: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
password: { type: 'string' },
resetPasswordToken: { type: 'string' },
resetPasswordTokenSentAt: { type: 'string' },
status: {
type: 'string',
enum: ['active', 'invited'],
default: 'active',
},
resetPasswordToken: { type: ['string', 'null'] },
resetPasswordTokenSentAt: {
type: ['string', 'null'],
format: 'date-time',
},
invitationToken: { type: ['string', 'null'] },
invitationTokenSentAt: {
type: ['string', 'null'],
format: 'date-time',
},
trialExpiryDate: { type: 'string' },
roleId: { type: 'string', format: 'uuid' },
deletedAt: { type: 'string' },
@@ -202,6 +222,13 @@ class User extends Base {
await this.$query().patch({ resetPasswordToken, resetPasswordTokenSentAt });
}
async generateInvitationToken() {
const invitationToken = crypto.randomBytes(64).toString('hex');
const invitationTokenSentAt = new Date().toISOString();
await this.$query().patch({ invitationToken, invitationTokenSentAt });
}
async resetPassword(password) {
return await this.$query().patch({
resetPasswordToken: null,
@@ -210,7 +237,53 @@ class User extends Base {
});
}
async isResetPasswordTokenValid() {
async acceptInvitation(password) {
return await this.$query().patch({
invitationToken: null,
invitationTokenSentAt: null,
status: 'active',
password,
});
}
async softRemove() {
await this.$query().delete();
const jobName = `Delete user - ${this.id}`;
const jobPayload = { id: this.id };
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobOptions = {
delay: millisecondsFor30Days,
};
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
}
async sendResetPasswordEmail() {
await this.generateResetPasswordToken();
const jobName = `Reset Password Email - ${this.id}`;
const jobPayload = {
email: this.email,
subject: 'Reset Password',
template: 'reset-password-instructions.ee',
params: {
token: this.resetPasswordToken,
webAppUrl: appConfig.webAppUrl,
fullName: this.fullName,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
await emailQueue.add(jobName, jobPayload, jobOptions);
}
isResetPasswordTokenValid() {
if (!this.resetPasswordTokenSentAt) {
return false;
}
@@ -222,6 +295,18 @@ class User extends Base {
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
}
isInvitationTokenValid() {
if (!this.invitationTokenSentAt) {
return false;
}
const sentAt = new Date(this.invitationTokenSentAt);
const now = new Date();
const seventyTwoHoursInMilliseconds = 1000 * 60 * 60 * 72;
return now.getTime() - sentAt.getTime() < seventyTwoHoursInMilliseconds;
}
async generateHash() {
if (this.password) {
this.password = await bcrypt.hash(this.password, 10);
@@ -381,7 +466,7 @@ class User extends Base {
email,
password,
fullName,
roleId: adminRole.id
roleId: adminRole.id,
});
await Config.markInstallationCompleted();

View File

@@ -4,6 +4,7 @@ import { authenticateUser } from '../../../../helpers/authentication.js';
import { authorizeAdmin } from '../../../../helpers/authorization.js';
import getUsersAction from '../../../../controllers/api/v1/admin/users/get-users.ee.js';
import getUserAction from '../../../../controllers/api/v1/admin/users/get-user.ee.js';
import deleteUserAction from '../../../../controllers/api/v1/admin/users/delete-user.js';
const router = Router();
@@ -16,4 +17,11 @@ router.get(
asyncHandler(getUserAction)
);
router.delete(
'/:userId',
authenticateUser,
authorizeAdmin,
asyncHandler(deleteUserAction)
);
export default router;

View File

@@ -9,6 +9,9 @@ import getAppsAction from '../../../controllers/api/v1/users/get-apps.js';
import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.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 acceptInvitationAction from '../../../controllers/api/v1/users/accept-invitation.js';
import forgotPasswordAction from '../../../controllers/api/v1/users/forgot-password.js';
import resetPasswordAction from '../../../controllers/api/v1/users/reset-password.js';
const router = Router();
@@ -49,4 +52,9 @@ router.get(
asyncHandler(getPlanAndUsageAction)
);
router.post('/invitation', asyncHandler(acceptInvitationAction));
router.post('/forgot-password', asyncHandler(forgotPasswordAction));
router.post('/reset-password', asyncHandler(resetPasswordAction));
export default router;

View File

@@ -8,6 +8,7 @@ const userSerializer = (user) => {
email: user.email,
createdAt: user.createdAt.getTime(),
updatedAt: user.updatedAt.getTime(),
status: user.status,
fullName: user.fullName,
};

View File

@@ -35,6 +35,7 @@ describe('userSerializer', () => {
email: user.email,
fullName: user.fullName,
id: user.id,
status: user.status,
updatedAt: user.updatedAt.getTime(),
};

View File

@@ -0,0 +1,23 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Invitation instructions</title>
</head>
<body>
<p>
Hello {{ fullName }},
</p>
<p>
You have been invited to join our platform. To accept the invitation, click the link below.
</p>
<p>
<a href="{{ acceptInvitationUrl }}">Accept invitation</a>
</p>
<p>
If you did not expect this invitation, you can ignore this email.
</p>
</body>
</html>

View File

@@ -9,7 +9,7 @@
</p>
<p>
Someone has requested a link to change your password, and you can do this through the link below.
Someone has requested a link to change your password, and you can do this through the link below within 72 hours.
</p>
<p>

View File

@@ -40,6 +40,7 @@ export const worker = new Worker(
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
}
await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete();
await user.$query().withSoftDeleted().hardDelete();
},
{ connection: redisConfig }

View File

@@ -21,7 +21,7 @@ export const worker = new Worker(
async (job) => {
const { email, subject, template, params } = job.data;
if (isCloudSandbox && !isAutomatischEmail(email)) {
if (isCloudSandbox() && !isAutomatischEmail(email)) {
logger.info(
'Only Automatisch emails are allowed for non-production environments!'
);

View File

@@ -14,6 +14,7 @@ const getUserMock = (currentUser, role) => {
name: role.name,
updatedAt: role.updatedAt.getTime(),
},
status: currentUser.status,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.getTime(),
},

View File

@@ -18,6 +18,7 @@ const getUsersMock = async (users, roles) => {
updatedAt: role.updatedAt.getTime(),
}
: null,
status: user.status,
trialExpiryDate: user.trialExpiryDate.toISOString(),
updatedAt: user.updatedAt.getTime(),
};

View File

@@ -4,6 +4,7 @@ const infoMock = () => {
isCloud: false,
isMation: false,
isEnterprise: true,
docsUrl: 'https://automatisch.io/docs',
},
meta: {
count: 1,

View File

@@ -23,6 +23,7 @@ const getCurrentUserMock = (currentUser, role, permissions) => {
name: role.name,
updatedAt: role.updatedAt.getTime(),
},
status: currentUser.status,
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
updatedAt: currentUser.updatedAt.getTime(),
},

View File

@@ -59,6 +59,15 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/carbone/connection' },
],
},
{
text: 'Cryptography',
collapsible: true,
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/cryptography/actions' },
{ text: 'Connection', link: '/apps/cryptography/connection' },
],
},
{
text: 'Datastore',
collapsible: true,
@@ -113,16 +122,6 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/dropbox/connection' },
],
},
{
text: 'Eventbrite',
collapsible: true,
collapsed: true,
items: [
{ text: 'Triggers', link: '/apps/eventbrite/triggers' },
{ text: 'Actions', link: '/apps/eventbrite/actions' },
{ text: 'Connection', link: '/apps/eventbrite/connection' },
],
},
{
text: 'Filter',
collapsible: true,

View File

@@ -14,7 +14,7 @@ connection in Automatisch. If any of the steps are outdated, please let us know!
7. Click on the **Select all** and then click on the **Create** button.
8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch.
9. Write any screen name to be displayed in Automatisch.
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatsich.
11. If you are using self-hosted Appwrite project, you can paste the instace url into **Appwrite instance URL** field in Automatisch.
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatisch.
11. If you are using self-hosted Appwrite project, you can paste the instance url into **Appwrite instance URL** field in Automatisch.
12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL.
13. Start using Appwrite integration with Automatisch!

View File

@@ -1,7 +1,7 @@
---
favicon: /favicons/appwrite.svg
items:
- name: New documets
- name: New documents
desc: Triggers when a new document is created.
---

View File

@@ -0,0 +1,14 @@
---
favicon: /favicons/cryptography.svg
items:
- name: Create HMAC
desc: Create a Hash-based Message Authentication Code (HMAC) using the specified algorithm, secret key, and message.
- name: Create Signature
desc: Create a digital signature using the specified algorithm, secret key, and message.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -0,0 +1,3 @@
# Cryptography
Cryptography is a built-in app shipped with Automatisch, allowing you to perform cryptographic operations without needing to connect to any external services.

View File

@@ -1,14 +0,0 @@
---
favicon: /favicons/eventbrite.svg
items:
- name: Create event
desc: Creates a new event.
- name: Publish event
desc: Publishes a draft event.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -1,18 +0,0 @@
# Eventbrite
:::info
This page explains the steps you need to follow to set up the Eventbrite
connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Go to your Eventbrite account settings.
2. Click on the **Developer Links**, and click on the **API Keys** button.
3. Click on the **Create API Key** button.
4. Fill the form.
5. Copy **OAuth Redirect URL** from Automatisch to **OAuth Redirect URI** field in the form.
6. After filling the form, click on the **Create Key** button.
7. Click on the **Show API key, client secret and tokens** in the middle of the page.
8. Copy the **API Key** value to the `API Key` field on Automatisch.
9. Copy the **Client secret** value to the `Client Secret` field on Automatisch.
10. Click **Submit** button on Automatisch.
11. Congrats! Start using your new Eventbrite connection within the flows.

View File

@@ -1,18 +0,0 @@
---
favicon: /favicons/eventbrite.svg
items:
- name: New attendee check in
desc: Triggers when an attendee's barcode is scanned in.
- name: New attendee registered
desc: Triggers when an attendee orders a ticket for an event.
- name: New events
desc: Triggers when a new event is published and live within an organization.
- name: Updated Attendee
desc: Triggers when attendee data is updated.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -5,6 +5,8 @@ items:
desc: Creates an attachment of a specified object by given parent ID.
- name: Find record
desc: Finds a record of a specified object by a field and value.
- name: Find partially matching record
desc: Finds a record of a specified object by a field containing a value.
- name: Execute query
desc: Executes a SOQL query in Salesforce.
---

View File

@@ -6,16 +6,12 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
.
├── packages
│   ├── backend
│   ├── cli
│   ├── docs
│   ├── e2e-tests
│   ├── types
│   └── web
```
- `backend` - The backend package contains the backend application and all integrations.
- `cli` - The cli package contains the CLI application of Automatisch.
- `docs` - The docs package contains the documentation website.
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
- `types` - The types package contains the shared types for both the backend and web packages.
- `web` - The web package contains the frontend application of Automatisch.

View File

@@ -11,7 +11,6 @@ The following integrations are currently supported by Automatisch.
- [Discord](/apps/discord/actions)
- [Disqus](/apps/disqus/triggers)
- [Dropbox](/apps/dropbox/actions)
- [Eventbrite](/apps/eventbrite/triggers)
- [Filter](/apps/filter/actions)
- [Flickr](/apps/flickr/triggers)
- [Formatter](/apps/formatter/actions)

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100pt" height="100pt" version="1.1" viewBox="0 0 100 100">
<path d="m66.012 33h-3.0117v-11c0-7.1719-5.8281-13-13-13s-13 5.8281-13 13v11h-3.0117c-2.75 0-4.9883 2.2383-4.9883 4.9883v28.012c0 2.75 2.2383 4.9883 4.9883 4.9883h32.012c2.75 0 4.9883-2.2383 4.9883-4.9883v-28.012c0.011719-2.75-2.2266-4.9883-4.9766-4.9883zm-27.012-11c0-6.0703 4.9297-11 11-11s11 4.9297 11 11v11h-22zm30 44.012c0 1.6484-1.3398 2.9883-2.9883 2.9883h-32.023c-1.6484 0-2.9883-1.3398-2.9883-2.9883v-28.023c0-1.6484 1.3398-2.9883 2.9883-2.9883h32.023c1.6484 0 2.9883 1.3398 2.9883 2.9883zm-18 9.9883v14c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-14c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm20 8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm-32-8v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1zm-14-26c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm0-12c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v7h7c0.55078 0 1 0.44922 1 1zm0 24c0 0.55078-0.44922 1-1 1h-7v7c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-8c0-0.55078 0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm66-12c0 0.55078-0.44922 1-1 1h-14c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h14c0.55078 0 1 0.44922 1 1zm-16-12c0-0.55078 0.44922-1 1-1h7v-7c0-0.55078 0.44922-1 1-1s1 0.44922 1 1v8c0 0.55078-0.44922 1-1 1h-8c-0.55078 0-1-0.44922-1-1zm10 24v8c0 0.55078-0.44922 1-1 1s-1-0.44922-1-1v-7h-7c-0.55078 0-1-0.44922-1-1s0.44922-1 1-1h8c0.55078 0 1 0.44922 1 1zm-35-17c-2.7617 0-5 2.2383-5 5 0 2.4102 1.7188 4.4297 4 4.8984v5.1016c0 0.55078 0.44922 1 1 1s1-0.44922 1-1v-5.1016c2.2812-0.46094 4-2.4805 4-4.8984 0-2.7617-2.2383-5-5-5zm0 8c-1.6484 0-3-1.3516-3-3s1.3516-3 3-3 3 1.3516 3 3-1.3516 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<circle fill="#F05537" cx="128" cy="128" r="128"></circle>
<path d="M117.475323,82.7290398 C136.772428,78.4407943 156.069532,86.3025777 166.790146,101.311437 L81.5017079,120.608542 C84.3605382,102.26438 98.1782181,87.0172853 117.475323,82.7290398 Z M167.266618,153.48509 C160.596014,163.252761 150.351872,170.161601 138.678314,172.782195 C119.38121,177.070441 99.8458692,169.208657 89.1252554,153.961562 L174.651929,134.664457 L188.469609,131.567391 L215.152026,125.611495 C214.91379,119.893834 214.199082,114.176173 213.007903,108.696749 C202.287289,62.7172275 155.354825,33.8906884 108.42236,44.6113021 C61.4898956,55.3319159 32.1868848,101.073201 43.1457344,147.290958 C54.1045839,193.508715 100.798813,222.097018 147.731277,211.376404 C175.366637,205.182272 196.807864,186.599875 207.766714,163.014525 L167.266618,153.48509 L167.266618,153.48509 Z" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
POSTGRES_DB=automatisch
POSTGRES_USER=automatisch_user
POSTGRES_PASSWORD=automatisch_password
POSTGRES_PORT=5432
POSTGRES_HOST=localhost

View File

@@ -0,0 +1,6 @@
node_modules
build
.eslintrc.js
playwright-report/*

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