Compare commits

..

85 Commits

Author SHA1 Message Date
Faruk AYDIN
82031da6a6 Release v0.9.2 2023-08-28 16:30:29 +02:00
Faruk AYDIN
9df5ee7b11 chore: Update version to 0.9.2 in Dockerfiles 2023-08-28 16:29:53 +02:00
Ömer Faruk Aydın
2ed1a57cd9 Merge pull request #1249 from automatisch/permission-contions
chore: Convert conditions of permissions to array
2023-08-28 16:27:29 +02:00
Faruk AYDIN
101450cba6 chore: Convert conditions of permissions to array 2023-08-28 16:24:39 +02:00
Ömer Faruk Aydın
6bab5b3f7c Merge pull request #1248 from automatisch/release/0.9.1
Release v0.9.1
2023-08-28 15:15:25 +02:00
Faruk AYDIN
ca3c0e00a7 Release v0.9.1 2023-08-28 14:47:05 +02:00
Faruk AYDIN
6d64daf324 chore: Update version to 0.9.1 in Dockerfiles 2023-08-28 14:46:26 +02:00
Ömer Faruk Aydın
9ae4578e19 Merge pull request #1247 from automatisch/remove-api-url
chore(web): Remove API url env variable
2023-08-28 14:44:41 +02:00
Faruk AYDIN
e06b7ab87a chore(web): Remove API url env variable 2023-08-28 14:37:10 +02:00
Ömer Faruk Aydın
1e2adedcbf Merge pull request #1246 from automatisch/release/0.9.0
Release v0.9.0
2023-08-28 13:34:24 +02:00
Faruk AYDIN
adf763c1b0 Release v0.9.0 2023-08-28 13:13:23 +02:00
Faruk AYDIN
36ee0df256 chore: Update version to 0.9.0 in Dockerfiles 2023-08-28 13:06:20 +02:00
Rıdvan Akca
823d85b24a feat(custom-logo): constraint svg logo dimensions (#1243) 2023-08-25 21:43:53 +02:00
Rıdvan Akca
a3b3038709 test(user-interface-configuration): write initial tests (#1242)
* test(user-interface): add tests with playwright

* test: refactor UI configuration tests

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-25 21:31:02 +02:00
kattoczko
ddeb18f626 feat: introduce authentication page (#1241)
* feat: introduce authentication page

* feat: update page width

* fix(saml): cover non-existing role mapping on onboarding

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-25 15:24:50 +02:00
Rıdvan Akca
90cd11bd38 feat: align admin pages vertically (#1240) 2023-08-24 16:34:18 +02:00
Ömer Faruk Aydın
e9ba37b8de fix: use withSoftDeleted scope to remove user associations permanently (#1239) 2023-08-24 16:34:07 +02:00
Faruk AYDIN
d5e4a1b1ad fix: Soft delete existing associations of soft deleted users 2023-08-24 15:05:54 +02:00
Faruk AYDIN
129e6d60e5 fix: Remove all related records when user is deleted 2023-08-24 15:05:54 +02:00
Faruk AYDIN
4b77f2f590 fix: Remove deleted flows from Redis 2023-08-24 15:05:54 +02:00
Rıdvan Akca
a909966562 feat(executions): display execution step id (#1232)
* feat(executions): display execution step id

* refactor: remove conditional components in execution steps

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-23 21:53:16 +02:00
Ömer Faruk Aydın
fd184239d6 Merge pull request #1233 from automatisch/enhance-variable-coverage
feat: enhance step variable coverage
2023-08-23 12:35:07 +02:00
Ali BARIN
52bc49dc6a feat: enhance step variable coverage 2023-08-22 16:17:20 +02:00
Ali BARIN
b9352ccc06 fix(mutations/update-flow-status): correct permission check 2023-08-22 16:17:20 +02:00
Ali BARIN
525b2baf06 fix(mutations/execute-flow): correct permission check 2023-08-22 16:17:20 +02:00
Ali BARIN
a8edeb2459 fix(mutations/update-step): correct permission check 2023-08-22 16:17:20 +02:00
Ali BARIN
e3830d64e0 feat: add getSamlAuthProviderRoleMappings query (#1229) 2023-08-22 14:50:01 +02:00
Ali BARIN
91f3e2c2b4 feat: make user.role_id not nullable (#1217) 2023-08-22 14:49:53 +02:00
Ali BARIN
77b4408416 chore: correct e2e test results path in GH actions (#1231) 2023-08-22 10:57:40 +03:00
QAComet
cede96f018 test: refactor create flow test cases with test.step (#1228) 2023-08-22 00:27:10 +02:00
dependabot[bot]
8e0a28d238 chore(deps): bump @node-saml/node-saml from 4.0.4 to 4.0.5 (#1227)
Bumps [@node-saml/node-saml](https://github.com/node-saml/node-saml) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/node-saml/node-saml/releases)
- [Changelog](https://github.com/node-saml/node-saml/blob/v4.0.5/CHANGELOG.md)
- [Commits](https://github.com/node-saml/node-saml/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: "@node-saml/node-saml"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 23:35:38 +02:00
Rıdvan Akca
da5d594428 feat(user-interface): introduce user interface page (#1226) 2023-08-21 23:11:25 +02:00
kattoczko
9f9ee0bb58 feat: create clear button for ControlledCustomAutocomplete (#1222)
Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-21 22:52:59 +02:00
Rıdvan Akca
163aca6179 feat(user-list): add pagination (#1219)
* feat(user-list): add pagination

* feat: add actual total count in getUsers

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-21 21:15:07 +02:00
Ali BARIN
cb06d3b0ae test: add in-between assertions and more fixtures (#1224) 2023-08-18 18:34:52 +02:00
Ali BARIN
dbe18dd100 chore: configure login env. vars. in e2e test workflow (#1221) 2023-08-18 09:48:41 +02:00
Ali BARIN
217970667a chore: make e2e tests manually triggerable (#1220) 2023-08-18 10:25:43 +03:00
Rıdvan Akca
dace794167 feat: introduce playwright (#1194)
* feat: introduce playwright

* test: migrate apps folder to playwright (#1201)

* test: rewrite connections tests with playwright (#1203)

* test: rewrite executions tests with playwright (#1207)

* test: rewrite flow editor tests with playwright (#1212)

* test(flow-editor): rewrite tests using serial mode (#1218)

* test: update custom connection creation paths

* test: move login logic to page fixture

* test: remove cypress tests and deps

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-08-17 23:31:38 +02:00
Rıdvan Akca
590780a539 feat(user-list): display user role (#1215) 2023-08-16 19:26:49 +02:00
Ömer Faruk Aydın
cbd1f47e87 fix(formatter): capitalize all words without trimming any data (#1216) 2023-08-16 19:21:49 +02:00
Ömer Faruk Aydın
f89cff4e4a Merge pull request #1214 from automatisch/formatter-integration
feat: Implement initial version of formatter app
2023-08-16 19:08:43 +02:00
Faruk AYDIN
cb08e0bf9f feat: Implement initial version of formatter app 2023-08-16 18:59:36 +02:00
Ali BARIN
3b54b29a99 feat: introduce app configs with shared auth clients (#1213) 2023-08-16 15:46:43 +02:00
Ali BARIN
25983e046c chore: move config behind checks (#1211) 2023-08-11 22:32:13 +02:00
Ömer Faruk Aydın
a6a124d2e6 feat: add role mappings for SAML configuration (#1210) 2023-08-11 19:07:39 +02:00
Ali BARIN
c7e1d30553 fix(get-apps): fetch additionalFields for triggers (#1209) 2023-08-11 16:24:09 +02:00
Ömer Faruk Aydın
6cc8c45634 Merge pull request #1208 from automatisch/docs-postgresql
docs: Add warning for PostgreSQL version
2023-08-11 16:07:37 +02:00
Ömer Faruk Aydın
ee9a9114b7 Merge pull request #1205 from automatisch/white-labelling
feat: introduce dynamic configuration
2023-08-11 16:02:45 +02:00
Faruk AYDIN
11f00f866c docs: Add warning for PostgreSQL version 2023-08-11 16:00:05 +02:00
Ali BARIN
03ea61ba81 feat: use dynamic custom logo 2023-08-11 08:29:57 +00:00
Ali BARIN
f6c500c998 feat: use dynamic custom theme 2023-08-11 08:29:57 +00:00
Ali BARIN
b590f0f98f feat: write useConfig hook 2023-08-11 08:29:57 +00:00
Ali BARIN
ef9359b208 feat: write updateConfig GQL mutation 2023-08-11 08:29:57 +00:00
Ali BARIN
efd243a340 feat: create getConfig GQL query 2023-08-11 08:29:57 +00:00
Ali BARIN
bafb8b86db feat: create Config model 2023-08-11 08:29:57 +00:00
Ömer Faruk Aydın
84b701747f feat: add license info in getAutomatischInfo query (#1202) 2023-08-10 23:18:10 +02:00
Ali BARIN
ec42446daa feat(wordpress): add auth and new post trigger (#1160) 2023-08-09 22:34:21 +02:00
Rıdvan Akca
5046c4c911 feat(auth): add loading state for user and role management (#1188) 2023-08-09 21:51:07 +02:00
Ömer Faruk Aydın
ce8c9906cb chore: Rename createSamlAuthProvider mutation as upsertSamlAuthProvider (#1200) 2023-08-08 22:56:23 +02:00
Ömer Faruk Aydın
6fb5482bba Merge pull request #1199 from automatisch/get-saml-auth-provider
feat: Implement getSamlAuthProvider graphQL query
2023-08-07 16:51:33 +02:00
Faruk AYDIN
58189963f5 feat: Implement getSamlAuthProvider graphQL query 2023-08-07 16:48:36 +02:00
Ömer Faruk Aydın
f488a71304 Merge pull request #1198 from automatisch/list-saml-auth-providers
Rename getSamlAuthProviders as listSamlAuthProviders query
2023-08-07 16:48:06 +02:00
Faruk AYDIN
4b706e004d chore: Rename getSamlAuthProviders as listSamlAuthProviders query 2023-08-07 16:44:59 +02:00
Ömer Faruk Aydın
40e10cc270 Merge pull request #1196 from automatisch/remove-role-check
chore: Warn user about default role of SAML before deleting role
2023-08-07 15:31:13 +02:00
Ömer Faruk Aydın
41db227eb3 Merge pull request #1195 from automatisch/saml-configuration-create
feat: Add createSamlAuthProvider graphQL mutation
2023-08-07 15:30:52 +02:00
Faruk AYDIN
43eea965c5 chore: Warn user about default role of SAML before deleting role 2023-08-07 15:21:32 +02:00
Faruk AYDIN
8101c9f0bc feat: Add createSamlAuthProvider graphQL mutation 2023-08-07 15:02:25 +02:00
Rıdvan Akca
b4cda90338 feat(auth): add feedback state for user and role management (#1191) 2023-08-07 11:08:29 +02:00
Ali BARIN
7ca37c412e fix: clone base db queries 2023-08-03 21:11:59 +02:00
Ali BARIN
e4e3356dc9 fix: add fallback for api url 2023-08-03 20:19:02 +02:00
Ali BARIN
0deaa03218 feat(auth): add user and role management 2023-08-03 19:39:48 +02:00
Ali BARIN
a7104c41a2 feat(sso): introduce authentication with SAML 2023-08-03 19:39:48 +02:00
Ali BARIN
5176b8c322 feat(authorization): add update connection checks 2023-08-03 19:39:48 +02:00
Ali BARIN
c37c70446d feat(authorization): add read connection checks 2023-08-03 19:39:48 +02:00
Ali BARIN
63abc8a2c8 feat(authorization): add delete flow checks 2023-08-03 19:39:48 +02:00
Ali BARIN
ba5c038e3b feat(authorization): add create flow checks 2023-08-03 19:39:48 +02:00
Ali BARIN
a6669415f5 feat(authorization): add delete connection checks 2023-08-03 19:39:48 +02:00
Ali BARIN
4086fad867 feat(authorization): add create connection checks 2023-08-03 19:39:48 +02:00
Ali BARIN
8a71c13078 feat(authorization): add read execution checks 2023-08-03 19:39:48 +02:00
Ali BARIN
5d77f64e76 feat(authorization): add update flow checks 2023-08-03 19:39:48 +02:00
Ali BARIN
0d092b977f feat(authorization): add read flow checks 2023-08-03 19:39:48 +02:00
Ali BARIN
69582ff83d feat: introduce role based access control 2023-08-03 19:39:48 +02:00
Ömer Faruk Aydın
a5c7da331a Merge pull request #1190 from automatisch/docs-available-apps
docs: Remove warning from available apps
2023-08-02 17:31:07 +02:00
Faruk AYDIN
8e842296b7 docs: Remove warning from available apps 2023-08-02 17:19:53 +02:00
Ömer Faruk Aydın
7db14d1df7 Merge pull request #1189 from automatisch/release/0.8.0
Release v0.8.0
2023-08-02 15:48:33 +02:00
310 changed files with 9316 additions and 1447 deletions

View File

@@ -33,7 +33,32 @@ services:
- '6379:6379' - '6379:6379'
expose: expose:
- 6379 - 6379
keycloak:
image: quay.io/keycloak/keycloak:21.1
restart: always
container_name: keycloak
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
- KC_DB=postgres
- KC_DB_URL_HOST=postgres
- KC_DB_URL_DATABASE=keycloak
- KC_DB_USERNAME=automatisch_user
- KC_DB_PASSWORD=automatisch_password
- KC_HEALTH_ENABLED=true
ports:
- "8080:8080"
command: start-dev
depends_on:
- postgres
healthcheck:
test: "curl -f http://localhost:8080/health/ready || exit 1"
volumes:
- keycloak:/opt/keycloak/data/
expose:
- 8080
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:
keycloak:

31
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Automatisch UI Tests
on:
schedule:
- cron: '0 12 * * *'
workflow_dispatch:
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
working-directory: ./packages/e2e-tests
env:
LOGIN_EMAIL: ${{ secrets.LOGIN_EMAIL }}
LOGIN_PASSWORD: ${{ secrets.LOGIN_PASSWORD }}
run: yarn test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: test-results/
retention-days: 30

View File

@@ -4,7 +4,7 @@ WORKDIR /automatisch
RUN \ RUN \
apk --no-cache add --virtual build-dependencies python3 build-base && \ apk --no-cache add --virtual build-dependencies python3 build-base && \
yarn global add @automatisch/cli@0.8.0 --network-timeout 1000000 && \ yarn global add @automatisch/cli@0.9.2 --network-timeout 1000000 && \
rm -rf /usr/local/share/.cache/ && \ rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies apk del build-dependencies

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM automatischio/automatisch:0.8.0 FROM automatischio/automatisch:0.9.2
WORKDIR /automatisch WORKDIR /automatisch
RUN apk add --no-cache openssl dos2unix RUN apk add --no-cache openssl dos2unix

View File

@@ -2,7 +2,7 @@
"packages": [ "packages": [
"packages/*" "packages/*"
], ],
"version": "0.8.0", "version": "0.9.2",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"command": { "command": {

View File

@@ -2,18 +2,33 @@ import appConfig from '../../src/config/app';
import logger from '../../src/helpers/logger'; import logger from '../../src/helpers/logger';
import client from './client'; import client from './client';
import User from '../../src/models/user'; import User from '../../src/models/user';
import Role from '../../src/models/role';
import '../../src/config/orm'; import '../../src/config/orm';
async function fetchAdminRole() {
const role = await Role
.query()
.where({
key: 'admin'
})
.limit(1)
.first();
return role;
}
export async function createUser( export async function createUser(
email = 'user@automatisch.io', email = 'user@automatisch.io',
password = 'sample' password = 'sample'
) { ) {
const UNIQUE_VIOLATION_CODE = '23505'; const UNIQUE_VIOLATION_CODE = '23505';
const role = await fetchAdminRole();
const userParams = { const userParams = {
email, email,
password, password,
fullName: 'Initial admin', fullName: 'Initial admin',
role: 'admin', roleId: role.id,
}; };
try { try {

View File

@@ -12,6 +12,7 @@ const knexConfig = {
database: appConfig.postgresDatabase, database: appConfig.postgresDatabase,
ssl: appConfig.postgresEnableSsl, ssl: appConfig.postgresEnableSsl,
}, },
asyncStackTraces: appConfig.isDev,
searchPath: [appConfig.postgresSchema], searchPath: [appConfig.postgresSchema],
pool: { min: 0, max: 20 }, pool: { min: 0, max: 20 },
migrations: { migrations: {

View File

@@ -1,10 +1,10 @@
{ {
"name": "@automatisch/backend", "name": "@automatisch/backend",
"version": "0.8.0", "version": "0.9.2",
"license": "See LICENSE file", "license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": { "scripts": {
"dev": "ts-node-dev --exit-child src/server.ts", "dev": "ts-node-dev --watch 'src/graphql/schema.graphql' --exit-child src/server.ts",
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts", "worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
"build": "tsc && yarn copy-statics", "build": "tsc && yarn copy-statics",
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts", "build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
@@ -22,14 +22,17 @@
"prebuild": "rm -rf ./dist" "prebuild": "rm -rf ./dist"
}, },
"dependencies": { "dependencies": {
"@automatisch/web": "^0.8.0", "@automatisch/web": "^0.9.2",
"@bull-board/express": "^3.10.1", "@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0",
"@graphql-tools/graphql-file-loader": "^7.3.4", "@graphql-tools/graphql-file-loader": "^7.3.4",
"@graphql-tools/load": "^7.5.2", "@graphql-tools/load": "^7.5.2",
"@node-saml/passport-saml": "^4.0.4",
"@rudderstack/rudder-sdk-node": "^1.1.2", "@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0", "@sentry/node": "^7.42.0",
"@sentry/tracing": "^7.42.0", "@sentry/tracing": "^7.42.0",
"@types/luxon": "^2.3.1", "@types/luxon": "^2.3.1",
"@types/passport": "^1.0.12",
"@types/xmlrpc": "^1.3.7", "@types/xmlrpc": "^1.3.7",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
"axios": "0.24.0", "axios": "0.24.0",
@@ -59,11 +62,14 @@
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"node-html-markdown": "^1.3.0",
"nodemailer": "6.7.0", "nodemailer": "6.7.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"objection": "^3.0.0", "objection": "^3.0.0",
"passport": "^0.6.0",
"pg": "^8.7.1", "pg": "^8.7.1",
"php-serialize": "^4.0.2", "php-serialize": "^4.0.2",
"showdown": "^2.1.0",
"stripe": "^11.13.0", "stripe": "^11.13.0",
"winston": "^3.7.1", "winston": "^3.7.1",
"xmlrpc": "^1.3.2" "xmlrpc": "^1.3.2"
@@ -104,7 +110,7 @@
"url": "https://github.com/automatisch/automatisch/issues" "url": "https://github.com/automatisch/automatisch/issues"
}, },
"devDependencies": { "devDependencies": {
"@automatisch/types": "^0.8.0", "@automatisch/types": "^0.9.2",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8", "@types/bull": "^3.15.8",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
@@ -120,6 +126,7 @@
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/pg": "^8.6.1", "@types/pg": "^8.6.1",
"@types/pino": "^7.0.5", "@types/pino": "^7.0.5",
"@types/showdown": "^2.0.1",
"ava": "^3.15.0", "ava": "^3.15.0",
"nodemon": "^2.0.13", "nodemon": "^2.0.13",
"sinon": "^11.1.2", "sinon": "^11.1.2",

View File

@@ -17,6 +17,7 @@ import {
} from './helpers/create-bull-board-handler'; } from './helpers/create-bull-board-handler';
import injectBullBoardHandler from './helpers/inject-bull-board-handler'; import injectBullBoardHandler from './helpers/inject-bull-board-handler';
import router from './routes'; import router from './routes';
import configurePassport from './helpers/passport';
createBullBoardHandler(serverAdapter); createBullBoardHandler(serverAdapter);
@@ -50,6 +51,9 @@ app.use(
}) })
); );
app.use(cors(corsOptions)); app.use(cors(corsOptions));
configurePassport(app);
app.use('/', router); app.use('/', router);
webUIHandler(app); webUIHandler(app);

View File

@@ -0,0 +1,3 @@
import text from './text';
export default [text];

View 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,
},
});
},
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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

View File

@@ -0,0 +1,3 @@
import listTransformOptions from './list-transform-options';
export default [listTransformOptions];

View File

@@ -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];
},
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

View 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,
});

View File

@@ -25,6 +25,12 @@ const verifyCredentials = async ($: IGlobalVariable) => {
$.auth.data.accessToken = data.access_token; $.auth.data.accessToken = data.access_token;
const currentUser = await getCurrentUser($); const currentUser = await getCurrentUser($);
const screenName = [
currentUser.username,
$.auth.data.instanceUrl,
]
.filter(Boolean)
.join(' @ ');
await $.auth.set({ await $.auth.set({
clientId: $.auth.data.clientId, clientId: $.auth.data.clientId,
@@ -34,7 +40,7 @@ const verifyCredentials = async ($: IGlobalVariable) => {
scope: data.scope, scope: data.scope,
tokenType: data.token_type, tokenType: data.token_type,
userId: currentUser.id, userId: currentUser.id,
screenName: `${currentUser.username} @ ${$.auth.data.instanceUrl}`, screenName,
}); });
}; };

View 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

View File

@@ -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,
});
}

View 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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
import { IGlobalVariable } from '@automatisch/types';
const getInstanceUrl = ($: IGlobalVariable): string => {
return $.auth.data.instanceUrl as string;
};
export default getInstanceUrl;

View 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;

View File

@@ -0,0 +1,3 @@
import listStatuses from './list-statuses';
export default [listStatuses];

View File

@@ -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;
},
};

View File

View 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,
});

View File

@@ -0,0 +1,3 @@
import newPost from './new-post';
export default [newPost];

View File

@@ -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);
},
});

View File

@@ -0,0 +1,46 @@
import { Knex } from 'knex';
import capitalize from 'lodash/capitalize';
import lowerCase from 'lodash/lowerCase';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('roles', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('name').notNullable();
table.string('key').notNullable();
table.string('description');
table.timestamps(true, true);
});
const uniqueUserRoles = await knex('users')
.select('role')
.groupBy('role');
let shouldCreateAdminRole = true;
for (const { role } of uniqueUserRoles) {
// skip empty roles
if (!role) continue;
const lowerCaseRole = lowerCase(role);
if (lowerCaseRole === 'admin') {
shouldCreateAdminRole = false;
}
await knex('roles').insert({
name: capitalize(role),
key: lowerCaseRole,
});
}
if (shouldCreateAdminRole) {
await knex('roles').insert({
name: 'Admin',
key: 'admin',
});
}
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('roles');
}

View File

@@ -0,0 +1,48 @@
import { Knex } from 'knex';
const getPermissionForRole = (roleId: string, subject: string, actions: string[], conditions: string[] = []) => actions
.map(action => ({
role_id: roleId,
subject,
action,
conditions,
}));
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('permissions', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('role_id').references('id').inTable('roles');
table.string('action').notNullable();
table.string('subject').notNullable();
table.jsonb('conditions').notNullable().defaultTo([]);
table.timestamps(true, true);
});
const roles = await knex('roles').select(['id', 'key']) as { id: string, key: string }[];
for (const role of roles) {
// `admin` role should have no conditions unlike others by default
const isAdmin = role.key === 'admin';
const roleConditions = isAdmin ? [] : ['isCreator'];
// default permissions
await knex('permissions').insert([
...getPermissionForRole(role.id, 'Connection', ['create', 'read', 'delete', 'update'], roleConditions),
...getPermissionForRole(role.id, 'Execution', ['read'], roleConditions),
...getPermissionForRole(role.id, 'Flow', ['create', 'delete', 'publish', 'read', 'update'], roleConditions),
]);
// admin specific permission
if (isAdmin) {
await knex('permissions').insert([
...getPermissionForRole(role.id, 'User', ['create', 'read', 'delete', 'update']),
...getPermissionForRole(role.id, 'Role', ['create', 'read', 'delete', 'update']),
]);
}
}
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('permissions');
}

View File

@@ -0,0 +1,29 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.table('users', async (table) => {
table.uuid('role_id').references('id').inTable('roles');
});
const theRole = await knex('roles').select('id').limit(1).first();
const roles = await knex('roles').select('id', 'key');
for (const role of roles) {
await knex('users')
.where({
role: role.key
})
.update({
role_id: role.id
});
}
// backfill not-migratables
await knex('users').whereNull('role_id').update({ role_id: theRole.id });
}
export async function down(knex: Knex): Promise<void> {
return await knex.schema.table('users', (table) => {
table.dropColumn('role_id');
});
}

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.table('users', async (table) => {
table.dropColumn('role');
});
}
export async function down(knex: Knex): Promise<void> {
return await knex.schema.table('users', (table) => {
table.string('role').defaultTo('user');
});
}

View File

@@ -0,0 +1,24 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('saml_auth_providers', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('name').notNullable();
table.text('certificate').notNullable();
table.string('signature_algorithm').notNullable();
table.string('issuer').notNullable();
table.text('entry_point').notNullable();
table.text('firstname_attribute_name').notNullable();
table.text('surname_attribute_name').notNullable();
table.text('email_attribute_name').notNullable();
table.text('role_attribute_name').notNullable();
table.uuid('default_role_id').references('id').inTable('roles');
table.boolean('active').defaultTo(false);
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('saml_auth_providers');
}

View File

@@ -0,0 +1,17 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('identities', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('user_id').references('id').inTable('users');
table.string('remote_id').notNullable();
table.string('provider_id').notNullable();
table.string('provider_type').notNullable();
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('identities');
}

View File

@@ -0,0 +1,11 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return await knex.schema.alterTable('users', (table) => {
table.string('password').nullable().alter();
});
}
export async function down(): Promise<void> {
// void
}

View File

@@ -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, 'SamlAuthProvider', [
'create',
'read',
'delete',
'update',
])
);
}
export async function down(knex: Knex): Promise<void> {
await knex('permissions').where({ subject: 'SamlAuthProvider' }).delete();
}

View File

@@ -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');
}

View File

@@ -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();
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
});
}

View File

@@ -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();
}

View File

@@ -0,0 +1,25 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
const role = await knex('roles')
.select('id')
.whereIn('key', ['user', 'admin'])
.orderBy('key', 'desc')
.limit(1)
.first();
if (role) {
// backfill nulls
await knex('users').whereNull('role_id').update({ role_id: role.id });
}
return await knex.schema.alterTable('users', (table) => {
table.uuid('role_id').notNullable().alter();
});
}
export async function down(knex: Knex): Promise<void> {
return await knex.schema.alterTable('users', (table) => {
table.uuid('role_id').nullable().alter();
});
}

View File

@@ -0,0 +1,35 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
const users = await knex('users').whereNotNull('deleted_at');
const userIds = users.map((user) => user.id);
const flows = await knex('flows').whereIn('user_id', userIds);
const flowIds = flows.map((flow) => flow.id);
const executions = await knex('executions').whereIn('flow_id', flowIds);
const executionIds = executions.map((execution) => execution.id);
await knex('execution_steps').whereIn('execution_id', executionIds).update({
deleted_at: knex.fn.now(),
});
await knex('executions').whereIn('id', executionIds).update({
deleted_at: knex.fn.now(),
});
await knex('steps').whereIn('flow_id', flowIds).update({
deleted_at: knex.fn.now(),
});
await knex('flows').whereIn('id', flowIds).update({
deleted_at: knex.fn.now(),
});
await knex('connections').whereIn('user_id', userIds).update({
deleted_at: knex.fn.now(),
});
}
export async function down(): Promise<void> {
// void
}

View File

@@ -0,0 +1,11 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex('permissions')
.where(knex.raw('conditions::text'), '=', knex.raw("'{}'::text"))
.update('conditions', JSON.stringify([]));
}
export async function down(): Promise<void> {
// void
}

View File

@@ -1,47 +1,73 @@
import createAppAuthClient from './mutations/create-app-auth-client.ee';
import createAppConfig from './mutations/create-app-config.ee';
import createConnection from './mutations/create-connection'; import createConnection from './mutations/create-connection';
import generateAuthUrl from './mutations/generate-auth-url';
import updateConnection from './mutations/update-connection';
import resetConnection from './mutations/reset-connection';
import verifyConnection from './mutations/verify-connection';
import deleteConnection from './mutations/delete-connection';
import createFlow from './mutations/create-flow'; import createFlow from './mutations/create-flow';
import createRole from './mutations/create-role.ee';
import createStep from './mutations/create-step';
import createUser from './mutations/create-user.ee';
import deleteConnection from './mutations/delete-connection';
import deleteCurrentUser from './mutations/delete-current-user.ee';
import deleteFlow from './mutations/delete-flow';
import deleteRole from './mutations/delete-role.ee';
import deleteStep from './mutations/delete-step';
import deleteUser from './mutations/delete-user.ee';
import duplicateFlow from './mutations/duplicate-flow';
import executeFlow from './mutations/execute-flow';
import forgotPassword from './mutations/forgot-password.ee';
import generateAuthUrl from './mutations/generate-auth-url';
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'; import updateFlow from './mutations/update-flow';
import updateFlowStatus from './mutations/update-flow-status'; import updateFlowStatus from './mutations/update-flow-status';
import executeFlow from './mutations/execute-flow'; import updateRole from './mutations/update-role.ee';
import deleteFlow from './mutations/delete-flow';
import duplicateFlow from './mutations/duplicate-flow';
import createStep from './mutations/create-step';
import updateStep from './mutations/update-step'; import updateStep from './mutations/update-step';
import deleteStep from './mutations/delete-step'; import updateUser from './mutations/update-user.ee';
import createUser from './mutations/create-user.ee'; import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee';
import deleteUser from './mutations/delete-user.ee'; import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee';
import updateUser from './mutations/update-user'; import verifyConnection from './mutations/verify-connection';
import forgotPassword from './mutations/forgot-password.ee';
import resetPassword from './mutations/reset-password.ee';
import login from './mutations/login';
const mutationResolvers = { const mutationResolvers = {
createAppAuthClient,
createAppConfig,
createConnection, createConnection,
generateAuthUrl,
updateConnection,
resetConnection,
verifyConnection,
deleteConnection,
createFlow, createFlow,
createRole,
createStep,
createUser,
deleteConnection,
deleteCurrentUser,
deleteFlow,
deleteRole,
deleteStep,
deleteUser,
duplicateFlow,
executeFlow,
forgotPassword,
generateAuthUrl,
login,
registerUser,
resetConnection,
resetPassword,
updateAppAuthClient,
updateAppConfig,
updateConfig,
updateConnection,
updateCurrentUser,
updateFlow, updateFlow,
updateFlowStatus, updateFlowStatus,
executeFlow, updateRole,
deleteFlow,
duplicateFlow,
createStep,
updateStep, updateStep,
deleteStep,
createUser,
deleteUser,
updateUser, updateUser,
forgotPassword, upsertSamlAuthProvider,
resetPassword, upsertSamlAuthProvidersRoleMappings,
login, verifyConnection,
}; };
export default mutationResolvers; export default mutationResolvers;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,25 +1,59 @@
import App from '../../models/app';
import Context from '../../types/express/context';
import { IJSONObject } from '@automatisch/types'; import { IJSONObject } from '@automatisch/types';
import App from '../../models/app';
import AppConfig from '../../models/app-config';
import Context from '../../types/express/context';
type Params = { type Params = {
input: { input: {
key: string; key: string;
appAuthClientId: string;
formattedData: IJSONObject; formattedData: IJSONObject;
}; };
}; };
const createConnection = async ( const createConnection = async (
_parent: unknown, _parent: unknown,
params: Params, params: Params,
context: Context context: Context
) => { ) => {
await App.findOneByKey(params.input.key); context.currentUser.can('create', 'Connection');
return await context.currentUser.$relatedQuery('connections').insert({ const { key, appAuthClientId } = params.input;
key: params.input.key,
formattedData: params.input.formattedData, const app = await App.findOneByKey(key);
verified: false,
}); 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; export default createConnection;

View File

@@ -14,6 +14,8 @@ const createFlow = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('create', 'Flow');
const connectionId = params?.input?.connectionId; const connectionId = params?.input?.connectionId;
const appKey = params?.input?.triggerAppKey; const appKey = params?.input?.triggerAppKey;

View File

@@ -0,0 +1,34 @@
import kebabCase from 'lodash/kebabCase';
import Permission from '../../models/permission';
import Role from '../../models/role';
import Context from '../../types/express/context';
type Params = {
input: {
name: string;
description: string;
permissions: Permission[];
};
};
const createRole = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('create', 'Role');
const { name, description, permissions } = params.input;
const key = kebabCase(name);
const existingRole = await Role.query().findOne({ key });
if (existingRole) {
throw new Error('Role already exists!');
}
return await Role.query().insertGraph({
key,
name,
description,
permissions,
}, { relate: ['permissions'] }).returning('*');
};
export default createRole;

View File

@@ -22,6 +22,8 @@ const createStep = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('update', 'Flow');
const { input } = params; const { input } = params;
if (input.appKey && input.key) { if (input.appKey && input.key) {

View File

@@ -1,14 +1,21 @@
import User from '../../models/user'; import User from '../../models/user';
import Role from '../../models/role';
import Context from '../../types/express/context';
type Params = { type Params = {
input: { input: {
fullName: string; fullName: string;
email: string; email: string;
password: string; password: string;
role: {
id: string;
};
}; };
}; };
const createUser = async (_parent: unknown, params: Params) => { const createUser = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('create', 'User');
const { fullName, email, password } = params.input; const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({ email }); const existingUser = await User.query().findOne({ email });
@@ -17,12 +24,23 @@ const createUser = async (_parent: unknown, params: Params) => {
throw new Error('User already exists!'); throw new Error('User already exists!');
} }
const user = await User.query().insert({ const userPayload: Partial<User> = {
fullName, fullName,
email, email,
password, password,
role: 'user', };
});
try {
context.currentUser.can('update', 'Role');
userPayload.roleId = params.input.role.id;
} catch {
// void
const role = await Role.query().findOne({ key: 'user' });
userPayload.roleId = role.id;
}
const user = await User.query().insert(userPayload);
return user; return user;
}; };

View File

@@ -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;

View File

@@ -11,6 +11,8 @@ const deleteConnection = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('delete', 'Connection');
await context.currentUser await context.currentUser
.$relatedQuery('connections') .$relatedQuery('connections')
.delete() .delete()

View File

@@ -0,0 +1,64 @@
import { Duration } from 'luxon';
import Context from '../../types/express/context';
import deleteUserQueue from '../../queues/delete-user.ee';
import flowQueue from '../../queues/flow';
import Flow from '../../models/flow';
import Execution from '../../models/execution';
import ExecutionStep from '../../models/execution-step';
import appConfig from '../../config/app';
const deleteCurrentUser = async (
_parent: unknown,
params: never,
context: Context
) => {
const id = context.currentUser.id;
const flows = await context.currentUser.$relatedQuery('flows').where({
active: true,
});
const repeatableJobs = await flowQueue.getRepeatableJobs();
for (const flow of flows) {
const job = repeatableJobs.find((job) => job.id === flow.id);
if (job) {
await flowQueue.removeRepeatableByKey(job.key);
}
}
const executionIds = (
await context.currentUser
.$relatedQuery('executions')
.select('executions.id')
).map((execution: Execution) => execution.id);
const flowIds = flows.map((flow) => flow.id);
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
await context.currentUser.$relatedQuery('executions').delete();
await context.currentUser.$relatedQuery('steps').delete();
await Flow.query().whereIn('id', flowIds).delete();
await context.currentUser.$relatedQuery('connections').delete();
await context.currentUser.$relatedQuery('identities').delete();
if (appConfig.isCloud) {
await context.currentUser.$relatedQuery('subscriptions').delete();
await context.currentUser.$relatedQuery('usageData').delete();
}
await context.currentUser.$query().delete();
const jobName = `Delete user - ${id}`;
const jobPayload = { id };
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobOptions = {
delay: millisecondsFor30Days,
};
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
return true;
};
export default deleteCurrentUser;

View File

@@ -13,6 +13,8 @@ const deleteFlow = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('delete', 'Flow');
const flow = await context.currentUser const flow = await context.currentUser
.$relatedQuery('flows') .$relatedQuery('flows')
.findOne({ .findOne({

View File

@@ -0,0 +1,47 @@
import Role from '../../models/role';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
type Params = {
input: {
id: string;
};
};
const deleteRole = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'Role');
const role = await Role.query().findById(params.input.id).throwIfNotFound();
const count = await role.$relatedQuery('users').resultSize();
if (count > 0) {
throw new Error('All users must be migrated away from the role!');
}
if (role.isAdmin) {
throw new Error('Admin role cannot be deleted!');
}
const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query()
.where({ default_role_id: role.id })
.limit(1)
.first();
if (samlAuthProviderUsingDefaultRole) {
throw new Error(
'You need to change the default role in the SAML configuration before deleting this role.'
);
}
// delete permissions first
await role.$relatedQuery('permissions').delete();
await role.$query().delete();
return true;
};
export default deleteRole;

View File

@@ -11,6 +11,8 @@ const deleteStep = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('update', 'Flow');
const step = await context.currentUser const step = await context.currentUser
.$relatedQuery('steps') .$relatedQuery('steps')
.withGraphFetched('flow') .withGraphFetched('flow')

View File

@@ -1,11 +1,24 @@
import Context from '../../types/express/context';
import deleteUserQueue from '../../queues/delete-user.ee';
import { Duration } from 'luxon'; import { Duration } from 'luxon';
import Context from '../../types/express/context';
import User from '../../models/user';
import deleteUserQueue from '../../queues/delete-user.ee';
const deleteUser = async (_parent: unknown, params: never, context: Context) => { type Params = {
const id = context.currentUser.id; input: {
id: string;
};
};
await context.currentUser.$query().delete(); const deleteUser = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'User');
const id = params.input.id;
await User.query().deleteById(id);
const jobName = `Delete user - ${id}`; const jobName = `Delete user - ${id}`;
const jobPayload = { id }; const jobPayload = { id };

View File

@@ -53,6 +53,8 @@ const duplicateFlow = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('create', 'Flow');
const flow = await context.currentUser const flow = await context.currentUser
.$relatedQuery('flows') .$relatedQuery('flows')
.withGraphJoined('[steps]') .withGraphJoined('[steps]')

View File

@@ -1,5 +1,6 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import testRun from '../../services/test-run'; import testRun from '../../services/test-run';
import Step from '../../models/step';
type Params = { type Params = {
input: { input: {
@@ -12,10 +13,16 @@ const executeFlow = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('update', 'Flow');
const isCreator = conditions.isCreator;
const allSteps = Step.query();
const userSteps = context.currentUser.$relatedQuery('steps');
const baseQuery = isCreator ? userSteps : allSteps;
const { stepId } = params.input; const { stepId } = params.input;
const untilStep = await context.currentUser const untilStep = await baseQuery
.$relatedQuery('steps') .clone()
.findById(stepId) .findById(stepId)
.throwIfNotFound(); .throwIfNotFound();

View File

@@ -13,6 +13,8 @@ const generateAuthUrl = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('create', 'Connection');
const connection = await context.currentUser const connection = await context.currentUser
.$relatedQuery('connections') .$relatedQuery('connections')
.findOne({ .findOne({

View File

@@ -0,0 +1,33 @@
import User from '../../models/user';
import Role from '../../models/role';
type Params = {
input: {
fullName: string;
email: string;
password: string;
};
};
const registerUser = async (_parent: unknown, params: Params) => {
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({ email });
if (existingUser) {
throw new Error('User already exists!');
}
const role = await Role.query().findOne({ key: 'user' });
const user = await User.query().insert({
fullName,
email,
password,
roleId: role.id,
});
return user;
};
export default registerUser;

View File

@@ -11,6 +11,8 @@ const resetConnection = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('create', 'Connection');
let connection = await context.currentUser let connection = await context.currentUser
.$relatedQuery('connections') .$relatedQuery('connections')
.findOne({ .findOne({

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,52 @@
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];
if (newValue) {
const entryUpdate = Config.query()
.insert({
key,
value: {
data: newValue,
},
})
.onConflict('key')
.merge({
value: {
data: newValue,
},
});
updates.push(entryUpdate);
} else {
const entryUpdate = Config.query().findOne({ key }).delete();
updates.push(entryUpdate);
}
}
await Promise.all(updates);
return config;
};
export default updateConfig;

View File

@@ -1,10 +1,12 @@
import Context from '../../types/express/context';
import { IJSONObject } from '@automatisch/types'; import { IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context';
import AppAuthClient from '../../models/app-auth-client';
type Params = { type Params = {
input: { input: {
id: string; id: string;
formattedData: IJSONObject; formattedData?: IJSONObject;
appAuthClientId?: string;
}; };
}; };
@@ -13,6 +15,8 @@ const updateConnection = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('create', 'Connection');
let connection = await context.currentUser let connection = await context.currentUser
.$relatedQuery('connections') .$relatedQuery('connections')
.findOne({ .findOne({
@@ -20,10 +24,21 @@ const updateConnection = async (
}) })
.throwIfNotFound(); .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({ connection = await connection.$query().patchAndFetch({
formattedData: { formattedData: {
...connection.formattedData, ...connection.formattedData,
...params.input.formattedData, ...formattedData,
}, },
}); });

View File

@@ -8,7 +8,7 @@ type Params = {
}; };
}; };
const updateUser = async ( const updateCurrentUser = async (
_parent: unknown, _parent: unknown,
params: Params, params: Params,
context: Context context: Context
@@ -22,4 +22,4 @@ const updateUser = async (
return user; return user;
}; };
export default updateUser; export default updateCurrentUser;

View File

@@ -1,3 +1,4 @@
import Flow from '../../models/flow';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import flowQueue from '../../queues/flow'; import flowQueue from '../../queues/flow';
import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../../helpers/remove-job-configuration'; import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../../helpers/remove-job-configuration';
@@ -18,8 +19,14 @@ const updateFlowStatus = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
let flow = await context.currentUser const conditions = context.currentUser.can('publish', 'Flow');
.$relatedQuery('flows') const isCreator = conditions.isCreator;
const allFlows = Flow.query();
const userFlows = context.currentUser.$relatedQuery('flows');
const baseQuery = isCreator ? userFlows : allFlows;
let flow = await baseQuery
.clone()
.findOne({ .findOne({
id: params.input.id, id: params.input.id,
}) })
@@ -55,7 +62,7 @@ const updateFlowStatus = async (
} else { } else {
if (newActiveValue) { if (newActiveValue) {
flow = await flow.$query().patchAndFetch({ flow = await flow.$query().patchAndFetch({
published_at: new Date().toISOString(), publishedAt: new Date().toISOString(),
}); });
const jobName = `${JOB_NAME}-${flow.id}`; const jobName = `${JOB_NAME}-${flow.id}`;
@@ -78,9 +85,12 @@ const updateFlowStatus = async (
} }
} }
flow = await flow.$query().withGraphFetched('steps').patchAndFetch({ flow = await flow
active: newActiveValue, .$query()
}); .withGraphFetched('steps')
.patchAndFetch({
active: newActiveValue,
});
return flow; return flow;
}; };

View File

@@ -12,6 +12,8 @@ const updateFlow = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('update', 'Flow');
let flow = await context.currentUser let flow = await context.currentUser
.$relatedQuery('flows') .$relatedQuery('flows')
.findOne({ .findOne({

View File

@@ -0,0 +1,91 @@
import Context from '../../types/express/context';
import Role from '../../models/role';
import Permission from '../../models/permission';
import permissionCatalog from '../../helpers/permission-catalog.ee';
type Params = {
input: {
id: string;
name: string;
description: string;
permissions: Permission[];
};
};
const updateRole = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'Role');
const {
id,
name,
description,
permissions,
} = params.input;
const role = await Role
.query()
.findById(id)
.throwIfNotFound();
try {
const updatedRole = await Role.transaction(async (trx) => {
await role.$relatedQuery('permissions', trx).delete();
if (permissions?.length) {
const sanitizedPermissions = permissions
.filter((permission) => {
const {
action,
subject,
conditions,
} = permission;
const relevantAction = permissionCatalog.actions.find(actionCatalogItem => actionCatalogItem.key === action);
const validSubject = relevantAction.subjects.includes(subject);
const validConditions = conditions.every(condition => {
return !!permissionCatalog
.conditions
.find((conditionCatalogItem) => conditionCatalogItem.key === condition);
})
return validSubject && validConditions;
})
.map((permission) => ({
...permission,
roleId: role.id,
}));
await Permission.query().insert(sanitizedPermissions);
}
await role
.$query(trx)
.patch(
{
name,
description,
}
);
return await Role
.query(trx)
.leftJoinRelated({
permissions: true
})
.withGraphFetched({
permissions: true
})
.findById(id);
});
return updatedRole;
} catch (err) {
throw new Error('The role could not be updated!');
}
};
export default updateRole;

View File

@@ -1,6 +1,7 @@
import { IJSONObject } from '@automatisch/types'; import { IJSONObject } from '@automatisch/types';
import App from '../../models/app'; import App from '../../models/app';
import Step from '../../models/step'; import Step from '../../models/step';
import Connection from '../../models/connection';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
type Params = { type Params = {
@@ -23,10 +24,14 @@ const updateStep = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const { isCreator } = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const baseQuery = isCreator ? userSteps : allSteps;
const { input } = params; const { input } = params;
let step = await context.currentUser let step = await baseQuery
.$relatedQuery('steps')
.findOne({ .findOne({
'steps.id': input.id, 'steps.id': input.id,
flow_id: input.flow.id, flow_id: input.flow.id,
@@ -34,11 +39,24 @@ const updateStep = async (
.throwIfNotFound(); .throwIfNotFound();
if (input.connection.id) { if (input.connection.id) {
const hasConnection = await context.currentUser let canSeeAllConnections = false;
.$relatedQuery('connections') try {
.findById(input.connection?.id); const conditions = context.currentUser.can('read', 'Connection');
if (!hasConnection) { canSeeAllConnections = !conditions.isCreator;
} catch {
// void
}
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;
const connection = await baseConnectionsQuery
.clone()
.findById(input.connection?.id)
if (!connection) {
throw new Error('The connection does not exist!'); throw new Error('The connection does not exist!');
} }
} }

View File

@@ -0,0 +1,43 @@
import Context from '../../types/express/context';
import User from '../../models/user';
type Params = {
input: {
id: string;
email: string;
fullName: string;
role: {
id: string;
};
};
};
const updateUser = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'User');
const userPayload: Partial<User> = {
email: params.input.email,
fullName: params.input.fullName,
};
try {
context.currentUser.can('update', 'Role');
userPayload.roleId = params.input.role.id;
} catch {
// void
}
const user = await User.query().patchAndFetchById(
params.input.id,
userPayload
);
return user;
};
export default updateUser;

View File

@@ -0,0 +1,52 @@
import type { SamlConfig } from '@node-saml/passport-saml';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
type Params = {
input: {
name: string;
certificate: string;
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
issuer: string;
entryPoint: string;
firstnameAttributeName: string;
surnameAttributeName: string;
emailAttributeName: string;
roleAttributeName: string;
defaultRoleId: string;
active: boolean;
};
};
const upsertSamlAuthProvider = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'SamlAuthProvider');
const samlAuthProviderPayload: Partial<SamlAuthProvider> = {
...params.input,
};
const existingSamlAuthProvider = await SamlAuthProvider.query()
.limit(1)
.first();
if (!existingSamlAuthProvider) {
const samlAuthProvider = await SamlAuthProvider.query().insert(
samlAuthProviderPayload
);
return samlAuthProvider;
}
const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
existingSamlAuthProvider.id,
samlAuthProviderPayload
);
return samlAuthProvider;
};
export default upsertSamlAuthProvider;

View File

@@ -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;

View File

@@ -13,6 +13,8 @@ const verifyConnection = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
context.currentUser.can('create', 'Connection');
let connection = await context.currentUser let connection = await context.currentUser
.$relatedQuery('connections') .$relatedQuery('connections')
.findOne({ .findOne({

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -1,4 +1,5 @@
import App from '../../models/app'; import App from '../../models/app';
import Connection from '../../models/connection';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
type Params = { type Params = {
@@ -6,12 +7,22 @@ type Params = {
}; };
const getApp = async (_parent: unknown, params: Params, context: Context) => { const getApp = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
const app = await App.findOneByKey(params.key); const app = await App.findOneByKey(params.key);
if (context.currentUser) { if (context.currentUser) {
const connections = await context.currentUser const connections = await connectionBaseQuery
.$relatedQuery('connections') .clone()
.select('connections.*') .select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true
})
.fullOuterJoinRelated('steps') .fullOuterJoinRelated('steps')
.where({ .where({
'connections.key': params.key, 'connections.key': params.key,

View File

@@ -1,5 +1,5 @@
import App from '../../models/app';
import { IApp } from '@automatisch/types'; import { IApp } from '@automatisch/types';
import App from '../../models/app';
type Params = { type Params = {
name: string; name: string;

View File

@@ -1,8 +1,19 @@
import appConfig from '../../config/app'; import appConfig from '../../config/app';
import { getLicense } from '../../helpers/license.ee';
const getAutomatischInfo = async () => { 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 { return {
isCloud: appConfig.isCloud, isCloud: appConfig.isCloud,
license: computedLicense,
}; };
}; };

View 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;

View File

@@ -1,6 +1,8 @@
import { IConnection } from '@automatisch/types';
import App from '../../models/app'; import App from '../../models/app';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import { IApp, IConnection } from '@automatisch/types'; import Flow from '../../models/flow';
import Connection from '../../models/connection';
type Params = { type Params = {
name: string; name: string;
@@ -11,17 +13,27 @@ const getConnectedApps = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const flowBaseQuery = conditions.isCreator ? userFlows : allFlows;
let apps = await App.findAll(params.name); let apps = await App.findAll(params.name);
const connections = await context.currentUser const connections = await connectionBaseQuery
.$relatedQuery('connections') .clone()
.select('connections.key') .select('connections.key')
.where({ draft: false }) .where({ draft: false })
.count('connections.id as count') .count('connections.id as count')
.groupBy('connections.key'); .groupBy('connections.key');
const flows = await context.currentUser const flows = await flowBaseQuery
.$relatedQuery('flows') .clone()
.withGraphJoined('steps') .withGraphJoined('steps')
.orderBy('created_at', 'desc'); .orderBy('created_at', 'desc');

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