Compare commits

..

3 Commits

Author SHA1 Message Date
Rıdvan Akca
6062cfafaf feat(eventbrite): add new attendee check in trigger 2024-06-11 12:18:00 +02:00
Rıdvan Akca
5263e774d2 feat(eventbrite): add new events trigger 2024-06-11 11:31:28 +02:00
Rıdvan Akca
22b4a04567 feat(eventbrite): add eventbrite integration 2024-06-11 10:44:29 +02:00
229 changed files with 1403 additions and 3503 deletions

View File

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

View File

@@ -1,64 +0,0 @@
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

@@ -1,65 +0,0 @@
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

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

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,14 +0,0 @@
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

@@ -0,0 +1,7 @@
<?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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,19 @@
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

@@ -0,0 +1,46 @@
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

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

View File

@@ -0,0 +1,42 @@
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

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
export default {
name: 'List events',
key: 'listEvents',
async run($) {
const events = {
data: [],
};
const organizationId = $.step.parameters.organizationId;
if (!organizationId) {
return events;
}
const params = {
continuation: undefined,
order_by: 'created_desc',
};
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

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,20 @@
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';
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,
});

View File

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

View File

@@ -0,0 +1,120 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,8 @@
const replace = ($) => { const replace = ($) => {
const input = $.step.parameters.input; const input = $.step.parameters.input;
const find = $.step.parameters.find; const find = $.step.parameters.find;
const replace = $.step.parameters.replace; 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); return input.replaceAll(find, replace);
}; };

View File

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

View File

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

View File

@@ -1,12 +0,0 @@
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,33 +23,6 @@ const replace = [
description: 'Text that will replace the found text.', description: 'Text that will replace the found text.',
variables: true, 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; export default replace;

View File

@@ -1,101 +0,0 @@
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,6 +1,5 @@
import createAttachment from './create-attachment/index.js'; import createAttachment from './create-attachment/index.js';
import executeQuery from './execute-query/index.js'; import executeQuery from './execute-query/index.js';
import findRecord from './find-record/index.js'; import findRecord from './find-record/index.js';
import findPartiallyMatchingRecord from './find-partially-matching-record/index.js';
export default [findRecord, findPartiallyMatchingRecord, createAttachment, executeQuery]; export default [findRecord, createAttachment, executeQuery];

View File

@@ -97,12 +97,8 @@ const appConfig = {
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true', disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true', disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK, additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkIcon: process.env.ADDITIONAL_DRAWER_LINK_ICON,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT, additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
disableSeedUser: process.env.DISABLE_SEED_USER === 'true', 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) { if (!appConfig.encryptionKey) {

View File

@@ -1,10 +0,0 @@
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

@@ -1,43 +0,0 @@
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,7 +7,6 @@ export default async (request, response) => {
disableNotificationsPage: appConfig.disableNotificationsPage, disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon, disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink, additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkIcon: appConfig.additionalDrawerLinkIcon,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText, additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
}; };

View File

@@ -4,7 +4,6 @@ import { createConfig } from '../../../../../test/factories/config.js';
import app from '../../../../app.js'; import app from '../../../../app.js';
import configMock from '../../../../../test/mocks/rest/api/v1/automatisch/config.js'; import configMock from '../../../../../test/mocks/rest/api/v1/automatisch/config.js';
import * as license from '../../../../helpers/license.ee.js'; import * as license from '../../../../helpers/license.ee.js';
import appConfig from '../../../../config/app.js';
describe('GET /api/v1/automatisch/config', () => { describe('GET /api/v1/automatisch/config', () => {
it('should return Automatisch config', async () => { it('should return Automatisch config', async () => {
@@ -49,18 +48,4 @@ describe('GET /api/v1/automatisch/config', () => {
expect(response.body).toEqual(expectedPayload); 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,7 +7,6 @@ export default async (request, response) => {
isCloud: appConfig.isCloud, isCloud: appConfig.isCloud,
isMation: appConfig.isMation, isMation: appConfig.isMation,
isEnterprise: await hasValidLicense(), isEnterprise: await hasValidLicense(),
docsUrl: appConfig.docsUrl,
}; };
renderObject(response, info); renderObject(response, info);

View File

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

View File

@@ -1,21 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,23 +0,0 @@
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

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

View File

@@ -1,11 +0,0 @@
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

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

View File

@@ -1,16 +1,10 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js'; import User from '../../models/user.js';
import Role from '../../models/role.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) => { const createUser = async (_parent, params, context) => {
context.currentUser.can('create', 'User'); context.currentUser.can('create', 'User');
const { fullName, email } = params.input; const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({ const existingUser = await User.query().findOne({
email: email.toLowerCase(), email: email.toLowerCase(),
@@ -23,7 +17,7 @@ const createUser = async (_parent, params, context) => {
const userPayload = { const userPayload = {
fullName, fullName,
email, email,
status: 'invited', password,
}; };
try { try {
@@ -38,29 +32,7 @@ const createUser = async (_parent, params, context) => {
const user = await User.query().insert(userPayload); const user = await User.query().insert(userPayload);
await user.generateInvitationToken(); return user;
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; export default createUser;

View File

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,43 @@
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

@@ -0,0 +1,17 @@
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

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

View File

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

View File

@@ -1,13 +1,12 @@
import axios from 'axios'; import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
import appConfig from '../config/app.js';
const config = axios.defaults; const config = axios.defaults;
const httpProxyUrl = appConfig.httpProxy; const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = appConfig.httpsProxy; const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl; const supportsProxy = httpProxyUrl || httpsProxyUrl;
const noProxyEnv = appConfig.noProxy; const noProxyEnv = process.env.no_proxy;
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : []; const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : [];
if (supportsProxy) { if (supportsProxy) {
@@ -30,12 +29,8 @@ function shouldSkipProxy(hostname) {
}); });
}; };
/** axiosWithProxyInstance.interceptors.request.use(function skipProxyIfInNoProxy(requestConfig) {
* The interceptors are executed in the reverse order they are added. const hostname = new URL(requestConfig.url).hostname;
*/
axiosWithProxyInstance.interceptors.request.use(
function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.baseURL).hostname;
if (supportsProxy && shouldSkipProxy(hostname)) { if (supportsProxy && shouldSkipProxy(hostname)) {
requestConfig.httpAgent = undefined; requestConfig.httpAgent = undefined;
@@ -43,30 +38,6 @@ axiosWithProxyInstance.interceptors.request.use(
} }
return requestConfig; return requestConfig;
}, });
undefined,
{ synchronous: true }
);
axiosWithProxyInstance.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 {
return requestConfig;
}
},
undefined,
{ synchronous: true}
);
export default axiosWithProxyInstance; export default axiosWithProxyInstance;

View File

@@ -1,119 +0,0 @@
import { beforeEach, describe, it, expect, vi } from 'vitest';
describe('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');
});
describe('skipProxyIfInNoProxy', () => {
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('removeBaseUrlForAbsoluteUrls', () => {
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');
});
});
});

View File

@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compileEmail = (emailPath, replacements = {}) => { const compileEmail = (emailPath, replacements = {}) => {
const filePath = path.join(__dirname, `../views/emails/${emailPath}.hbs`); const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);
const source = fs.readFileSync(filePath, 'utf-8').toString(); const source = fs.readFileSync(filePath, 'utf-8').toString();
const template = handlebars.compile(source); const template = handlebars.compile(source);
return template(replacements); return template(replacements);

View File

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

View File

@@ -1,38 +1,41 @@
import { URL } from 'node:url';
import HttpError from '../../errors/http.js'; import HttpError from '../../errors/http.js';
import axios from '../axios-with-proxy.js'; import axios from '../axios-with-proxy.js';
// Mutates the `toInstance` by copying the request interceptors from `fromInstance` const removeBaseUrlForAbsoluteUrls = (requestConfig) => {
const copyRequestInterceptors = (fromInstance, toInstance) => { try {
// Copy request interceptors const url = new URL(requestConfig.url);
fromInstance.interceptors.request.forEach(interceptor => { requestConfig.baseURL = url.origin;
toInstance.interceptors.request.use( requestConfig.url = url.pathname + url.search;
interceptor.fulfilled,
interceptor.rejected, return requestConfig;
{ } catch {
synchronous: interceptor.synchronous, return requestConfig;
runWhen: interceptor.runWhen
}
);
});
} }
};
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) { export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
const instance = axios.create({ const instance = axios.create({
baseURL, baseURL,
}); });
// 1. apply the beforeRequest functions from the app
instance.interceptors.request.use((requestConfig) => { instance.interceptors.request.use((requestConfig) => {
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => { const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
return beforeRequestFunc($, newConfig); return beforeRequestFunc($, newConfig);
}, requestConfig); }, newRequestConfig);
/**
* 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; return result;
}); });
// 2. inherit the request inceptors from the parent instance
copyRequestInterceptors(axios, instance);
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => response, (response) => response,
async (error) => { async (error) => {

View File

@@ -1,5 +1,5 @@
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { DateTime, Duration } from 'luxon'; import { DateTime } from 'luxon';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
@@ -21,13 +21,6 @@ import Subscription from './subscription.ee.js';
import UsageData from './usage-data.ee.js'; import UsageData from './usage-data.ee.js';
import Billing from '../helpers/billing/index.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 { class User extends Base {
static tableName = 'users'; static tableName = 'users';
@@ -40,21 +33,8 @@ class User extends Base {
fullName: { type: 'string', minLength: 1 }, fullName: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
password: { type: 'string' }, password: { type: 'string' },
status: { resetPasswordToken: { type: 'string' },
type: 'string', resetPasswordTokenSentAt: { 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' }, trialExpiryDate: { type: 'string' },
roleId: { type: 'string', format: 'uuid' }, roleId: { type: 'string', format: 'uuid' },
deletedAt: { type: 'string' }, deletedAt: { type: 'string' },
@@ -222,13 +202,6 @@ class User extends Base {
await this.$query().patch({ resetPasswordToken, resetPasswordTokenSentAt }); 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) { async resetPassword(password) {
return await this.$query().patch({ return await this.$query().patch({
resetPasswordToken: null, resetPasswordToken: null,
@@ -237,53 +210,7 @@ class User extends Base {
}); });
} }
async acceptInvitation(password) { async isResetPasswordTokenValid() {
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) { if (!this.resetPasswordTokenSentAt) {
return false; return false;
} }
@@ -295,18 +222,6 @@ class User extends Base {
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds; 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() { async generateHash() {
if (this.password) { if (this.password) {
this.password = await bcrypt.hash(this.password, 10); this.password = await bcrypt.hash(this.password, 10);
@@ -466,7 +381,7 @@ class User extends Base {
email, email,
password, password,
fullName, fullName,
roleId: adminRole.id, roleId: adminRole.id
}); });
await Config.markInstallationCompleted(); await Config.markInstallationCompleted();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
<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>
<p> <p>
Someone has requested a link to change your password, and you can do this through the link below within 72 hours. Someone has requested a link to change your password, and you can do this through the link below.
</p> </p>
<p> <p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,15 +59,6 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/carbone/connection' }, { 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', text: 'Datastore',
collapsible: true, collapsible: true,
@@ -122,6 +113,15 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/dropbox/connection' }, { text: 'Connection', link: '/apps/dropbox/connection' },
], ],
}, },
{
text: 'Eventbrite',
collapsible: true,
collapsed: true,
items: [
{ text: 'Triggers', link: '/apps/eventbrite/triggers' },
{ text: 'Connection', link: '/apps/eventbrite/connection' },
],
},
{ {
text: 'Filter', text: 'Filter',
collapsible: true, collapsible: true,

View File

@@ -1,14 +0,0 @@
---
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

@@ -1,3 +0,0 @@
# 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

@@ -0,0 +1,18 @@
# 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

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

View File

@@ -5,8 +5,6 @@ items:
desc: Creates an attachment of a specified object by given parent ID. desc: Creates an attachment of a specified object by given parent ID.
- name: Find record - name: Find record
desc: Finds a record of a specified object by a field and value. 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 - name: Execute query
desc: Executes a SOQL query in Salesforce. desc: Executes a SOQL query in Salesforce.
--- ---

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,7 @@
<?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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
{
"root": true,
"env": {
"node": true,
"es6": true
},
"extends": [
"eslint:recommended",
"prettier"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"semi": [
2,
"always"
],
"indent": [
"error",
2
]
}
}

View File

@@ -44,14 +44,6 @@ and it should install the associated browsers for the test running. For more inf
We recommend using [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) maintained by Microsoft. This lets you run playwright tests from within the code editor, giving you access to additional tools, such as easily running subsets of tests. We recommend using [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) maintained by Microsoft. This lets you run playwright tests from within the code editor, giving you access to additional tools, such as easily running subsets of tests.
[Global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown) are part of the tests.
By running `yarn test` setup and teardown actions will take place.
If you need to setup Admin account (if you didn't seed the DB with the admin account or have clean DB) you should run `auth.setup.js` file.
If you want to clean the database (drop tables) and perform required migrations run `global.teardown.js`.
# Test failures # Test failures
If there are failing tests in the test suite, this can be caused by a myriad of reasons, but one of the best places to start is either running the test in a headed browser, looking at the associated trace file for the failed test, or checking out the output of a failed GitHub Action. If there are failing tests in the test suite, this can be caused by a myriad of reasons, but one of the best places to start is either running the test in a headed browser, looking at the associated trace file for the failed test, or checking out the output of a failed GitHub Action.

View File

@@ -1,46 +0,0 @@
const { expect } = require('@playwright/test');
const { BasePage } = require('./base-page');
export class AcceptInvitation extends BasePage {
path = '/accept-invitation';
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
super(page);
this.page = page;
this.passwordTextField = this.page.getByTestId('password-text-field');
this.passwordConfirmationTextField = this.page.getByTestId('confirm-password-text-field');
this.submitButton = this.page.getByTestId('submit-button');
this.pageTitle = this.page.getByTestId('accept-invitation-form-title');
this.formErrorMessage = this.page.getByTestId('accept-invitation-form-error');
}
async open(token) {
return await this.page.goto(`${this.path}?token=${token}`);
}
async acceptInvitation(
password
) {
await this.passwordTextField.fill(password);
await this.passwordConfirmationTextField.fill(password);
await this.submitButton.click();
}
async fillPasswordField(password) {
await this.passwordTextField.fill(password);
await this.passwordConfirmationTextField.fill(password);
}
async excpectSubmitButtonToBeDisabled() {
await expect(this.submitButton).toBeDisabled();
}
async expectAlertToBeVisible() {
await expect(this.formErrorMessage).toBeVisible();
}
}

View File

@@ -1,75 +0,0 @@
import { BasePage } from "./base-page";
const { faker } = require('@faker-js/faker');
const { expect } = require('@playwright/test');
export class AdminSetupPage extends BasePage {
path = '/installation';
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
super(page);
this.fullNameTextField = this.page.getByTestId('fullName-text-field');
this.emailTextField = this.page.getByTestId('email-text-field');
this.passwordTextField = this.page.getByTestId('password-text-field');
this.repeatPasswordTextField = this.page.getByTestId('repeat-password-text-field');
this.createAdminButton = this.page.getByTestId('signUp-button');
this.invalidFields = this.page.locator('p.Mui-error');
this.successAlert = this.page.getByTestId('success-alert');
}
async open() {
return await this.page.goto(this.path);
}
async fillValidUserData() {
await this.fullNameTextField.fill(process.env.LOGIN_EMAIL);
await this.emailTextField.fill(process.env.LOGIN_EMAIL);
await this.passwordTextField.fill(process.env.LOGIN_PASSWORD);
await this.repeatPasswordTextField.fill(process.env.LOGIN_PASSWORD);
}
async fillInvalidUserData() {
await this.fullNameTextField.fill('');
await this.emailTextField.fill('abcde');
await this.passwordTextField.fill('');
await this.repeatPasswordTextField.fill('a');
}
async fillNotMatchingPasswordUserData() {
const testUser = this.generateUser();
await this.fullNameTextField.fill(testUser.fullName);
await this.emailTextField.fill(testUser.email);
await this.passwordTextField.fill(testUser.password);
await this.repeatPasswordTextField.fill(testUser.wronglyRepeatedPassword);
}
async submitAdminForm() {
await this.createAdminButton.click();
}
async expectInvalidFields(errorCount) {
await expect(await this.invalidFields.all()).toHaveLength(errorCount);
}
async expectSuccessAlertToBeVisible() {
await expect(await this.successAlert).toBeVisible();
}
async expectSuccessMessageToContainLoginLink() {
await expect(await this.successAlert.locator('a')).toHaveAttribute('href', '/login');
}
generateUser() {
faker.seed(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER));
return {
fullName: faker.person.fullName(),
email: faker.internet.email(),
password: faker.internet.password(),
wronglyRepeatedPassword: faker.internet.password()
};
}
};

View File

@@ -11,11 +11,10 @@ export class AdminCreateUserPage extends AuthenticatedPage {
super(page); super(page);
this.fullNameInput = page.getByTestId('full-name-input'); this.fullNameInput = page.getByTestId('full-name-input');
this.emailInput = page.getByTestId('email-input'); this.emailInput = page.getByTestId('email-input');
this.passwordInput = page.getByTestId('password-input');
this.roleInput = page.getByTestId('role.id-autocomplete'); this.roleInput = page.getByTestId('role.id-autocomplete');
this.createButton = page.getByTestId('create-button'); this.createButton = page.getByTestId('create-button');
this.pageTitle = page.getByTestId('create-user-title'); this.pageTitle = page.getByTestId('create-user-title');
this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert');
this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link');
} }
seed(seed) { seed(seed) {
@@ -26,6 +25,7 @@ export class AdminCreateUserPage extends AuthenticatedPage {
return { return {
fullName: faker.person.fullName(), fullName: faker.person.fullName(),
email: faker.internet.email().toLowerCase(), email: faker.internet.email().toLowerCase(),
password: faker.internet.password(),
}; };
} }
} }

View File

@@ -14,6 +14,6 @@ export class DeleteUserModal {
async close () { async close () {
await this.page.click('body', { await this.page.click('body', {
position: { x: 10, y: 10 } position: { x: 10, y: 10 }
}); })
} }
} }

View File

@@ -1,4 +1,4 @@
const { AdminCreateRolePage } = require('./create-role-page'); const { AdminCreateRolePage } = require('./create-role-page')
export class AdminEditRolePage extends AdminCreateRolePage { export class AdminEditRolePage extends AdminCreateRolePage {
constructor (page) { constructor (page) {

View File

@@ -23,7 +23,6 @@ export class AdminEditUserPage extends AuthenticatedPage {
*/ */
async waitForLoad(fullName) { async waitForLoad(fullName) {
return await this.page.waitForFunction((fullName) => { return await this.page.waitForFunction((fullName) => {
// eslint-disable-next-line no-undef
const el = document.querySelector("[data-test='full-name-input']"); const el = document.querySelector("[data-test='full-name-input']");
return el && el.value === fullName; return el && el.value === fullName;
}, fullName); }, fullName);

View File

@@ -25,5 +25,5 @@ export const adminFixtures = {
adminCreateRolePage: async ({ page}, use) => { adminCreateRolePage: async ({ page}, use) => {
await use(new AdminCreateRolePage(page)); await use(new AdminCreateRolePage(page));
}, },
}; }

View File

@@ -87,7 +87,6 @@ export class AdminUsersPage extends AuthenticatedPage {
await this.firstPageButton.click(); await this.firstPageButton.click();
} }
// eslint-disable-next-line no-constant-condition
while (true) { while (true) {
if (await this.usersLoader.isVisible()) { if (await this.usersLoader.isVisible()) {
await this.usersLoader.waitFor({ await this.usersLoader.waitFor({
@@ -109,7 +108,6 @@ export class AdminUsersPage extends AuthenticatedPage {
async getTotalRows() { async getTotalRows() {
return await this.page.evaluate(() => { return await this.page.evaluate(() => {
// eslint-disable-next-line no-undef
const node = document.querySelector('[data-total-count]'); const node = document.querySelector('[data-total-count]');
if (node) { if (node) {
const count = Number(node.dataset.totalCount); const count = Number(node.dataset.totalCount);
@@ -123,7 +121,6 @@ export class AdminUsersPage extends AuthenticatedPage {
async getRowsPerPage() { async getRowsPerPage() {
return await this.page.evaluate(() => { return await this.page.evaluate(() => {
// eslint-disable-next-line no-undef
const node = document.querySelector('[data-rows-per-page]'); const node = document.querySelector('[data-rows-per-page]');
if (node) { if (node) {
const count = Number(node.dataset.rowsPerPage); const count = Number(node.dataset.rowsPerPage);

View File

@@ -25,7 +25,7 @@ export class ApplicationsModal extends BasePage {
if (this.applications[link] === undefined) { if (this.applications[link] === undefined) {
throw { throw {
message: `Unknown link "${link}" passed to ApplicationsModal.selectLink` message: `Unknown link "${link}" passed to ApplicationsModal.selectLink`
}; }
} }
await this.searchInput.fill(link); await this.searchInput.fill(link);
await this.appListItem.first().click(); await this.appListItem.first().click();

View File

@@ -1,3 +1,4 @@
const path = require('node:path');
const { ApplicationsModal } = require('./applications-modal'); const { ApplicationsModal } = require('./applications-modal');
const { AuthenticatedPage } = require('./authenticated-page'); const { AuthenticatedPage } = require('./authenticated-page');

View File

@@ -1,11 +1,10 @@
const { BasePage } = require('../../base-page'); const { BasePage } = require('../../base-page');
const { AddGithubConnectionModal } = require('./add-github-connection-modal'); const { AddGithubConnectionModal } = require('./add-github-connection-modal');
const { expect } = require('@playwright/test');
export class GithubPage extends BasePage { export class GithubPage extends BasePage {
constructor (page) { constructor (page) {
super(page); super(page)
this.addConnectionButton = page.getByTestId('add-connection-button'); this.addConnectionButton = page.getByTestId('add-connection-button');
this.connectionsTab = page.getByTestId('connections-tab'); this.connectionsTab = page.getByTestId('connections-tab');
this.flowsTab = page.getByTestId('flows-tab'); this.flowsTab = page.getByTestId('flows-tab');
@@ -39,7 +38,7 @@ export class GithubPage extends BasePage {
await this.flowsTab.click(); await this.flowsTab.click();
await expect(this.flowsTab).toBeVisible(); await expect(this.flowsTab).toBeVisible();
} }
return await this.flowRows.count() > 0; return await this.flowRows.count() > 0
} }
async hasConnections () { async hasConnections () {

View File

@@ -1,5 +1,4 @@
const { BasePage } = require('../../base-page'); const { BasePage } = require('../../base-page');
const { expect } = require('@playwright/test');
export class GithubPopup extends BasePage { export class GithubPopup extends BasePage {
@@ -12,7 +11,7 @@ export class GithubPopup extends BasePage {
} }
getPathname () { getPathname () {
const url = this.page.url(); const url = this.page.url()
try { try {
return new URL(url).pathname; return new URL(url).pathname;
} catch (e) { } catch (e) {
@@ -35,17 +34,17 @@ export class GithubPopup extends BasePage {
loginInput.click(); loginInput.click();
await loginInput.fill(process.env.GITHUB_USERNAME); await loginInput.fill(process.env.GITHUB_USERNAME);
const passwordInput = this.page.getByLabel('Password'); const passwordInput = this.page.getByLabel('Password');
passwordInput.click(); passwordInput.click()
await passwordInput.fill(process.env.GITHUB_PASSWORD); await passwordInput.fill(process.env.GITHUB_PASSWORD);
await this.page.getByRole('button', { name: 'Sign in' }).click(); await this.page.getByRole('button', { name: 'Sign in' }).click();
// await this.page.waitForTimeout(2000); // await this.page.waitForTimeout(2000);
if (this.page.isClosed()) { if (this.page.isClosed()) {
return; return
} }
// await this.page.waitForLoadState('networkidle', 30000); // await this.page.waitForLoadState('networkidle', 30000);
this.page.waitForEvent('load'); this.page.waitForEvent('load');
if (this.page.isClosed()) { if (this.page.isClosed()) {
return; return
} }
await this.page.waitForURL(function (url) { await this.page.waitForURL(function (url) {
const u = new URL(url); const u = new URL(url);
@@ -56,7 +55,7 @@ export class GithubPopup extends BasePage {
} }
async handleAuthorize () { async handleAuthorize () {
if (this.page.isClosed()) { return; } if (this.page.isClosed()) { return }
const authorizeButton = this.page.getByRole( const authorizeButton = this.page.getByRole(
'button', 'button',
{ name: 'Authorize' } { name: 'Authorize' }
@@ -70,7 +69,7 @@ export class GithubPopup extends BasePage {
) && ( ) && (
u.searchParams.get('client_id') === null u.searchParams.get('client_id') === null
); );
}); })
const passwordInput = this.page.getByLabel('Password'); const passwordInput = this.page.getByLabel('Password');
if (await passwordInput.isVisible()) { if (await passwordInput.isVisible()) {
await passwordInput.fill(process.env.GITHUB_PASSWORD); await passwordInput.fill(process.env.GITHUB_PASSWORD);
@@ -88,6 +87,6 @@ export class GithubPopup extends BasePage {
}; };
} }
} }
await this.page.waitForEvent('close'); await this.page.waitForEvent('close')
} }
} }

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