Compare commits
30 Commits
update-sam
...
migrate-fl
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9752e2c4d2 | ||
![]() |
a5b31da3cc | ||
![]() |
8f7785e9d2 | ||
![]() |
69297c2dd8 | ||
![]() |
1c8e9fac7c | ||
![]() |
590780a539 | ||
![]() |
cbd1f47e87 | ||
![]() |
f89cff4e4a | ||
![]() |
cb08e0bf9f | ||
![]() |
3b54b29a99 | ||
![]() |
25983e046c | ||
![]() |
a6a124d2e6 | ||
![]() |
c7e1d30553 | ||
![]() |
6cc8c45634 | ||
![]() |
ee9a9114b7 | ||
![]() |
11f00f866c | ||
![]() |
03ea61ba81 | ||
![]() |
f6c500c998 | ||
![]() |
b590f0f98f | ||
![]() |
ef9359b208 | ||
![]() |
efd243a340 | ||
![]() |
bafb8b86db | ||
![]() |
84b701747f | ||
![]() |
ec42446daa | ||
![]() |
5046c4c911 | ||
![]() |
ce8c9906cb | ||
![]() |
6fb5482bba | ||
![]() |
58189963f5 | ||
![]() |
f488a71304 | ||
![]() |
4b706e004d |
25
.github/workflows/playwright.yml
vendored
Normal file
25
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Automatisch UI Test
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: yarn playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
@@ -62,12 +62,14 @@
|
||||
"memory-cache": "^0.2.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"nodemailer": "6.7.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"objection": "^3.0.0",
|
||||
"passport": "^0.6.0",
|
||||
"pg": "^8.7.1",
|
||||
"php-serialize": "^4.0.2",
|
||||
"showdown": "^2.1.0",
|
||||
"stripe": "^11.13.0",
|
||||
"winston": "^3.7.1",
|
||||
"xmlrpc": "^1.3.2"
|
||||
@@ -124,6 +126,7 @@
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/pg": "^8.6.1",
|
||||
"@types/pino": "^7.0.5",
|
||||
"@types/showdown": "^2.0.1",
|
||||
"ava": "^3.15.0",
|
||||
"nodemon": "^2.0.13",
|
||||
"sinon": "^11.1.2",
|
||||
|
3
packages/backend/src/apps/formatter/actions/index.ts
Normal file
3
packages/backend/src/apps/formatter/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import text from './text';
|
||||
|
||||
export default [text];
|
64
packages/backend/src/apps/formatter/actions/text/index.ts
Normal file
64
packages/backend/src/apps/formatter/actions/text/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
import capitalize from './transformers/capitalize';
|
||||
import htmlToMarkdown from './transformers/html-to-markdown';
|
||||
import markdownToHtml from './transformers/markdown-to-html';
|
||||
import useDefaultValue from './transformers/use-default-value';
|
||||
import extractEmailAddress from './transformers/extract-email-address';
|
||||
|
||||
const transformers = {
|
||||
capitalize,
|
||||
htmlToMarkdown,
|
||||
markdownToHtml,
|
||||
useDefaultValue,
|
||||
extractEmailAddress,
|
||||
};
|
||||
|
||||
export default defineAction({
|
||||
name: 'Text',
|
||||
key: 'text',
|
||||
description:
|
||||
'Transform text data to capitalize, extract emails, apply default value, and much more.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Transform',
|
||||
key: 'transform',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description: 'Pick a channel to send the message to.',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'Capitalize', value: 'capitalize' },
|
||||
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
|
||||
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
|
||||
{ label: 'Use Default Value', value: 'useDefaultValue' },
|
||||
{ label: 'Extract Email Address', value: 'extractEmailAddress' },
|
||||
],
|
||||
additionalFields: {
|
||||
type: 'query',
|
||||
name: 'getDynamicFields',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listTransformOptions',
|
||||
},
|
||||
{
|
||||
name: 'parameters.transform',
|
||||
value: '{parameters.transform}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const transformerName = $.step.parameters
|
||||
.transform as keyof typeof transformers;
|
||||
const output = transformers[transformerName]($);
|
||||
|
||||
$.setActionItem({
|
||||
raw: {
|
||||
output,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@@ -0,0 +1,11 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import { capitalize as lodashCapitalize } from 'lodash';
|
||||
|
||||
const capitalize = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
const capitalizedInput = input.replace(/\w+/g, lodashCapitalize);
|
||||
|
||||
return capitalizedInput;
|
||||
};
|
||||
|
||||
export default capitalize;
|
@@ -0,0 +1,12 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const extractEmailAddress = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
const emailRegexp =
|
||||
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||
|
||||
const email = input.match(emailRegexp);
|
||||
return email ? email[0] : '';
|
||||
};
|
||||
|
||||
export default extractEmailAddress;
|
@@ -0,0 +1,11 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import { NodeHtmlMarkdown } from 'node-html-markdown';
|
||||
|
||||
const htmlToMarkdown = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
|
||||
const markdown = NodeHtmlMarkdown.translate(input);
|
||||
return markdown;
|
||||
};
|
||||
|
||||
export default htmlToMarkdown;
|
@@ -0,0 +1,13 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import showdown from 'showdown';
|
||||
|
||||
const converter = new showdown.Converter();
|
||||
|
||||
const markdownToHtml = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
|
||||
const html = converter.makeHtml(input);
|
||||
return html;
|
||||
};
|
||||
|
||||
export default markdownToHtml;
|
@@ -0,0 +1,13 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const useDefaultValue = ($: IGlobalVariable) => {
|
||||
const input = $.step.parameters.input as string;
|
||||
|
||||
if (input && input.trim().length > 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return $.step.parameters.defaultValue as string;
|
||||
};
|
||||
|
||||
export default useDefaultValue;
|
3
packages/backend/src/apps/formatter/assets/favicon.svg
Normal file
3
packages/backend/src/apps/formatter/assets/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4H20M4 12H20M4 20H20M4 8H14M4 16H14" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1,3 @@
|
||||
import listTransformOptions from './list-transform-options';
|
||||
|
||||
export default [listTransformOptions];
|
@@ -0,0 +1,23 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
import capitalize from './options/capitalize';
|
||||
import htmlToMarkdown from './options/html-to-markdown';
|
||||
import markdownToHtml from './options/markdown-to-html';
|
||||
import useDefaultValue from './options/use-default-value';
|
||||
import extractEmailAddress from './options/extract-email-address';
|
||||
|
||||
const options: IJSONObject = {
|
||||
capitalize,
|
||||
htmlToMarkdown,
|
||||
markdownToHtml,
|
||||
useDefaultValue,
|
||||
extractEmailAddress,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'List fields after transform',
|
||||
key: 'listTransformOptions',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
return options[$.step.parameters.transform as string];
|
||||
},
|
||||
};
|
@@ -0,0 +1,12 @@
|
||||
const capitalize = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Text that will be capitalized.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default capitalize;
|
@@ -0,0 +1,12 @@
|
||||
const extractEmailAddress = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Text that will be searched for an email address.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default extractEmailAddress;
|
@@ -0,0 +1,12 @@
|
||||
const htmlToMarkdown = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'HTML that will be converted to Markdown.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default htmlToMarkdown;
|
@@ -0,0 +1,12 @@
|
||||
const markdownToHtml = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Markdown text that will be converted to HTML.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default markdownToHtml;
|
@@ -0,0 +1,21 @@
|
||||
const useDefaultValue = [
|
||||
{
|
||||
label: 'Input',
|
||||
key: 'input',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Text you want to check whether it is empty or not.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Default Value',
|
||||
key: 'defaultValue',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'Text that will be used as a default value if the input is empty.',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default useDefaultValue;
|
0
packages/backend/src/apps/formatter/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/formatter/index.d.ts
vendored
Normal file
16
packages/backend/src/apps/formatter/index.ts
Normal file
16
packages/backend/src/apps/formatter/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import actions from './actions';
|
||||
import dynamicFields from './dynamic-fields';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Formatter',
|
||||
key: 'formatter',
|
||||
iconUrl: '{BASE_URL}/apps/formatter/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/formatter/connection',
|
||||
supportsConnections: false,
|
||||
baseUrl: '',
|
||||
apiBaseUrl: '',
|
||||
primaryColor: '001F52',
|
||||
actions,
|
||||
dynamicFields,
|
||||
});
|
@@ -25,6 +25,12 @@ const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
$.auth.data.accessToken = data.access_token;
|
||||
|
||||
const currentUser = await getCurrentUser($);
|
||||
const screenName = [
|
||||
currentUser.username,
|
||||
$.auth.data.instanceUrl,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' @ ');
|
||||
|
||||
await $.auth.set({
|
||||
clientId: $.auth.data.clientId,
|
||||
@@ -34,7 +40,7 @@ const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
scope: data.scope,
|
||||
tokenType: data.token_type,
|
||||
userId: currentUser.id,
|
||||
screenName: `${currentUser.username} @ ${$.auth.data.instanceUrl}`,
|
||||
screenName,
|
||||
});
|
||||
};
|
||||
|
||||
|
6
packages/backend/src/apps/wordpress/assets/favicon.svg
Normal file
6
packages/backend/src/apps/wordpress/assets/favicon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<svg width="256px" height="255px" viewBox="0 0 256 255" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g fill="#464342">
|
||||
<path d="M18.1239675,127.500488 C18.1239675,170.795707 43.284813,208.211252 79.7700163,225.941854 L27.5938862,82.985626 C21.524813,96.5890081 18.1239675,111.643057 18.1239675,127.500488 L18.1239675,127.500488 Z M201.345041,121.980878 C201.345041,108.462829 196.489366,99.1011382 192.324683,91.8145041 C186.780098,82.8045528 181.583089,75.1745041 181.583089,66.1645528 C181.583089,56.1097886 189.208976,46.7501789 199.950569,46.7501789 C200.435512,46.7501789 200.89548,46.8105366 201.367935,46.8375935 C181.907772,29.0091707 155.981008,18.1239675 127.50465,18.1239675 C89.2919675,18.1239675 55.6727154,37.7298211 36.1147317,67.4258211 C38.6809756,67.5028293 41.0994472,67.5569431 43.1536911,67.5569431 C54.5946016,67.5569431 72.3043902,66.1687154 72.3043902,66.1687154 C78.2007154,65.8211382 78.8958699,74.4814309 73.0057886,75.1786667 C73.0057886,75.1786667 67.0803252,75.8759024 60.4867642,76.2213984 L100.318699,194.699447 L124.25574,122.909138 L107.214049,76.2172358 C101.323967,75.8717398 95.744,75.1745041 95.744,75.1745041 C89.8497561,74.8290081 90.540748,65.8169756 96.4349919,66.1645528 C96.4349919,66.1645528 114.498602,67.5527805 125.246439,67.5527805 C136.685268,67.5527805 154.397138,66.1645528 154.397138,66.1645528 C160.297626,65.8169756 160.990699,74.4772683 155.098537,75.1745041 C155.098537,75.1745041 149.160585,75.8717398 142.579512,76.2172358 L182.107577,193.798244 L193.017756,157.340098 C197.746472,142.211122 201.345041,131.34465 201.345041,121.980878 L201.345041,121.980878 Z M129.42361,137.068228 L96.6056585,232.43135 C106.404423,235.31187 116.76722,236.887415 127.50465,236.887415 C140.242211,236.887415 152.457366,234.685398 163.827512,230.68722 C163.534049,230.218927 163.267642,229.721496 163.049106,229.180358 L129.42361,137.068228 L129.42361,137.068228 Z M223.481756,75.0225691 C223.95213,78.5066667 224.218537,82.2467642 224.218537,86.2699187 C224.218537,97.3694959 222.145561,109.846894 215.901659,125.448325 L182.490537,222.04774 C215.00878,203.085008 236.881171,167.854829 236.881171,127.502569 C236.883252,108.485724 232.025496,90.603187 223.481756,75.0225691 L223.481756,75.0225691 Z M127.50465,0 C57.2003902,0 0,57.1962276 0,127.500488 C0,197.813073 57.2003902,255.00722 127.50465,255.00722 C197.806829,255.00722 255.015545,197.813073 255.015545,127.500488 C255.013463,57.1962276 197.806829,0 127.50465,0 L127.50465,0 Z M127.50465,249.162927 C60.4243252,249.162927 5.84637398,194.584976 5.84637398,127.500488 C5.84637398,60.4201626 60.4222439,5.84637398 127.50465,5.84637398 C194.582894,5.84637398 249.156683,60.4201626 249.156683,127.500488 C249.156683,194.584976 194.582894,249.162927 127.50465,249.162927 L127.50465,249.162927 Z"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,24 @@
|
||||
import { URL, URLSearchParams } from 'node:url';
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
import appConfig from '../../../config/app';
|
||||
import getInstanceUrl from '../common/get-instance-url';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const successUrl = new URL(
|
||||
'/app/wordpress/connections/add',
|
||||
appConfig.webAppUrl
|
||||
).toString();
|
||||
const baseUrl = getInstanceUrl($);
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
app_name: 'automatisch',
|
||||
success_url: successUrl,
|
||||
});
|
||||
|
||||
const url = new URL(`/wp-admin/authorize-application.php?${searchParams}`, baseUrl).toString();
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
24
packages/backend/src/apps/wordpress/auth/index.ts
Normal file
24
packages/backend/src/apps/wordpress/auth/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import isStillVerified from './is-still-verified';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'instanceUrl',
|
||||
label: 'WordPress instance URL',
|
||||
type: 'string' as const,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Your WordPress instance URL.',
|
||||
docUrl: 'https://automatisch.io/docs/wordpress#instance-url',
|
||||
clickToCopy: true,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
isStillVerified,
|
||||
verifyCredentials,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
await $.http.get('?rest_route=/wp/v2/settings');
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,24 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const instanceUrl = $.auth.data.instanceUrl as string;
|
||||
const password = $.auth.data.password as string;
|
||||
const siteUrl = $.auth.data.site_url as string;
|
||||
const url = $.auth.data.url as string;
|
||||
const userLogin = $.auth.data.user_login as string;
|
||||
|
||||
if (!password) {
|
||||
throw new Error('Failed while authorizing!');
|
||||
}
|
||||
|
||||
await $.auth.set({
|
||||
screenName: `${userLogin} @ ${siteUrl}`,
|
||||
instanceUrl,
|
||||
password,
|
||||
siteUrl,
|
||||
url,
|
||||
userLogin,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
@@ -0,0 +1,17 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
const userLogin = $.auth.data.userLogin as string;
|
||||
const password = $.auth.data.password as string;
|
||||
|
||||
if (userLogin && password) {
|
||||
requestConfig.auth = {
|
||||
username: userLogin,
|
||||
password,
|
||||
};
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,7 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const getInstanceUrl = ($: IGlobalVariable): string => {
|
||||
return $.auth.data.instanceUrl as string;
|
||||
};
|
||||
|
||||
export default getInstanceUrl;
|
12
packages/backend/src/apps/wordpress/common/set-base-url.ts
Normal file
12
packages/backend/src/apps/wordpress/common/set-base-url.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const setBaseUrl: TBeforeRequest = ($, requestConfig) => {
|
||||
const instanceUrl = $.auth.data.instanceUrl as string;
|
||||
if (instanceUrl) {
|
||||
requestConfig.baseURL = instanceUrl;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default setBaseUrl;
|
@@ -0,0 +1,3 @@
|
||||
import listStatuses from './list-statuses';
|
||||
|
||||
export default [listStatuses];
|
@@ -0,0 +1,37 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
type Status = {
|
||||
slug: string;
|
||||
name: string;
|
||||
}
|
||||
type Statuses = Record<string, Status>;
|
||||
|
||||
export default {
|
||||
name: 'List statuses',
|
||||
key: 'listStatuses',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
const statuses: {
|
||||
data: IJSONObject[];
|
||||
} = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const { data } = await $.http.get<Statuses>('?rest_route=/wp/v2/statuses');
|
||||
|
||||
if (!data) return statuses;
|
||||
|
||||
const values = Object.values(data);
|
||||
|
||||
if (!values?.length) return statuses;
|
||||
|
||||
for (const status of values) {
|
||||
statuses.data.push({
|
||||
value: status.slug,
|
||||
name: status.name,
|
||||
})
|
||||
}
|
||||
|
||||
return statuses;
|
||||
},
|
||||
};
|
0
packages/backend/src/apps/wordpress/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/wordpress/index.d.ts
vendored
Normal file
21
packages/backend/src/apps/wordpress/index.ts
Normal file
21
packages/backend/src/apps/wordpress/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import setBaseUrl from './common/set-base-url';
|
||||
import auth from './auth';
|
||||
import triggers from './triggers';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'WordPress',
|
||||
key: 'wordpress',
|
||||
iconUrl: '{BASE_URL}/apps/wordpress/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/wordpress/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://wordpress.com',
|
||||
apiBaseUrl: '',
|
||||
primaryColor: '464342',
|
||||
beforeRequest: [setBaseUrl, addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
dynamicData,
|
||||
});
|
3
packages/backend/src/apps/wordpress/triggers/index.ts
Normal file
3
packages/backend/src/apps/wordpress/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import newPost from './new-post';
|
||||
|
||||
export default [newPost];
|
@@ -0,0 +1,60 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New post',
|
||||
key: 'newPost',
|
||||
description: 'Triggers when a new post is created.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Status',
|
||||
key: 'status',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listStatuses',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const params = {
|
||||
per_page: 100,
|
||||
page: 1,
|
||||
order: 'desc',
|
||||
orderby: 'date',
|
||||
status: $.step.parameters.status || '',
|
||||
};
|
||||
|
||||
let totalPages = 1;
|
||||
do {
|
||||
const {
|
||||
data,
|
||||
headers
|
||||
} = await $.http.get('?rest_route=/wp/v2/posts', { params });
|
||||
|
||||
params.page = params.page + 1;
|
||||
totalPages = Number(headers['x-wp-totalpages']);
|
||||
|
||||
if (data.length) {
|
||||
for (const post of data) {
|
||||
const dataItem = {
|
||||
raw: post,
|
||||
meta: {
|
||||
internalId: post.id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
}
|
||||
}
|
||||
} while (params.page <= totalPages);
|
||||
},
|
||||
});
|
@@ -0,0 +1,15 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('config', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('key').unique().notNullable();
|
||||
table.jsonb('value').notNullable().defaultTo({});
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('config');
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const getPermissionForRole = (
|
||||
roleId: string,
|
||||
subject: string,
|
||||
actions: string[]
|
||||
) =>
|
||||
actions.map((action) => ({
|
||||
role_id: roleId,
|
||||
subject,
|
||||
action,
|
||||
conditions: [],
|
||||
}));
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const role = (await knex('roles')
|
||||
.first(['id', 'key'])
|
||||
.where({ key: 'admin' })
|
||||
.limit(1)) as { id: string; key: string };
|
||||
|
||||
await knex('permissions').insert(
|
||||
getPermissionForRole(role.id, 'Config', [
|
||||
'update',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex('permissions').where({ subject: 'Config' }).delete();
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable(
|
||||
'saml_auth_providers_role_mappings',
|
||||
(table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table
|
||||
.uuid('saml_auth_provider_id')
|
||||
.references('id')
|
||||
.inTable('saml_auth_providers');
|
||||
table.uuid('role_id').references('id').inTable('roles');
|
||||
table.string('remote_role_name').notNullable();
|
||||
|
||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||
|
||||
table.timestamps(true, true);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('saml_auth_providers_role_mappings');
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('app_configs', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('key').unique().notNullable();
|
||||
table.boolean('allow_custom_connection').notNullable().defaultTo(false);
|
||||
table.boolean('shared').notNullable().defaultTo(false);
|
||||
table.boolean('disabled').notNullable().defaultTo(false);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('app_configs');
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('app_auth_clients', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.string('name').unique().notNullable();
|
||||
table.uuid('app_config_id').notNullable().references('id').inTable('app_configs');
|
||||
table.text('auth_defaults').notNullable();
|
||||
table.boolean('active').notNullable().defaultTo(false);
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('app_auth_clients');
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.table('connections', async (table) => {
|
||||
table.uuid('app_auth_client_id').references('id').inTable('app_auth_clients');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.table('connections', (table) => {
|
||||
table.dropColumn('app_auth_client_id');
|
||||
});
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const getPermissionForRole = (
|
||||
roleId: string,
|
||||
subject: string,
|
||||
actions: string[]
|
||||
) =>
|
||||
actions.map((action) => ({
|
||||
role_id: roleId,
|
||||
subject,
|
||||
action,
|
||||
conditions: [],
|
||||
}));
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const role = (await knex('roles')
|
||||
.first(['id', 'key'])
|
||||
.where({ key: 'admin' })
|
||||
.limit(1)) as { id: string; key: string };
|
||||
|
||||
await knex('permissions').insert(
|
||||
getPermissionForRole(role.id, 'App', [
|
||||
'create',
|
||||
'read',
|
||||
'delete',
|
||||
'update',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex('permissions').where({ subject: 'App' }).delete();
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
import createAppAuthClient from './mutations/create-app-auth-client.ee';
|
||||
import createAppConfig from './mutations/create-app-config.ee';
|
||||
import createConnection from './mutations/create-connection';
|
||||
import createFlow from './mutations/create-flow';
|
||||
import createRole from './mutations/create-role.ee';
|
||||
@@ -17,6 +19,9 @@ import login from './mutations/login';
|
||||
import registerUser from './mutations/register-user.ee';
|
||||
import resetConnection from './mutations/reset-connection';
|
||||
import resetPassword from './mutations/reset-password.ee';
|
||||
import updateAppAuthClient from './mutations/update-app-auth-client.ee';
|
||||
import updateAppConfig from './mutations/update-app-config.ee';
|
||||
import updateConfig from './mutations/update-config.ee';
|
||||
import updateConnection from './mutations/update-connection';
|
||||
import updateCurrentUser from './mutations/update-current-user';
|
||||
import updateFlow from './mutations/update-flow';
|
||||
@@ -24,10 +29,13 @@ import updateFlowStatus from './mutations/update-flow-status';
|
||||
import updateRole from './mutations/update-role.ee';
|
||||
import updateStep from './mutations/update-step';
|
||||
import updateUser from './mutations/update-user.ee';
|
||||
import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee';
|
||||
import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee';
|
||||
import verifyConnection from './mutations/verify-connection';
|
||||
import createSamlAuthProvider from './mutations/create-saml-auth-provider.ee';
|
||||
|
||||
const mutationResolvers = {
|
||||
createAppAuthClient,
|
||||
createAppConfig,
|
||||
createConnection,
|
||||
createFlow,
|
||||
createRole,
|
||||
@@ -47,15 +55,19 @@ const mutationResolvers = {
|
||||
registerUser,
|
||||
resetConnection,
|
||||
resetPassword,
|
||||
updateAppAuthClient,
|
||||
updateAppConfig,
|
||||
updateConfig,
|
||||
updateConnection,
|
||||
updateCurrentUser,
|
||||
updateUser,
|
||||
updateFlow,
|
||||
updateFlowStatus,
|
||||
updateRole,
|
||||
updateStep,
|
||||
updateUser,
|
||||
upsertSamlAuthProvider,
|
||||
upsertSamlAuthProvidersRoleMappings,
|
||||
verifyConnection,
|
||||
createSamlAuthProvider,
|
||||
};
|
||||
|
||||
export default mutationResolvers;
|
||||
|
@@ -0,0 +1,35 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
appConfigId: string;
|
||||
name: string;
|
||||
formattedAuthDefaults?: IJSONObject;
|
||||
active?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const createAppAuthClient = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.findById(params.input.appConfigId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const appAuthClient = await appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.insert(
|
||||
params.input
|
||||
);
|
||||
|
||||
return appAuthClient;
|
||||
};
|
||||
|
||||
export default createAppAuthClient;
|
@@ -0,0 +1,36 @@
|
||||
import App from '../../models/app';
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
key: string;
|
||||
allowCustomConnection?: boolean;
|
||||
shared?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const createAppConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const key = params.input.key;
|
||||
|
||||
const app = await App.findOneByKey(key);
|
||||
|
||||
if (!app) throw new Error('The app cannot be found!');
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.insert(
|
||||
params.input
|
||||
);
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
|
||||
export default createAppConfig;
|
@@ -1,13 +1,16 @@
|
||||
import App from '../../models/app';
|
||||
import Context from '../../types/express/context';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
key: string;
|
||||
appAuthClientId: string;
|
||||
formattedData: IJSONObject;
|
||||
};
|
||||
};
|
||||
|
||||
const createConnection = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
@@ -15,13 +18,42 @@ const createConnection = async (
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
await App.findOneByKey(params.input.key);
|
||||
const { key, appAuthClientId } = params.input;
|
||||
|
||||
return await context.currentUser.$relatedQuery('connections').insert({
|
||||
key: params.input.key,
|
||||
formattedData: params.input.formattedData,
|
||||
verified: false,
|
||||
});
|
||||
const app = await App.findOneByKey(key);
|
||||
|
||||
const appConfig = await AppConfig.query().findOne({ key });
|
||||
|
||||
let formattedData = params.input.formattedData;
|
||||
if (appConfig) {
|
||||
if (appConfig.disabled) throw new Error('This application has been disabled for new connections!');
|
||||
|
||||
if (!appConfig.allowCustomConnection && formattedData) throw new Error(`Custom connections cannot be created for ${app.name}!`);
|
||||
|
||||
if (appConfig.shared && !formattedData) {
|
||||
const authClient = await appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.findById(appAuthClientId)
|
||||
.where({
|
||||
active: true
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
formattedData = authClient.formattedAuthDefaults;
|
||||
}
|
||||
}
|
||||
|
||||
const createdConnection = await context
|
||||
.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.insert({
|
||||
key,
|
||||
appAuthClientId,
|
||||
formattedData,
|
||||
verified: false,
|
||||
});
|
||||
|
||||
return createdConnection;
|
||||
};
|
||||
|
||||
export default createConnection;
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import Context from '../../types/express/context';
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
const deleteAppAuthClient = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'App');
|
||||
|
||||
await AppAuthClient
|
||||
.query()
|
||||
.delete()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export default deleteAppAuthClient;
|
@@ -0,0 +1,38 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
name: string;
|
||||
formattedAuthDefaults?: IJSONObject;
|
||||
active?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const updateAppAuthClient = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const {
|
||||
id,
|
||||
...appAuthClientData
|
||||
} = params.input;
|
||||
|
||||
const appAuthClient = await AppAuthClient
|
||||
.query()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
await appAuthClient
|
||||
.$query()
|
||||
.patch(appAuthClientData);
|
||||
|
||||
return appAuthClient;
|
||||
};
|
||||
|
||||
export default updateAppAuthClient;
|
@@ -0,0 +1,39 @@
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
allowCustomConnection?: boolean;
|
||||
shared?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const updateAppConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'App');
|
||||
|
||||
const {
|
||||
id,
|
||||
...appConfigToUpdate
|
||||
} = params.input;
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
await appConfig
|
||||
.$query()
|
||||
.patch(
|
||||
appConfigToUpdate
|
||||
);
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
|
||||
export default updateAppConfig;
|
44
packages/backend/src/graphql/mutations/update-config.ee.ts
Normal file
44
packages/backend/src/graphql/mutations/update-config.ee.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { IJSONValue } from '@automatisch/types';
|
||||
import Config from '../../models/config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
[index: string]: IJSONValue;
|
||||
};
|
||||
};
|
||||
|
||||
const updateConfig = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('update', 'Config');
|
||||
|
||||
const config = params.input;
|
||||
const configKeys = Object.keys(config);
|
||||
const updates = [];
|
||||
|
||||
for (const key of configKeys) {
|
||||
const newValue = config[key];
|
||||
|
||||
const entryUpdate = Config
|
||||
.query()
|
||||
.insert({
|
||||
key,
|
||||
value: {
|
||||
data: newValue
|
||||
}
|
||||
})
|
||||
.onConflict('key')
|
||||
.merge({
|
||||
value: {
|
||||
data: newValue
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(entryUpdate);
|
||||
}
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export default updateConfig;
|
@@ -1,10 +1,12 @@
|
||||
import Context from '../../types/express/context';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import Context from '../../types/express/context';
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
formattedData: IJSONObject;
|
||||
formattedData?: IJSONObject;
|
||||
appAuthClientId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,10 +24,21 @@ const updateConnection = async (
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
let formattedData = params.input.formattedData;
|
||||
|
||||
if (params.input.appAuthClientId) {
|
||||
const appAuthClient = await AppAuthClient
|
||||
.query()
|
||||
.findById(params.input.appAuthClientId)
|
||||
.throwIfNotFound();
|
||||
|
||||
formattedData = appAuthClient.formattedAuthDefaults;
|
||||
}
|
||||
|
||||
connection = await connection.$query().patchAndFetch({
|
||||
formattedData: {
|
||||
...connection.formattedData,
|
||||
...params.input.formattedData,
|
||||
...formattedData,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -18,7 +18,7 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
const createSamlAuthProvider = async (
|
||||
const upsertSamlAuthProvider = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
@@ -33,17 +33,15 @@ const createSamlAuthProvider = async (
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
let samlAuthProvider: SamlAuthProvider;
|
||||
|
||||
if (!existingSamlAuthProvider) {
|
||||
samlAuthProvider = await SamlAuthProvider.query().insert(
|
||||
const samlAuthProvider = await SamlAuthProvider.query().insert(
|
||||
samlAuthProviderPayload
|
||||
);
|
||||
|
||||
return samlAuthProvider;
|
||||
}
|
||||
|
||||
samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
|
||||
const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
|
||||
existingSamlAuthProvider.id,
|
||||
samlAuthProviderPayload
|
||||
);
|
||||
@@ -51,4 +49,4 @@ const createSamlAuthProvider = async (
|
||||
return samlAuthProvider;
|
||||
};
|
||||
|
||||
export default createSamlAuthProvider;
|
||||
export default upsertSamlAuthProvider;
|
@@ -0,0 +1,54 @@
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../../models/saml-auth-providers-role-mapping.ee';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
samlAuthProviderId: string;
|
||||
samlAuthProvidersRoleMappings: [
|
||||
{
|
||||
roleId: string;
|
||||
remoteRoleName: string;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
const upsertSamlAuthProvidersRoleMappings = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'SamlAuthProvider');
|
||||
|
||||
const samlAuthProviderId = params.input.samlAuthProviderId;
|
||||
|
||||
const samlAuthProvider = await SamlAuthProvider.query()
|
||||
.findById(samlAuthProviderId)
|
||||
.throwIfNotFound();
|
||||
|
||||
await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.delete();
|
||||
|
||||
if (!params.input.samlAuthProvidersRoleMappings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const samlAuthProvidersRoleMappingsData =
|
||||
params.input.samlAuthProvidersRoleMappings.map(
|
||||
(samlAuthProvidersRoleMapping) => ({
|
||||
...samlAuthProvidersRoleMapping,
|
||||
samlAuthProviderId: samlAuthProvider.id,
|
||||
})
|
||||
);
|
||||
|
||||
const samlAuthProvidersRoleMappings =
|
||||
await SamlAuthProvidersRoleMapping.query().insert(
|
||||
samlAuthProvidersRoleMappingsData
|
||||
);
|
||||
|
||||
return samlAuthProvidersRoleMappings;
|
||||
};
|
||||
|
||||
export default upsertSamlAuthProvidersRoleMappings;
|
@@ -0,0 +1,30 @@
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const getAppAuthClient = async (_parent: unknown, params: Params, context: Context) => {
|
||||
let canSeeAllClients = false;
|
||||
try {
|
||||
context.currentUser.can('read', 'App');
|
||||
|
||||
canSeeAllClients = true;
|
||||
} catch {
|
||||
// void
|
||||
}
|
||||
|
||||
const appAuthClient = AppAuthClient
|
||||
.query()
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
|
||||
if (!canSeeAllClients) {
|
||||
appAuthClient.where({ active: true });
|
||||
}
|
||||
|
||||
return await appAuthClient;
|
||||
};
|
||||
|
||||
export default getAppAuthClient;
|
@@ -0,0 +1,40 @@
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
appKey: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
const getAppAuthClients = async (_parent: unknown, params: Params, context: Context) => {
|
||||
let canSeeAllClients = false;
|
||||
try {
|
||||
context.currentUser.can('read', 'App');
|
||||
|
||||
canSeeAllClients = true;
|
||||
} catch {
|
||||
// void
|
||||
}
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.findOne({
|
||||
key: params.appKey,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
const appAuthClients = appConfig
|
||||
.$relatedQuery('appAuthClients')
|
||||
.where({ active: params.active })
|
||||
.skipUndefined();
|
||||
|
||||
if (!canSeeAllClients) {
|
||||
appAuthClients.where({
|
||||
active: true
|
||||
})
|
||||
}
|
||||
|
||||
return await appAuthClients;
|
||||
};
|
||||
|
||||
export default getAppAuthClients;
|
23
packages/backend/src/graphql/queries/get-app-config.ee.ts
Normal file
23
packages/backend/src/graphql/queries/get-app-config.ee.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import AppConfig from '../../models/app-config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
const getAppConfig = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
const appConfig = await AppConfig
|
||||
.query()
|
||||
.withGraphFetched({
|
||||
appAuthClients: true
|
||||
})
|
||||
.findOne({
|
||||
key: params.key
|
||||
});
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
|
||||
export default getAppConfig;
|
@@ -19,6 +19,10 @@ const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
.select('connections.*')
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
appAuthClient: true
|
||||
})
|
||||
.fullOuterJoinRelated('steps')
|
||||
.where({
|
||||
'connections.key': params.key,
|
||||
|
@@ -1,8 +1,19 @@
|
||||
import appConfig from '../../config/app';
|
||||
import { getLicense } from '../../helpers/license.ee';
|
||||
|
||||
const getAutomatischInfo = async () => {
|
||||
const license = await getLicense();
|
||||
|
||||
const computedLicense = {
|
||||
id: license ? license.id : null,
|
||||
name: license ? license.name : null,
|
||||
expireAt: license ? license.expireAt : null,
|
||||
verified: license ? true : false,
|
||||
};
|
||||
|
||||
return {
|
||||
isCloud: appConfig.isCloud,
|
||||
license: computedLicense,
|
||||
};
|
||||
};
|
||||
|
||||
|
34
packages/backend/src/graphql/queries/get-config.ee.ts
Normal file
34
packages/backend/src/graphql/queries/get-config.ee.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { hasValidLicense } from '../../helpers/license.ee';
|
||||
import Config from '../../models/config';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
keys: string[];
|
||||
};
|
||||
|
||||
const getConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
if (!await hasValidLicense()) return {};
|
||||
|
||||
const configQuery = Config
|
||||
.query();
|
||||
|
||||
if (Array.isArray(params.keys)) {
|
||||
configQuery.whereIn('key', params.keys);
|
||||
}
|
||||
|
||||
const config = await configQuery;
|
||||
|
||||
return config.reduce((computedConfig, configEntry) => {
|
||||
const { key, value } = configEntry;
|
||||
|
||||
computedConfig[key] = value?.data;
|
||||
|
||||
return computedConfig;
|
||||
}, {} as Record<string, unknown>);
|
||||
};
|
||||
|
||||
export default getConfig;
|
@@ -32,7 +32,7 @@ const getDynamicFields = async (
|
||||
|
||||
const connection = step.connection;
|
||||
|
||||
if (!connection || !step.appKey) return null;
|
||||
if (!step.appKey) return null;
|
||||
|
||||
const app = await App.findOneByKey(step.appKey);
|
||||
const $ = await globalVariable({ connection, app, flow: step.flow, step });
|
||||
|
@@ -0,0 +1,19 @@
|
||||
import Context from '../../types/express/context';
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
const getSamlAuthProvider = async (
|
||||
_parent: unknown,
|
||||
params: unknown,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('read', 'SamlAuthProvider');
|
||||
|
||||
const samlAuthProvider = await SamlAuthProvider.query()
|
||||
.limit(1)
|
||||
.first()
|
||||
.throwIfNotFound();
|
||||
|
||||
return samlAuthProvider;
|
||||
};
|
||||
|
||||
export default getSamlAuthProvider;
|
@@ -1,9 +1,9 @@
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
const getSamlAuthProviders = async () => {
|
||||
const listSamlAuthProviders = async () => {
|
||||
const providers = await SamlAuthProvider.query().where({ active: true });
|
||||
|
||||
return providers;
|
||||
};
|
||||
|
||||
export default getSamlAuthProviders;
|
||||
export default listSamlAuthProviders;
|
@@ -1,7 +1,11 @@
|
||||
import getApp from './queries/get-app';
|
||||
import getAppAuthClient from './queries/get-app-auth-client.ee';
|
||||
import getAppAuthClients from './queries/get-app-auth-clients.ee';
|
||||
import getAppConfig from './queries/get-app-config.ee';
|
||||
import getApps from './queries/get-apps';
|
||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
||||
import getConfig from './queries/get-config.ee';
|
||||
import getConnectedApps from './queries/get-connected-apps';
|
||||
import getCurrentUser from './queries/get-current-user';
|
||||
import getDynamicData from './queries/get-dynamic-data';
|
||||
@@ -11,26 +15,31 @@ import getExecutionSteps from './queries/get-execution-steps';
|
||||
import getExecutions from './queries/get-executions';
|
||||
import getFlow from './queries/get-flow';
|
||||
import getFlows from './queries/get-flows';
|
||||
import getUser from './queries/get-user';
|
||||
import getUsers from './queries/get-users';
|
||||
import getInvoices from './queries/get-invoices.ee';
|
||||
import getPaddleInfo from './queries/get-paddle-info.ee';
|
||||
import getPaymentPlans from './queries/get-payment-plans.ee';
|
||||
import getPermissionCatalog from './queries/get-permission-catalog.ee';
|
||||
import getRole from './queries/get-role.ee';
|
||||
import getRoles from './queries/get-roles.ee';
|
||||
import getSamlAuthProviders from './queries/get-saml-auth-providers.ee';
|
||||
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||
import getSubscriptionStatus from './queries/get-subscription-status.ee';
|
||||
import getTrialStatus from './queries/get-trial-status.ee';
|
||||
import getUser from './queries/get-user';
|
||||
import getUsers from './queries/get-users';
|
||||
import healthcheck from './queries/healthcheck';
|
||||
import listSamlAuthProviders from './queries/list-saml-auth-providers.ee';
|
||||
import testConnection from './queries/test-connection';
|
||||
|
||||
const queryResolvers = {
|
||||
getApp,
|
||||
getAppAuthClient,
|
||||
getAppAuthClients,
|
||||
getAppConfig,
|
||||
getApps,
|
||||
getAutomatischInfo,
|
||||
getBillingAndUsage,
|
||||
getConfig,
|
||||
getConnectedApps,
|
||||
getCurrentUser,
|
||||
getDynamicData,
|
||||
@@ -46,13 +55,14 @@ const queryResolvers = {
|
||||
getPermissionCatalog,
|
||||
getRole,
|
||||
getRoles,
|
||||
getSamlAuthProviders,
|
||||
getSamlAuthProvider,
|
||||
getStepWithTestExecutions,
|
||||
getSubscriptionStatus,
|
||||
getTrialStatus,
|
||||
getUser,
|
||||
getUsers,
|
||||
healthcheck,
|
||||
listSamlAuthProviders,
|
||||
testConnection,
|
||||
};
|
||||
|
||||
|
@@ -5,6 +5,9 @@ type Query {
|
||||
onlyWithActions: Boolean
|
||||
): [App]
|
||||
getApp(key: String!): App
|
||||
getAppConfig(key: String!): AppConfig
|
||||
getAppAuthClient(id: String!): AppAuthClient
|
||||
getAppAuthClients(appKey: String!, active: Boolean): [AppAuthClient]
|
||||
getConnectedApps(name: String): [App]
|
||||
testConnection(id: String!): Connection
|
||||
getFlow(id: String!): Flow
|
||||
@@ -33,24 +36,28 @@ type Query {
|
||||
key: String!
|
||||
parameters: JSONObject
|
||||
): [SubstepArgument]
|
||||
getCurrentUser: User
|
||||
getPaymentPlans: [PaymentPlan]
|
||||
getPaddleInfo: GetPaddleInfo
|
||||
getBillingAndUsage: GetBillingAndUsage
|
||||
getInvoices: [Invoice]
|
||||
getAutomatischInfo: GetAutomatischInfo
|
||||
getTrialStatus: GetTrialStatus
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getSamlAuthProviders: [GetSamlAuthProviders]
|
||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||
getUser(id: String!): User
|
||||
getRoles: [Role]
|
||||
getRole(id: String!): Role
|
||||
getBillingAndUsage: GetBillingAndUsage
|
||||
getCurrentUser: User
|
||||
getConfig(keys: [String]): JSONObject
|
||||
getInvoices: [Invoice]
|
||||
getPaddleInfo: GetPaddleInfo
|
||||
getPaymentPlans: [PaymentPlan]
|
||||
getPermissionCatalog: PermissionCatalog
|
||||
getRole(id: String!): Role
|
||||
getRoles: [Role]
|
||||
getSamlAuthProvider: SamlAuthProvider
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getTrialStatus: GetTrialStatus
|
||||
getUser(id: String!): User
|
||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||
healthcheck: AppHealth
|
||||
listSamlAuthProviders: [ListSamlAuthProvider]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createAppConfig(input: CreateAppConfigInput): AppConfig
|
||||
createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient
|
||||
createConnection(input: CreateConnectionInput): Connection
|
||||
createFlow(input: CreateFlowInput): Flow
|
||||
createRole(input: CreateRoleInput): Role
|
||||
@@ -70,6 +77,9 @@ type Mutation {
|
||||
registerUser(input: RegisterUserInput): User
|
||||
resetConnection(input: ResetConnectionInput): Connection
|
||||
resetPassword(input: ResetPasswordInput): Boolean
|
||||
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
|
||||
updateAppConfig(input: UpdateAppConfigInput): AppConfig
|
||||
updateConfig(input: JSONObject): JSONObject
|
||||
updateConnection(input: UpdateConnectionInput): Connection
|
||||
updateCurrentUser(input: UpdateCurrentUserInput): User
|
||||
updateFlow(input: UpdateFlowInput): Flow
|
||||
@@ -77,8 +87,11 @@ type Mutation {
|
||||
updateRole(input: UpdateRoleInput): Role
|
||||
updateStep(input: UpdateStepInput): Step
|
||||
updateUser(input: UpdateUserInput): User
|
||||
upsertSamlAuthProvider(input: UpsertSamlAuthProviderInput): SamlAuthProvider
|
||||
upsertSamlAuthProvidersRoleMappings(
|
||||
input: UpsertSamlAuthProvidersRoleMappingsInput
|
||||
): [SamlAuthProvidersRoleMapping]
|
||||
verifyConnection(input: VerifyConnectionInput): Connection
|
||||
createSamlAuthProvider(input: CreateSamlAuthProviderInput): SamlAuthProvider
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -156,6 +169,16 @@ type SubstepArgumentAdditionalFieldsArgument {
|
||||
value: String
|
||||
}
|
||||
|
||||
type AppConfig {
|
||||
id: String
|
||||
key: String
|
||||
allowCustomConnection: Boolean
|
||||
canConnect: Boolean
|
||||
canCustomConnect: Boolean
|
||||
shared: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
|
||||
type App {
|
||||
name: String
|
||||
key: String
|
||||
@@ -175,7 +198,9 @@ type App {
|
||||
type AppAuth {
|
||||
fields: [Field]
|
||||
authenticationSteps: [AuthenticationStep]
|
||||
sharedAuthenticationSteps: [AuthenticationStep]
|
||||
reconnectionSteps: [ReconnectionStep]
|
||||
sharedReconnectionSteps: [ReconnectionStep]
|
||||
}
|
||||
|
||||
enum ArgumentEnumType {
|
||||
@@ -213,6 +238,8 @@ type AuthLink {
|
||||
type Connection {
|
||||
id: String
|
||||
key: String
|
||||
reconnectable: Boolean
|
||||
appAuthClientId: String
|
||||
formattedData: ConnectionData
|
||||
verified: Boolean
|
||||
app: App
|
||||
@@ -304,6 +331,13 @@ type SamlAuthProvider {
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
type SamlAuthProvidersRoleMapping {
|
||||
id: String
|
||||
samlAuthProviderId: String
|
||||
roleId: String
|
||||
remoteRoleName: String
|
||||
}
|
||||
|
||||
type UserConnection {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
@@ -315,7 +349,8 @@ type UserEdge {
|
||||
|
||||
input CreateConnectionInput {
|
||||
key: String!
|
||||
formattedData: JSONObject!
|
||||
appAuthClientId: String
|
||||
formattedData: JSONObject
|
||||
}
|
||||
|
||||
input GenerateAuthUrlInput {
|
||||
@@ -324,7 +359,8 @@ input GenerateAuthUrlInput {
|
||||
|
||||
input UpdateConnectionInput {
|
||||
id: String!
|
||||
formattedData: JSONObject!
|
||||
formattedData: JSONObject
|
||||
appAuthClientId: String
|
||||
}
|
||||
|
||||
input ResetConnectionInput {
|
||||
@@ -335,7 +371,7 @@ input VerifyConnectionInput {
|
||||
id: String!
|
||||
}
|
||||
|
||||
input CreateSamlAuthProviderInput {
|
||||
input UpsertSamlAuthProviderInput {
|
||||
name: String!
|
||||
certificate: String!
|
||||
signatureAlgorithm: String!
|
||||
@@ -349,6 +385,16 @@ input CreateSamlAuthProviderInput {
|
||||
active: Boolean!
|
||||
}
|
||||
|
||||
input UpsertSamlAuthProvidersRoleMappingsInput {
|
||||
samlAuthProviderId: String!
|
||||
samlAuthProvidersRoleMappings: [SamlAuthProviderRoleMappingInput]
|
||||
}
|
||||
|
||||
input SamlAuthProviderRoleMappingInput {
|
||||
roleId: String!
|
||||
remoteRoleName: String!
|
||||
}
|
||||
|
||||
input DeleteConnectionInput {
|
||||
id: String!
|
||||
}
|
||||
@@ -592,6 +638,14 @@ type AppHealth {
|
||||
|
||||
type GetAutomatischInfo {
|
||||
isCloud: Boolean
|
||||
license: License
|
||||
}
|
||||
|
||||
type License {
|
||||
id: String
|
||||
name: String
|
||||
expireAt: String
|
||||
verified: Boolean
|
||||
}
|
||||
|
||||
type GetTrialStatus {
|
||||
@@ -659,7 +713,7 @@ type PaymentPlan {
|
||||
productId: String
|
||||
}
|
||||
|
||||
type GetSamlAuthProviders {
|
||||
type ListSamlAuthProvider {
|
||||
id: String
|
||||
name: String
|
||||
issuer: String
|
||||
@@ -694,6 +748,41 @@ type Subject {
|
||||
key: String
|
||||
}
|
||||
|
||||
input CreateAppConfigInput {
|
||||
key: String
|
||||
allowCustomConnection: Boolean
|
||||
shared: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
|
||||
input UpdateAppConfigInput {
|
||||
id: String
|
||||
allowCustomConnection: Boolean
|
||||
shared: Boolean
|
||||
disabled: Boolean
|
||||
}
|
||||
|
||||
type AppAuthClient {
|
||||
id: String
|
||||
appConfigId: String
|
||||
name: String
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
input CreateAppAuthClientInput {
|
||||
appConfigId: String
|
||||
name: String
|
||||
formattedAuthDefaults: JSONObject
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
input UpdateAppAuthClientInput {
|
||||
id: String
|
||||
name: String
|
||||
formattedAuthDefaults: JSONObject
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
|
@@ -3,6 +3,7 @@ import { IApp } from '@automatisch/types';
|
||||
function addAuthenticationSteps(app: IApp): IApp {
|
||||
if (app.auth.generateAuthUrl) {
|
||||
app.auth.authenticationSteps = authenticationStepsWithAuthUrl;
|
||||
app.auth.sharedAuthenticationSteps = sharedAuthenticationStepsWithAuthUrl;
|
||||
} else {
|
||||
app.auth.authenticationSteps = authenticationStepsWithoutAuthUrl;
|
||||
}
|
||||
@@ -98,4 +99,65 @@ const authenticationStepsWithAuthUrl = [
|
||||
},
|
||||
];
|
||||
|
||||
const sharedAuthenticationStepsWithAuthUrl = [
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'createConnection',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: '{key}',
|
||||
},
|
||||
{
|
||||
name: 'appAuthClientId',
|
||||
value: '{appAuthClientId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'generateAuthUrl',
|
||||
arguments: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '{createConnection.id}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'openWithPopup' as const,
|
||||
name: 'openAuthPopup',
|
||||
arguments: [
|
||||
{
|
||||
name: 'url',
|
||||
value: '{generateAuthUrl.url}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'updateConnection',
|
||||
arguments: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '{createConnection.id}',
|
||||
},
|
||||
{
|
||||
name: 'formattedData',
|
||||
value: '{openAuthPopup.all}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'mutation' as const,
|
||||
name: 'verifyConnection',
|
||||
arguments: [
|
||||
{
|
||||
name: 'id',
|
||||
value: '{createConnection.id}',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default addAuthenticationSteps;
|
||||
|
@@ -67,11 +67,21 @@ function addReconnectionSteps(app: IApp): IApp {
|
||||
|
||||
if (hasReconnectionSteps) return app;
|
||||
|
||||
const updatedSteps = replaceCreateConnectionsWithUpdate(
|
||||
app.auth.authenticationSteps
|
||||
);
|
||||
if (app.auth.authenticationSteps) {
|
||||
const updatedSteps = replaceCreateConnectionsWithUpdate(
|
||||
app.auth.authenticationSteps
|
||||
);
|
||||
|
||||
app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps];
|
||||
app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps];
|
||||
}
|
||||
|
||||
if (app.auth.sharedAuthenticationSteps) {
|
||||
const updatedStepsWithEmbeddedDefaults = replaceCreateConnectionsWithUpdate(
|
||||
app.auth.sharedAuthenticationSteps
|
||||
);
|
||||
|
||||
app.auth.sharedReconnectionSteps = [resetConnectionStep, ...updatedStepsWithEmbeddedDefaults];
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@@ -12,8 +12,7 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
|
||||
const { userId } = jwt.verify(token, appConfig.appSecretKey) as {
|
||||
userId: string;
|
||||
};
|
||||
req.currentUser = await User
|
||||
.query()
|
||||
req.currentUser = await User.query()
|
||||
.findById(userId)
|
||||
.leftJoinRelated({
|
||||
role: true,
|
||||
@@ -35,8 +34,9 @@ const authentication = shield(
|
||||
Query: {
|
||||
'*': isAuthenticated,
|
||||
getAutomatischInfo: allow,
|
||||
getSamlAuthProviders: allow,
|
||||
listSamlAuthProviders: allow,
|
||||
healthcheck: allow,
|
||||
getConfig: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': isAuthenticated,
|
||||
|
@@ -1,19 +1,27 @@
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import User from '../models/user';
|
||||
import Identity from '../models/identity.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
|
||||
const getUser = (user: Record<string, unknown>, providerConfig: SamlAuthProvider) => ({
|
||||
const getUser = (
|
||||
user: Record<string, unknown>,
|
||||
providerConfig: SamlAuthProvider
|
||||
) => ({
|
||||
name: user[providerConfig.firstnameAttributeName],
|
||||
surname: user[providerConfig.surnameAttributeName],
|
||||
id: user.nameID,
|
||||
email: user[providerConfig.emailAttributeName],
|
||||
role: user[providerConfig.roleAttributeName],
|
||||
})
|
||||
role: user[providerConfig.roleAttributeName] as string | string[],
|
||||
});
|
||||
|
||||
const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unknown>, samlAuthProvider: SamlAuthProvider) => {
|
||||
const findOrCreateUserBySamlIdentity = async (
|
||||
userIdentity: Record<string, unknown>,
|
||||
samlAuthProvider: SamlAuthProvider
|
||||
) => {
|
||||
const mappedUser = getUser(userIdentity, samlAuthProvider);
|
||||
const identity = await Identity.query().findOne({
|
||||
remote_id: mappedUser.id,
|
||||
provider_type: 'saml',
|
||||
});
|
||||
|
||||
if (identity) {
|
||||
@@ -22,25 +30,38 @@ const findOrCreateUserBySamlIdentity = async (userIdentity: Record<string, unkno
|
||||
return user;
|
||||
}
|
||||
|
||||
const createdUser = await User.query().insertGraph({
|
||||
fullName: [
|
||||
mappedUser.name,
|
||||
mappedUser.surname
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
email: mappedUser.email as string,
|
||||
roleId: samlAuthProvider.defaultRoleId,
|
||||
identities: [
|
||||
const mappedRoles = Array.isArray(mappedUser.role)
|
||||
? mappedUser.role
|
||||
: [mappedUser.role];
|
||||
|
||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.whereIn('remote_role_name', mappedRoles)
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
const createdUser = await User.query()
|
||||
.insertGraph(
|
||||
{
|
||||
remoteId: mappedUser.id as string,
|
||||
providerId: samlAuthProvider.id,
|
||||
providerType: 'saml'
|
||||
fullName: [mappedUser.name, mappedUser.surname]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
email: mappedUser.email as string,
|
||||
roleId:
|
||||
samlAuthProviderRoleMapping.roleId || samlAuthProvider.defaultRoleId,
|
||||
identities: [
|
||||
{
|
||||
remoteId: mappedUser.id as string,
|
||||
providerId: samlAuthProvider.id,
|
||||
providerType: 'saml',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
relate: ['identities'],
|
||||
}
|
||||
]
|
||||
}, {
|
||||
relate: ['identities']
|
||||
}).returning('*');
|
||||
)
|
||||
.returning('*');
|
||||
|
||||
return createdUser;
|
||||
};
|
||||
|
@@ -5,7 +5,13 @@ import memoryCache from 'memory-cache';
|
||||
|
||||
const CACHE_DURATION = 1000 * 60 * 60 * 24; // 24 hours in milliseconds
|
||||
|
||||
const checkLicense = async () => {
|
||||
const hasValidLicense = async () => {
|
||||
const license = await getLicense();
|
||||
|
||||
return license ? true : false;
|
||||
};
|
||||
|
||||
const getLicense = async () => {
|
||||
const licenseKey = appConfig.licenseKey;
|
||||
|
||||
if (!licenseKey) {
|
||||
@@ -20,13 +26,13 @@ const checkLicense = async () => {
|
||||
} else {
|
||||
try {
|
||||
const { data } = await axios.post(url, { licenseKey });
|
||||
memoryCache.put(url, data.verified, CACHE_DURATION);
|
||||
memoryCache.put(url, data, CACHE_DURATION);
|
||||
|
||||
return data.verified;
|
||||
return data;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default checkLicense;
|
||||
export { getLicense, hasValidLicense };
|
91
packages/backend/src/models/app-auth-client.ts
Normal file
91
packages/backend/src/models/app-auth-client.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
import appConfig from '../config/app';
|
||||
import AppConfig from './app-config';
|
||||
import Base from './base';
|
||||
|
||||
class AppAuthClient extends Base {
|
||||
id!: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
appConfigId!: string;
|
||||
authDefaults: string;
|
||||
formattedAuthDefaults?: IJSONObject;
|
||||
appConfig?: AppConfig;
|
||||
|
||||
static tableName = 'app_auth_clients';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['name', 'appConfigId', 'formattedAuthDefaults'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
appConfigId: { type: 'string', format: 'uuid' },
|
||||
active: { type: 'boolean' },
|
||||
authDefaults: { type: ['string', 'null'] },
|
||||
formattedAuthDefaults: { type: 'object' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'app_auth_clients.app_config_id',
|
||||
to: 'app_configs.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
encryptData(): void {
|
||||
if (!this.eligibleForEncryption()) return;
|
||||
|
||||
this.authDefaults = AES.encrypt(
|
||||
JSON.stringify(this.formattedAuthDefaults),
|
||||
appConfig.encryptionKey
|
||||
).toString();
|
||||
|
||||
delete this.formattedAuthDefaults;
|
||||
}
|
||||
decryptData(): void {
|
||||
if (!this.eligibleForDecryption()) return;
|
||||
|
||||
this.formattedAuthDefaults = JSON.parse(
|
||||
AES.decrypt(this.authDefaults, appConfig.encryptionKey).toString(enc.Utf8)
|
||||
);
|
||||
}
|
||||
|
||||
eligibleForEncryption(): boolean {
|
||||
return this.formattedAuthDefaults ? true : false;
|
||||
}
|
||||
|
||||
eligibleForDecryption(): boolean {
|
||||
return this.authDefaults ? true : false;
|
||||
}
|
||||
|
||||
// TODO: Make another abstraction like beforeSave instead of using
|
||||
// beforeInsert and beforeUpdate separately for the same operation.
|
||||
async $beforeInsert(queryContext: QueryContext): Promise<void> {
|
||||
await super.$beforeInsert(queryContext);
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $beforeUpdate(
|
||||
opt: ModelOptions,
|
||||
queryContext: QueryContext
|
||||
): Promise<void> {
|
||||
await super.$beforeUpdate(opt, queryContext);
|
||||
this.encryptData();
|
||||
}
|
||||
|
||||
async $afterFind(): Promise<void> {
|
||||
this.decryptData();
|
||||
}
|
||||
}
|
||||
|
||||
export default AppAuthClient;
|
70
packages/backend/src/models/app-config.ts
Normal file
70
packages/backend/src/models/app-config.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import App from './app';
|
||||
import Base from './base';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
|
||||
class AppConfig extends Base {
|
||||
id!: string;
|
||||
key!: string;
|
||||
allowCustomConnection: boolean;
|
||||
shared: boolean;
|
||||
disabled: boolean;
|
||||
app?: App;
|
||||
appAuthClients?: AppAuthClient[];
|
||||
|
||||
static tableName = 'app_configs';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['key'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
key: { type: 'string' },
|
||||
allowCustomConnection: { type: 'boolean', default: false },
|
||||
shared: { type: 'boolean', default: false },
|
||||
disabled: { type: 'boolean', default: false },
|
||||
},
|
||||
};
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['canConnect', 'canCustomConnect'];
|
||||
}
|
||||
|
||||
static relationMappings = () => ({
|
||||
appAuthClients: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: AppAuthClient,
|
||||
join: {
|
||||
from: 'app_configs.id',
|
||||
to: 'app_auth_clients.app_config_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get canCustomConnect() {
|
||||
return !this.disabled && this.allowCustomConnection;
|
||||
}
|
||||
|
||||
get canConnect() {
|
||||
const hasSomeActiveAppAuthClients = !!this.appAuthClients
|
||||
?.some(appAuthClient => appAuthClient.active);
|
||||
const shared = this.shared;
|
||||
const active = this.disabled === false;
|
||||
|
||||
const conditions = [
|
||||
hasSomeActiveAppAuthClients,
|
||||
shared,
|
||||
active
|
||||
];
|
||||
|
||||
return conditions.every(Boolean);
|
||||
}
|
||||
|
||||
async getApp() {
|
||||
if (!this.key) return null;
|
||||
|
||||
return await App.findOneByKey(this.key);
|
||||
}
|
||||
}
|
||||
|
||||
export default AppConfig;
|
23
packages/backend/src/models/config.ts
Normal file
23
packages/backend/src/models/config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IJSONValue } from '@automatisch/types';
|
||||
import Base from './base';
|
||||
|
||||
class Config extends Base {
|
||||
id!: string;
|
||||
key!: string;
|
||||
value!: { data: IJSONValue };
|
||||
|
||||
static tableName = 'config';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['key', 'value'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
key: { type: 'string', minLength: 1 },
|
||||
value: { type: 'object' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Config;
|
@@ -3,6 +3,8 @@ import type { RelationMappings } from 'objection';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { IRequest } from '@automatisch/types';
|
||||
import App from './app';
|
||||
import AppConfig from './app-config';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
import Base from './base';
|
||||
import User from './user';
|
||||
import Step from './step';
|
||||
@@ -25,6 +27,9 @@ class Connection extends Base {
|
||||
user?: User;
|
||||
steps?: Step[];
|
||||
triggerSteps?: Step[];
|
||||
appAuthClientId?: string;
|
||||
appAuthClient?: AppAuthClient;
|
||||
appConfig?: AppConfig;
|
||||
|
||||
static tableName = 'connections';
|
||||
|
||||
@@ -38,6 +43,7 @@ class Connection extends Base {
|
||||
data: { type: 'string' },
|
||||
formattedData: { type: 'object' },
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
appAuthClientId: { type: 'string', format: 'uuid' },
|
||||
verified: { type: 'boolean', default: false },
|
||||
draft: { type: 'boolean' },
|
||||
deletedAt: { type: 'string' },
|
||||
@@ -46,6 +52,10 @@ class Connection extends Base {
|
||||
},
|
||||
};
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['reconnectable'];
|
||||
}
|
||||
|
||||
static relationMappings = (): RelationMappings => ({
|
||||
user: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
@@ -74,8 +84,36 @@ class Connection extends Base {
|
||||
builder.where('type', '=', 'trigger');
|
||||
},
|
||||
},
|
||||
appConfig: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppConfig,
|
||||
join: {
|
||||
from: 'connections.key',
|
||||
to: 'app_configs.key',
|
||||
},
|
||||
},
|
||||
appAuthClient: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: AppAuthClient,
|
||||
join: {
|
||||
from: 'connections.app_auth_client_id',
|
||||
to: 'app_auth_clients.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get reconnectable() {
|
||||
if (this.appAuthClientId) {
|
||||
return this.appAuthClient.active;
|
||||
}
|
||||
|
||||
if (this.appConfig) {
|
||||
return !this.appConfig.disabled && this.appConfig.allowCustomConnection;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
encryptData(): void {
|
||||
if (!this.eligibleForEncryption()) return;
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import type { SamlConfig } from '@node-saml/passport-saml';
|
||||
import appConfig from '../config/app';
|
||||
import Base from './base';
|
||||
import Identity from './identity.ee';
|
||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee';
|
||||
|
||||
class SamlAuthProvider extends Base {
|
||||
id!: string;
|
||||
@@ -17,6 +18,7 @@ class SamlAuthProvider extends Base {
|
||||
roleAttributeName: string;
|
||||
defaultRoleId: string;
|
||||
active: boolean;
|
||||
samlAuthProvidersRoleMappings?: SamlAuthProvidersRoleMapping[];
|
||||
|
||||
static tableName = 'saml_auth_providers';
|
||||
|
||||
@@ -63,6 +65,14 @@ class SamlAuthProvider extends Base {
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
samlAuthProvidersRoleMappings: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: SamlAuthProvidersRoleMapping,
|
||||
join: {
|
||||
from: 'saml_auth_providers.id',
|
||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get config(): SamlConfig {
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import Base from './base';
|
||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||
|
||||
class SamlAuthProvidersRoleMapping extends Base {
|
||||
id!: string;
|
||||
samlAuthProviderId: string;
|
||||
roleId: string;
|
||||
remoteRoleName: string;
|
||||
|
||||
static tableName = 'saml_auth_providers_role_mappings';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['samlAuthProviderId', 'roleId', 'remoteRoleName'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
samlAuthProviderId: { type: 'string', format: 'uuid' },
|
||||
roleId: { type: 'string', format: 'uuid' },
|
||||
remoteRoleName: { type: 'string', minLength: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
samlAuthProvider: {
|
||||
relation: Base.BelongsToOneRelation,
|
||||
modelClass: SamlAuthProvider,
|
||||
join: {
|
||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||
to: 'saml_auth_providers.id',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default SamlAuthProvidersRoleMapping;
|
@@ -4,7 +4,7 @@ import crypto from 'node:crypto';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import checkLicense from '../helpers/check-license.ee';
|
||||
import { hasValidLicense } from '../helpers/license.ee';
|
||||
import userAbility from '../helpers/user-ability';
|
||||
import Base from './base';
|
||||
import Connection from './connection';
|
||||
@@ -289,17 +289,18 @@ class User extends Base {
|
||||
}
|
||||
|
||||
async $afterFind(): Promise<any> {
|
||||
const hasValidLicense = await checkLicense();
|
||||
|
||||
if (hasValidLicense) return this;
|
||||
if (await hasValidLicense()) return this;
|
||||
|
||||
if (Array.isArray(this.permissions)) {
|
||||
this.permissions = this.permissions.filter((permission) => {
|
||||
const isRolePermission = permission.subject === 'Role';
|
||||
const isSamlAuthProviderPermission =
|
||||
permission.subject === 'SamlAuthProvider';
|
||||
const restrictedSubjects = [
|
||||
'App',
|
||||
'Role',
|
||||
'SamlAuthProvider',
|
||||
'Config',
|
||||
];
|
||||
|
||||
return !isRolePermission && !isSamlAuthProviderPermission;
|
||||
return !restrictedSubjects.includes(permission.subject);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -86,6 +86,15 @@ export default defineConfig({
|
||||
{ text: 'Connection', link: '/apps/flickr/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Formatter',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Actions', link: '/apps/formatter/actions' },
|
||||
{ text: 'Connection', link: '/apps/formatter/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'GitHub',
|
||||
collapsible: true,
|
||||
@@ -337,6 +346,15 @@ export default defineConfig({
|
||||
{ text: 'Connection', link: '/apps/webhooks/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'WordPress',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Triggers', link: '/apps/wordpress/triggers' },
|
||||
{ text: 'Connection', link: '/apps/wordpress/connection' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/': [
|
||||
{
|
||||
|
12
packages/docs/pages/apps/formatter/actions.md
Normal file
12
packages/docs/pages/apps/formatter/actions.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
favicon: /favicons/formatter.svg
|
||||
items:
|
||||
- name: Text
|
||||
desc: Transform text data to capitalize, extract emails, apply default value, and much more.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
11
packages/docs/pages/apps/formatter/connection.md
Normal file
11
packages/docs/pages/apps/formatter/connection.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Formatter
|
||||
|
||||
Formatter is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the Formatter app. It can be used as an action, and you can use it to format the data from the previous steps. It can be used to format the data in the following ways.
|
||||
|
||||
## Text
|
||||
|
||||
- Capitalize
|
||||
- Convert HTML to Markdown
|
||||
- Convert Markdown to HTML
|
||||
- Use Default Value
|
||||
- Extract Email Address
|
9
packages/docs/pages/apps/wordpress/connection.md
Normal file
9
packages/docs/pages/apps/wordpress/connection.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# WordPress
|
||||
|
||||
:::info
|
||||
This page explains the steps you need to follow to set up the WordPress connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||
:::
|
||||
|
||||
1. Add your WordPress public URL (without any path in the address) in the **WordPress instance URL** field on Automatisch.
|
||||
1. Click **Submit** button on Automatisch.
|
||||
1. Congrats! Start using your new WordPress connection within the flows.
|
12
packages/docs/pages/apps/wordpress/triggers.md
Normal file
12
packages/docs/pages/apps/wordpress/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
favicon: /favicons/wordpress.svg
|
||||
items:
|
||||
- name: New post
|
||||
desc: Triggers when a new post is created.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
@@ -17,6 +17,10 @@ yarn install
|
||||
|
||||
Make sure that you have **PostgreSQL** and **Redis** installed and running.
|
||||
|
||||
:::warning
|
||||
Scripts we have prepared for Automatisch work with PostgreSQL version 14. If you have a different version, you might have some problems with the database setup.
|
||||
:::
|
||||
|
||||
Create a `.env` file in the backend package:
|
||||
|
||||
```bash
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Available Apps
|
||||
|
||||
Following integrations are currently supported by Automatisch.
|
||||
The following integrations are currently supported by Automatisch.
|
||||
|
||||
- [DeepL](/apps/deepl/actions)
|
||||
- [Delay](/apps/delay/actions)
|
||||
@@ -8,6 +8,7 @@ Following integrations are currently supported by Automatisch.
|
||||
- [Dropbox](/apps/dropbox/actions)
|
||||
- [Filter](/apps/filter/actions)
|
||||
- [Flickr](/apps/flickr/triggers)
|
||||
- [Formatter](/apps/formatter/actions)
|
||||
- [GitHub](/apps/github/triggers)
|
||||
- [GitLab](/apps/gitlab/triggers)
|
||||
- [Google Drive](/apps/google-drive/triggers)
|
||||
@@ -35,3 +36,4 @@ Following integrations are currently supported by Automatisch.
|
||||
- [Twitter](/apps/twitter/triggers)
|
||||
- [Typeform](/apps/typeform/triggers)
|
||||
- [Webhooks](/apps/webhooks/triggers)
|
||||
- [WordPress](/apps/wordpress/triggers)
|
||||
|
3
packages/docs/pages/public/favicons/formatter.svg
Normal file
3
packages/docs/pages/public/favicons/formatter.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4H20M4 12H20M4 20H20M4 8H14M4 16H14" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 243 B |
6
packages/docs/pages/public/favicons/wordpress.svg
Normal file
6
packages/docs/pages/public/favicons/wordpress.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<svg width="256px" height="255px" viewBox="0 0 256 255" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g fill="#464342">
|
||||
<path d="M18.1239675,127.500488 C18.1239675,170.795707 43.284813,208.211252 79.7700163,225.941854 L27.5938862,82.985626 C21.524813,96.5890081 18.1239675,111.643057 18.1239675,127.500488 L18.1239675,127.500488 Z M201.345041,121.980878 C201.345041,108.462829 196.489366,99.1011382 192.324683,91.8145041 C186.780098,82.8045528 181.583089,75.1745041 181.583089,66.1645528 C181.583089,56.1097886 189.208976,46.7501789 199.950569,46.7501789 C200.435512,46.7501789 200.89548,46.8105366 201.367935,46.8375935 C181.907772,29.0091707 155.981008,18.1239675 127.50465,18.1239675 C89.2919675,18.1239675 55.6727154,37.7298211 36.1147317,67.4258211 C38.6809756,67.5028293 41.0994472,67.5569431 43.1536911,67.5569431 C54.5946016,67.5569431 72.3043902,66.1687154 72.3043902,66.1687154 C78.2007154,65.8211382 78.8958699,74.4814309 73.0057886,75.1786667 C73.0057886,75.1786667 67.0803252,75.8759024 60.4867642,76.2213984 L100.318699,194.699447 L124.25574,122.909138 L107.214049,76.2172358 C101.323967,75.8717398 95.744,75.1745041 95.744,75.1745041 C89.8497561,74.8290081 90.540748,65.8169756 96.4349919,66.1645528 C96.4349919,66.1645528 114.498602,67.5527805 125.246439,67.5527805 C136.685268,67.5527805 154.397138,66.1645528 154.397138,66.1645528 C160.297626,65.8169756 160.990699,74.4772683 155.098537,75.1745041 C155.098537,75.1745041 149.160585,75.8717398 142.579512,76.2172358 L182.107577,193.798244 L193.017756,157.340098 C197.746472,142.211122 201.345041,131.34465 201.345041,121.980878 L201.345041,121.980878 Z M129.42361,137.068228 L96.6056585,232.43135 C106.404423,235.31187 116.76722,236.887415 127.50465,236.887415 C140.242211,236.887415 152.457366,234.685398 163.827512,230.68722 C163.534049,230.218927 163.267642,229.721496 163.049106,229.180358 L129.42361,137.068228 L129.42361,137.068228 Z M223.481756,75.0225691 C223.95213,78.5066667 224.218537,82.2467642 224.218537,86.2699187 C224.218537,97.3694959 222.145561,109.846894 215.901659,125.448325 L182.490537,222.04774 C215.00878,203.085008 236.881171,167.854829 236.881171,127.502569 C236.883252,108.485724 232.025496,90.603187 223.481756,75.0225691 L223.481756,75.0225691 Z M127.50465,0 C57.2003902,0 0,57.1962276 0,127.500488 C0,197.813073 57.2003902,255.00722 127.50465,255.00722 C197.806829,255.00722 255.015545,197.813073 255.015545,127.500488 C255.013463,57.1962276 197.806829,0 127.50465,0 L127.50465,0 Z M127.50465,249.162927 C60.4243252,249.162927 5.84637398,194.584976 5.84637398,127.500488 C5.84637398,60.4201626 60.4222439,5.84637398 127.50465,5.84637398 C194.582894,5.84637398 249.156683,60.4201626 249.156683,127.500488 C249.156683,194.584976 194.582894,249.162927 127.50465,249.162927 L127.50465,249.162927 Z"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
5
packages/e2e-tests/.gitignore
vendored
Normal file
5
packages/e2e-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
/output
|
12
packages/e2e-tests/fixtures/applications-page.js
Normal file
12
packages/e2e-tests/fixtures/applications-page.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const path = require('node:path');
|
||||
const { BasePage } = require('./base-page');
|
||||
|
||||
export class ApplicationsPage extends BasePage {
|
||||
async screenshot(options = {}) {
|
||||
const { path: plainPath, ...restOptions } = options;
|
||||
|
||||
const computedPath = path.join('applications', plainPath);
|
||||
|
||||
return await super.screenshot({ path: computedPath, ...restOptions });
|
||||
}
|
||||
}
|
34
packages/e2e-tests/fixtures/base-page.js
Normal file
34
packages/e2e-tests/fixtures/base-page.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const path = require('node:path');
|
||||
|
||||
export class BasePage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async clickAway() {
|
||||
await this.page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
}
|
||||
|
||||
async screenshot(options = {}) {
|
||||
const { path: plainPath, ...restOptions } = options;
|
||||
|
||||
const computedPath = path.join('output/screenshots', plainPath);
|
||||
|
||||
return await this.page.screenshot({ path: computedPath, ...restOptions });
|
||||
}
|
||||
|
||||
async login() {
|
||||
await this.page.goto('/login');
|
||||
await this.page
|
||||
.getByTestId('email-text-field')
|
||||
.fill(process.env.LOGIN_EMAIL);
|
||||
await this.page
|
||||
.getByTestId('password-text-field')
|
||||
.fill(process.env.LOGIN_PASSWORD);
|
||||
|
||||
await this.page.getByTestId('login-button').click();
|
||||
}
|
||||
}
|
16
packages/e2e-tests/fixtures/connections-page.js
Normal file
16
packages/e2e-tests/fixtures/connections-page.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const path = require('node:path');
|
||||
const { BasePage } = require('./base-page');
|
||||
|
||||
export class ConnectionsPage extends BasePage {
|
||||
async screenshot(options = {}) {
|
||||
const { path: plainPath, ...restOptions } = options;
|
||||
|
||||
const computedPath = path.join('connections', plainPath);
|
||||
|
||||
return await super.screenshot({ path: computedPath, ...restOptions });
|
||||
}
|
||||
|
||||
async clickAddConnectionButton() {
|
||||
await this.page.getByTestId('add-connection-button').click();
|
||||
}
|
||||
}
|
12
packages/e2e-tests/fixtures/executions-page.js
Normal file
12
packages/e2e-tests/fixtures/executions-page.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const path = require('node:path');
|
||||
const { BasePage } = require('./base-page');
|
||||
|
||||
export class ExecutionsPage extends BasePage {
|
||||
async screenshot(options = {}) {
|
||||
const { path: plainPath, ...restOptions } = options;
|
||||
|
||||
const computedPath = path.join('executions', plainPath);
|
||||
|
||||
return await super.screenshot({ path: computedPath, ...restOptions });
|
||||
}
|
||||
}
|
27
packages/e2e-tests/fixtures/flow-editor-page.js
Normal file
27
packages/e2e-tests/fixtures/flow-editor-page.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const path = require('node:path');
|
||||
const { BasePage } = require('./base-page');
|
||||
|
||||
export class FlowEditorPage extends BasePage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
this.appAutocomplete = this.page.getByTestId('choose-app-autocomplete');
|
||||
this.eventAutocomplete = this.page.getByTestId('choose-event-autocomplete');
|
||||
this.continueButton = this.page.getByTestId('flow-substep-continue-button');
|
||||
this.connectionAutocomplete = this.page.getByTestId(
|
||||
'choose-connection-autocomplete'
|
||||
);
|
||||
this.testOuput = this.page.getByTestId('flow-test-substep-output');
|
||||
this.unpublishFlowButton = this.page.getByTestId('unpublish-flow-button');
|
||||
this.publishFlowButton = this.page.getByTestId('publish-flow-button');
|
||||
this.infoSnackbar = this.page.getByTestId('flow-cannot-edit-info-snackbar');
|
||||
this.trigger = this.page.getByLabel('Trigger on weekends?');
|
||||
}
|
||||
|
||||
async screenshot(options = {}) {
|
||||
const { path: plainPath, ...restOptions } = options;
|
||||
|
||||
const computedPath = path.join('flow-editor', plainPath);
|
||||
|
||||
return await super.screenshot({ path: computedPath, ...restOptions });
|
||||
}
|
||||
}
|
21
packages/e2e-tests/fixtures/index.js
Normal file
21
packages/e2e-tests/fixtures/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const base = require('@playwright/test');
|
||||
const { ApplicationsPage } = require('./applications-page');
|
||||
const { ConnectionsPage } = require('./connections-page');
|
||||
const { ExecutionsPage } = require('./executions-page');
|
||||
const { FlowEditorPage } = require('./flow-editor-page');
|
||||
|
||||
exports.test = base.test.extend({
|
||||
applicationsPage: async ({ page }, use) => {
|
||||
await use(new ApplicationsPage(page));
|
||||
},
|
||||
connectionsPage: async ({ page }, use) => {
|
||||
await use(new ConnectionsPage(page));
|
||||
},
|
||||
executionsPage: async ({ page }, use) => {
|
||||
await use(new ExecutionsPage(page));
|
||||
},
|
||||
flowEditorPage: async ({ page }, use) => {
|
||||
await use(new FlowEditorPage(page));
|
||||
},
|
||||
});
|
||||
exports.expect = base.expect;
|
@@ -5,7 +5,8 @@
|
||||
"private": true,
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
"open": "cypress open"
|
||||
"open": "cypress open",
|
||||
"playwright": "playwright test"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
@@ -22,6 +23,10 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.36.2",
|
||||
"cypress": "^10.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1"
|
||||
}
|
||||
}
|
||||
|
82
packages/e2e-tests/playwright.config.js
Normal file
82
packages/e2e-tests/playwright.config.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// @ts-check
|
||||
const { defineConfig, devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
require('dotenv').config();
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
module.exports = defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: process.env.CI ? 'github' : 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.CI
|
||||
? 'https://sandbox.automatisch.io'
|
||||
: 'http://localhost:3001',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
testIdAttribute: 'data-test',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
67
packages/e2e-tests/tests/apps/list-apps.spec.js
Normal file
67
packages/e2e-tests/tests/apps/list-apps.spec.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Apps page', () => {
|
||||
test.beforeEach(async ({ page, applicationsPage }) => {
|
||||
await applicationsPage.login();
|
||||
await page.getByTestId('apps-page-drawer-link').click();
|
||||
});
|
||||
|
||||
test('displays applications', async ({ page, applicationsPage }) => {
|
||||
await page.getByTestId('apps-loader').waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
await expect(page.getByTestId('app-row')).not.toHaveCount(0);
|
||||
|
||||
await applicationsPage.screenshot({
|
||||
path: 'Applications.png',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('can add connection', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await expect(page.getByTestId('add-connection-button')).toBeVisible();
|
||||
await page.getByTestId('add-connection-button').click();
|
||||
await page
|
||||
.getByTestId('search-for-app-loader')
|
||||
.waitFor({ state: 'detached' });
|
||||
});
|
||||
|
||||
test('lists applications', async ({ page, applicationsPage }) => {
|
||||
const appListItemCount = await page.getByTestId('app-list-item').count();
|
||||
expect(appListItemCount).toBeGreaterThan(10);
|
||||
|
||||
await applicationsPage.clickAway();
|
||||
});
|
||||
|
||||
test('searches an application', async ({ page, applicationsPage }) => {
|
||||
await page.getByTestId('search-for-app-text-field').fill('DeepL');
|
||||
await expect(page.getByTestId('app-list-item')).toHaveCount(1);
|
||||
|
||||
await applicationsPage.clickAway();
|
||||
});
|
||||
|
||||
test('goes to app page to create a connection', async ({
|
||||
page,
|
||||
applicationsPage,
|
||||
}) => {
|
||||
await page.getByTestId('app-list-item').first().click();
|
||||
await expect(page).toHaveURL('/app/deepl/connections/add');
|
||||
await expect(page.getByTestId('add-app-connection-dialog')).toBeVisible();
|
||||
|
||||
await applicationsPage.clickAway();
|
||||
});
|
||||
|
||||
test('closes the dialog on backdrop click', async ({
|
||||
page,
|
||||
applicationsPage,
|
||||
}) => {
|
||||
await page.getByTestId('app-list-item').first().click();
|
||||
await expect(page).toHaveURL('/app/deepl/connections/add');
|
||||
await expect(page.getByTestId('add-app-connection-dialog')).toBeVisible();
|
||||
await applicationsPage.clickAway();
|
||||
await expect(page).toHaveURL('/app/deepl/connections');
|
||||
await expect(page.getByTestId('add-app-connection-dialog')).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,50 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Connections page', () => {
|
||||
test.beforeEach(async ({ page, connectionsPage }) => {
|
||||
await connectionsPage.login();
|
||||
await page.getByTestId('apps-page-drawer-link').click();
|
||||
await page.goto('/app/ntfy/connections');
|
||||
});
|
||||
|
||||
test('shows connections if any', async ({ page, connectionsPage }) => {
|
||||
await page.getByTestId('apps-loader').waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
await connectionsPage.screenshot({
|
||||
path: 'Connections.png',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('can add connection', () => {
|
||||
test('has a button to open add connection dialog', async ({ page }) => {
|
||||
await expect(page.getByTestId('add-connection-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('add connection button takes user to add connection page', async ({
|
||||
page,
|
||||
connectionsPage,
|
||||
}) => {
|
||||
await connectionsPage.clickAddConnectionButton();
|
||||
await expect(page).toHaveURL('/app/ntfy/connections/add');
|
||||
});
|
||||
|
||||
test('shows add connection dialog to create a new connection', async ({
|
||||
page,
|
||||
connectionsPage,
|
||||
}) => {
|
||||
await connectionsPage.clickAddConnectionButton();
|
||||
await expect(page).toHaveURL('/app/ntfy/connections/add');
|
||||
await page.getByTestId('create-connection-button').click();
|
||||
await expect(
|
||||
page.getByTestId('create-connection-button')
|
||||
).not.toBeVisible();
|
||||
|
||||
await connectionsPage.screenshot({
|
||||
path: 'Ntfy connections after creating a connection.png',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,39 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Executions page', () => {
|
||||
test.beforeEach(async ({ page, executionsPage }) => {
|
||||
await executionsPage.login();
|
||||
|
||||
await page.getByTestId('executions-page-drawer-link').click();
|
||||
await page.getByTestId('execution-row').first().click();
|
||||
|
||||
await expect(page).toHaveURL(/\/executions\//);
|
||||
});
|
||||
|
||||
test('displays data in by default', async ({ page, executionsPage }) => {
|
||||
await expect(page.getByTestId('execution-step').last()).toBeVisible();
|
||||
await expect(page.getByTestId('execution-step')).toHaveCount(2);
|
||||
|
||||
await executionsPage.screenshot({
|
||||
path: 'Execution - data in.png',
|
||||
});
|
||||
});
|
||||
|
||||
test('displays data out', async ({ page, executionsPage }) => {
|
||||
const executionStepCount = await page.getByTestId('execution-step').count();
|
||||
for (let i = 0; i < executionStepCount; i++) {
|
||||
await page.getByTestId('data-out-tab').nth(i).click();
|
||||
await expect(page.getByTestId('data-out-panel').nth(i)).toBeVisible();
|
||||
|
||||
await executionsPage.screenshot({
|
||||
path: `Execution - data out - ${i}.png`,
|
||||
animations: 'disabled',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('does not display error', async ({ page }) => {
|
||||
await expect(page.getByTestId('error-tab')).toBeHidden();
|
||||
});
|
||||
});
|
19
packages/e2e-tests/tests/executions/list-executions.spec.js
Normal file
19
packages/e2e-tests/tests/executions/list-executions.spec.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Executions page', () => {
|
||||
test.beforeEach(async ({ page, executionsPage }) => {
|
||||
await executionsPage.login();
|
||||
|
||||
await page.getByTestId('executions-page-drawer-link').click();
|
||||
});
|
||||
|
||||
test('displays executions', async ({ page, executionsPage }) => {
|
||||
await page.getByTestId('executions-loader').waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
await expect(page.getByTestId('execution-row').first()).toBeVisible();
|
||||
|
||||
await executionsPage.screenshot({ path: 'Executions.png' });
|
||||
});
|
||||
});
|
205
packages/e2e-tests/tests/flow-editor/create-flow.spec.js
Normal file
205
packages/e2e-tests/tests/flow-editor/create-flow.spec.js
Normal file
@@ -0,0 +1,205 @@
|
||||
// @ts-check
|
||||
const { FlowEditorPage } = require('../../fixtures/flow-editor-page');
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
let page;
|
||||
let flowEditorPage;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
flowEditorPage = new FlowEditorPage(page);
|
||||
});
|
||||
|
||||
test('create flow', async ({}) => {
|
||||
await flowEditorPage.login();
|
||||
|
||||
await flowEditorPage.page.getByTestId('create-flow-button').click();
|
||||
await expect(flowEditorPage.page).toHaveURL(/\/editor\/create/);
|
||||
await expect(flowEditorPage.page).toHaveURL(
|
||||
/\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/
|
||||
);
|
||||
});
|
||||
|
||||
test('has two steps by default', async ({}) => {
|
||||
await expect(flowEditorPage.page.getByTestId('flow-step')).toHaveCount(2);
|
||||
});
|
||||
|
||||
test.describe('arrange Scheduler trigger', () => {
|
||||
test.describe('choose app and event substep', () => {
|
||||
test('choose application', async ({}) => {
|
||||
await flowEditorPage.appAutocomplete.click();
|
||||
await flowEditorPage.page
|
||||
.getByRole('option', { name: 'Scheduler' })
|
||||
.click();
|
||||
});
|
||||
|
||||
test('choose an event', async ({}) => {
|
||||
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
||||
await flowEditorPage.eventAutocomplete.click();
|
||||
await flowEditorPage.page
|
||||
.getByRole('option', { name: 'Every hour' })
|
||||
.click();
|
||||
});
|
||||
|
||||
test('continue to next step', async ({}) => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
test('collapses the substep', async ({}) => {
|
||||
await expect(flowEditorPage.appAutocomplete).not.toBeVisible();
|
||||
await expect(flowEditorPage.eventAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('set up a trigger', () => {
|
||||
test('choose "yes" in "trigger on weekends?"', async ({}) => {
|
||||
await expect(flowEditorPage.trigger).toBeVisible();
|
||||
await flowEditorPage.trigger.click();
|
||||
await flowEditorPage.page.getByRole('option', { name: 'Yes' }).click();
|
||||
});
|
||||
|
||||
test('continue to next step', async ({}) => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
test('collapses the substep', async ({}) => {
|
||||
await expect(flowEditorPage.trigger).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('test trigger', () => {
|
||||
test('show sample output', async ({}) => {
|
||||
await expect(flowEditorPage.testOuput).not.toBeVisible();
|
||||
await flowEditorPage.continueButton.click();
|
||||
await expect(flowEditorPage.testOuput).toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Scheduler trigger test output.png',
|
||||
});
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('arrange Ntfy action', () => {
|
||||
test.describe('choose app and event substep', () => {
|
||||
test('choose application', async ({}) => {
|
||||
await flowEditorPage.appAutocomplete.click();
|
||||
await flowEditorPage.page.getByRole('option', { name: 'Ntfy' }).click();
|
||||
});
|
||||
|
||||
test('choose an event', async ({}) => {
|
||||
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
||||
await flowEditorPage.eventAutocomplete.click();
|
||||
await flowEditorPage.page
|
||||
.getByRole('option', { name: 'Send message' })
|
||||
.click();
|
||||
});
|
||||
|
||||
test('continue to next step', async ({}) => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
test('collapses the substep', async ({}) => {
|
||||
await expect(flowEditorPage.appAutocomplete).not.toBeVisible();
|
||||
await expect(flowEditorPage.eventAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('choose connection', () => {
|
||||
test('choose connection list item', async ({}) => {
|
||||
await flowEditorPage.connectionAutocomplete.click();
|
||||
await flowEditorPage.page.getByRole('listitem').first().click();
|
||||
});
|
||||
|
||||
test('continue to next step', async ({}) => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
test('collapses the substep', async ({}) => {
|
||||
await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('set up action', () => {
|
||||
test('fill topic and message body', async ({}) => {
|
||||
await flowEditorPage.page
|
||||
.getByTestId('parameters.topic-power-input')
|
||||
.locator('[contenteditable]')
|
||||
.fill('Topic');
|
||||
await flowEditorPage.page
|
||||
.getByTestId('parameters.message-power-input')
|
||||
.locator('[contenteditable]')
|
||||
.fill('Message body');
|
||||
});
|
||||
|
||||
test('continue to next step', async ({}) => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
test('collapses the substep', async ({}) => {
|
||||
await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('test trigger', () => {
|
||||
test('show sample output', async ({}) => {
|
||||
await expect(flowEditorPage.testOuput).not.toBeVisible();
|
||||
await flowEditorPage.page
|
||||
.getByTestId('flow-substep-continue-button')
|
||||
.first()
|
||||
.click();
|
||||
await expect(flowEditorPage.testOuput).toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Ntfy action test output.png',
|
||||
});
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('publish and unpublish', () => {
|
||||
test('publish flow', async ({}) => {
|
||||
await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible();
|
||||
await expect(flowEditorPage.publishFlowButton).toBeVisible();
|
||||
await flowEditorPage.publishFlowButton.click();
|
||||
await expect(flowEditorPage.publishFlowButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('shows read-only sticky snackbar', async ({}) => {
|
||||
await expect(flowEditorPage.infoSnackbar).toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Published flow.png',
|
||||
});
|
||||
});
|
||||
|
||||
test('unpublish from snackbar', async ({}) => {
|
||||
await flowEditorPage.page
|
||||
.getByTestId('unpublish-flow-from-snackbar')
|
||||
.click();
|
||||
await expect(flowEditorPage.infoSnackbar).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('publish once again', async ({}) => {
|
||||
await expect(flowEditorPage.publishFlowButton).toBeVisible();
|
||||
await flowEditorPage.publishFlowButton.click();
|
||||
await expect(flowEditorPage.publishFlowButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('unpublish from layout top bar', async ({}) => {
|
||||
await expect(flowEditorPage.unpublishFlowButton).toBeVisible();
|
||||
await flowEditorPage.unpublishFlowButton.click();
|
||||
await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Unpublished flow.png',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('in layout', () => {
|
||||
test('can go back to flows page', async ({}) => {
|
||||
await flowEditorPage.page.getByTestId('editor-go-back-button').click();
|
||||
await expect(flowEditorPage.page).toHaveURL('/flows');
|
||||
});
|
||||
});
|
22
packages/types/index.d.ts
vendored
22
packages/types/index.d.ts
vendored
@@ -27,6 +27,8 @@ export interface IConnection {
|
||||
flowCount?: number;
|
||||
appData?: IApp;
|
||||
createdAt: string;
|
||||
reconnectable?: boolean;
|
||||
appAuthClientId?: string;
|
||||
}
|
||||
|
||||
export interface IExecutionStep {
|
||||
@@ -247,6 +249,8 @@ export interface IAuth {
|
||||
fields?: IField[];
|
||||
authenticationSteps?: IAuthenticationStep[];
|
||||
reconnectionSteps?: IAuthenticationStep[];
|
||||
sharedAuthenticationSteps?: IAuthenticationStep[];
|
||||
sharedReconnectionSteps?: IAuthenticationStep[];
|
||||
}
|
||||
|
||||
export interface ITriggerOutput {
|
||||
@@ -424,6 +428,24 @@ type TSamlAuthProvider = {
|
||||
defaultRoleId: string;
|
||||
}
|
||||
|
||||
type AppConfig = {
|
||||
id: string;
|
||||
key: string;
|
||||
allowCustomConnection: boolean;
|
||||
canConnect: boolean;
|
||||
canCustomConnect: boolean;
|
||||
shared: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
type AppAuthClient = {
|
||||
id: string;
|
||||
name: string;
|
||||
appConfigId: string;
|
||||
authDefaults: string;
|
||||
formattedAuthDefaults: IJSONObject;
|
||||
}
|
||||
|
||||
declare module 'axios' {
|
||||
interface AxiosResponse {
|
||||
httpError?: IJSONObject;
|
||||
|
@@ -1,17 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import type { IApp, IField, IJSONObject } from '@automatisch/types';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import * as React from 'react';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
import type { IApp, IJSONObject, IField } from '@automatisch/types';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
|
||||
import { processStep } from 'helpers/authenticationSteps';
|
||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import * as URLS from 'config/urls';
|
||||
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { generateExternalLink } from '../../helpers/translationValues';
|
||||
import { Form } from './style';
|
||||
|
||||
@@ -21,24 +23,27 @@ type AddAppConnectionProps = {
|
||||
connectionId?: string;
|
||||
};
|
||||
|
||||
type Response = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export default function AddAppConnection(
|
||||
props: AddAppConnectionProps
|
||||
): React.ReactElement {
|
||||
const { application, connectionId, onClose } = props;
|
||||
const { name, authDocUrl, key, auth } = application;
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const formatMessage = useFormatMessage();
|
||||
const [error, setError] = React.useState<IJSONObject | null>(null);
|
||||
const [inProgress, setInProgress] = React.useState(false);
|
||||
const hasConnection = Boolean(connectionId);
|
||||
const steps = hasConnection
|
||||
? auth?.reconnectionSteps
|
||||
: auth?.authenticationSteps;
|
||||
const useShared = searchParams.get('shared') === 'true';
|
||||
const appAuthClientId = searchParams.get('appAuthClientId') || undefined;
|
||||
const { authenticate } = useAuthenticateApp({
|
||||
appKey: key,
|
||||
connectionId,
|
||||
appAuthClientId,
|
||||
useShared: !!appAuthClientId,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
React.useEffect(function relayProviderData() {
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({
|
||||
source: 'automatisch',
|
||||
@@ -48,51 +53,61 @@ export default function AddAppConnection(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||
async (data) => {
|
||||
if (!steps) return;
|
||||
React.useEffect(
|
||||
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||
if (!appAuthClientId) return;
|
||||
if (!authenticate) return;
|
||||
|
||||
setInProgress(true);
|
||||
setError(null);
|
||||
const asyncAuthenticate = async () => {
|
||||
await authenticate();
|
||||
|
||||
const response: Response = {
|
||||
key,
|
||||
connection: {
|
||||
id: connectionId,
|
||||
},
|
||||
fields: data,
|
||||
navigate(URLS.APP_CONNECTIONS(key));
|
||||
};
|
||||
|
||||
let stepIndex = 0;
|
||||
while (stepIndex < steps.length) {
|
||||
const step = steps[stepIndex];
|
||||
const variables = computeAuthStepVariables(step.arguments, response);
|
||||
|
||||
try {
|
||||
const stepResponse = await processStep(step, variables);
|
||||
|
||||
response[step.name] = stepResponse;
|
||||
} catch (err) {
|
||||
const error = err as IJSONObject;
|
||||
console.log(error);
|
||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
||||
setInProgress(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
stepIndex++;
|
||||
|
||||
if (stepIndex === steps.length) {
|
||||
onClose(response);
|
||||
}
|
||||
}
|
||||
|
||||
setInProgress(false);
|
||||
asyncAuthenticate();
|
||||
},
|
||||
[connectionId, key, steps, onClose]
|
||||
[appAuthClientId, authenticate]
|
||||
);
|
||||
|
||||
const handleClientClick = (appAuthClientId: string) =>
|
||||
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
||||
|
||||
const handleAuthClientsDialogClose = () =>
|
||||
navigate(URLS.APP_CONNECTIONS(key));
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||
async (data) => {
|
||||
if (!authenticate) return;
|
||||
|
||||
setInProgress(true);
|
||||
|
||||
try {
|
||||
const response = await authenticate({
|
||||
fields: data,
|
||||
});
|
||||
onClose(response as Record<string, unknown>);
|
||||
} catch (err) {
|
||||
const error = err as IJSONObject;
|
||||
console.log(error);
|
||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
||||
} finally {
|
||||
setInProgress(false);
|
||||
}
|
||||
},
|
||||
[authenticate]
|
||||
);
|
||||
|
||||
if (useShared)
|
||||
return (
|
||||
<AppAuthClientsDialog
|
||||
appKey={key}
|
||||
onClose={handleAuthClientsDialogClose}
|
||||
onClientClick={handleClientClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (appAuthClientId) return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
||||
<DialogTitle>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user