Compare commits

..

6 Commits

Author SHA1 Message Date
Rıdvan Akca
ece63253f4 feat(wordpress): add delete post action 2024-05-29 15:50:09 +02:00
Rıdvan Akca
a7bd19e61f feat(wordpress): add find user action 2024-05-29 15:21:21 +02:00
Rıdvan Akca
3d4a9865fe feat(wordpress): add create user action 2024-05-29 14:28:53 +02:00
Rıdvan Akca
c191b7a3cf feat(wordpress): add find post action 2024-05-29 11:48:54 +02:00
Rıdvan Akca
69416c24e2 feat(wordpress): add update post action 2024-05-29 11:09:07 +02:00
Rıdvan Akca
2460e9f281 feat(wordpress): add create post action 2024-05-28 17:52:38 +02:00
237 changed files with 1673 additions and 4235 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,262 @@
import defineAction from '../../../../helpers/define-action.js';
import isEmpty from 'lodash/isEmpty.js';
import omitBy from 'lodash/omitBy.js';
export default defineAction({
name: 'Create post',
key: 'createPost',
description: 'Creates a new post.',
arguments: [
{
label: 'Title',
key: 'title',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Excerpt',
key: 'excerpt',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Password',
key: 'password',
type: 'string',
required: false,
description: 'A password to protect access to the content and excerpt.',
variables: true,
},
{
label: 'Author',
key: 'author',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
],
},
},
{
label: 'Featured Media',
key: 'featuredMedia',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listMedia',
},
],
},
},
{
label: 'Comment Status',
key: 'commentStatus',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Open', value: 'open' },
{ label: 'Closed', value: 'closed' },
],
},
{
label: 'Ping Status',
key: 'pingStatus',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Open', value: 'open' },
{ label: 'Closed', value: 'closed' },
],
},
{
label: 'Format',
key: 'format',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Standard', value: 'standard' },
{ label: 'Aside', value: 'aside' },
{ label: 'Chat', value: 'chat' },
{ label: 'Gallery', value: 'gallery' },
{ label: 'Link', value: 'link' },
{ label: 'Image', value: 'image' },
{ label: 'Quote', value: 'quote' },
{ label: 'Status', value: 'status' },
{ label: 'Status', value: 'status' },
{ label: 'Video', value: 'video' },
{ label: 'Audio', value: 'audio' },
],
},
{
label: 'Sticky',
key: 'sticky',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'False', value: 'false' },
{ label: 'True', value: 'true' },
],
},
{
label: 'Categories',
key: 'categoryIds',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Category',
key: 'categoryId',
type: 'dropdown',
required: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listCategories',
},
],
},
},
],
},
{
label: 'Tags',
key: 'tagIds',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Tag',
key: 'tagId',
type: 'dropdown',
required: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTags',
},
],
},
},
],
},
{
label: 'Status',
key: 'status',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listStatuses',
},
],
},
},
{
label: 'Date',
key: 'date',
type: 'string',
required: false,
description: "Post publish date in the site's timezone",
variables: true,
},
],
async run($) {
const {
title,
content,
excerpt,
password,
author,
featuredMedia,
commentStatus,
pingStatus,
format,
sticky,
categoryIds,
tagIds,
status,
date,
} = $.step.parameters;
const allCategoryIds = categoryIds
?.map((categoryId) => categoryId.categoryId)
.filter(Boolean);
const allTagIds = tagIds?.map((tagId) => tagId.tagId).filter(Boolean);
let body = {
title,
content,
excerpt,
password,
author,
featured_media: featuredMedia,
comment_status: commentStatus,
ping_status: pingStatus,
format,
sticky,
categories: allCategoryIds,
tags: allTagIds,
status,
date,
};
body = omitBy(body, isEmpty);
const response = await $.http.post('?rest_route=/wp/v2/posts', body);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,135 @@
import defineAction from '../../../../helpers/define-action.js';
import isEmpty from 'lodash/isEmpty.js';
import omitBy from 'lodash/omitBy.js';
export default defineAction({
name: 'Create user',
key: 'createUser',
description: 'Creates a new user.',
arguments: [
{
label: 'Email',
key: 'email',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Username',
key: 'username',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Password',
key: 'password',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'First Name',
key: 'firstName',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Last Name',
key: 'lastName',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Display Name',
key: 'displayName',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Nickname',
key: 'nickname',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Description',
key: 'description',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Website',
key: 'website',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Role',
key: 'role',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Administrator', value: 'administrator' },
{ label: 'Author', value: 'author' },
{ label: 'Contributor', value: 'contributor' },
{ label: 'Editor', value: 'editor' },
{ label: 'Subscriber', value: 'subscriber' },
],
},
],
async run($) {
const {
email,
username,
password,
firstName,
lastName,
displayName,
nickname,
description,
website,
role,
} = $.step.parameters;
let body = {
email,
username,
password,
first_name: firstName,
last_name: lastName,
name: displayName,
nickname,
description,
url: website,
};
if (role) {
body.roles = [role];
}
body = omitBy(body, isEmpty);
const response = await $.http.post('?rest_route=/wp/v2/users', body);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,35 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Delete post',
key: 'deletePost',
description: 'Deletes a post.',
arguments: [
{
label: 'Post ID',
key: 'postId',
type: 'dropdown',
required: false,
description: 'Choose a post to delete.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listPosts',
},
],
},
},
],
async run($) {
const { postId } = $.step.parameters;
const response = await $.http.delete(`?rest_route=/wp/v2/posts/${postId}`);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,35 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Find post',
key: 'findPost',
description: 'Finds a post.',
arguments: [
{
label: 'Post ID',
key: 'postId',
type: 'dropdown',
required: false,
description: 'Choose a post to update.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listPosts',
},
],
},
},
],
async run($) {
const { postId } = $.step.parameters;
const response = await $.http.get(`?rest_route=/wp/v2/posts/${postId}`);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,35 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Find user',
key: 'findUser',
description: 'Finds a user.',
arguments: [
{
label: 'User ID',
key: 'userId',
type: 'dropdown',
required: true,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
],
},
},
],
async run($) {
const userId = $.step.parameters.userId;
const response = await $.http.get(`?rest_route=/wp/v2/users/${userId}`);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -0,0 +1,15 @@
import createPost from './create-post/index.js';
import createUser from './create-user/index.js';
import deletePost from './delete-post/index.js';
import findPost from './find-post/index.js';
import findUser from './find-user/index.js';
import updatePost from './update-post/index.js';
export default [
createPost,
createUser,
deletePost,
findPost,
findUser,
updatePost,
];

View File

@@ -0,0 +1,284 @@
import defineAction from '../../../../helpers/define-action.js';
import isEmpty from 'lodash/isEmpty.js';
import omitBy from 'lodash/omitBy.js';
export default defineAction({
name: 'Update post',
key: 'updatePost',
description: 'Updates a post.',
arguments: [
{
label: 'Post',
key: 'postId',
type: 'dropdown',
required: false,
description: 'Choose a post to update.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listPosts',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string',
required: true,
description: '',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Excerpt',
key: 'excerpt',
type: 'string',
required: false,
description: '',
variables: true,
},
{
label: 'Password',
key: 'password',
type: 'string',
required: false,
description: 'A password to protect access to the content and excerpt.',
variables: true,
},
{
label: 'Author',
key: 'author',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listUsers',
},
],
},
},
{
label: 'Featured Media',
key: 'featuredMedia',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listMedia',
},
],
},
},
{
label: 'Comment Status',
key: 'commentStatus',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Open', value: 'open' },
{ label: 'Closed', value: 'closed' },
],
},
{
label: 'Ping Status',
key: 'pingStatus',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Open', value: 'open' },
{ label: 'Closed', value: 'closed' },
],
},
{
label: 'Format',
key: 'format',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'Standard', value: 'standard' },
{ label: 'Aside', value: 'aside' },
{ label: 'Chat', value: 'chat' },
{ label: 'Gallery', value: 'gallery' },
{ label: 'Link', value: 'link' },
{ label: 'Image', value: 'image' },
{ label: 'Quote', value: 'quote' },
{ label: 'Status', value: 'status' },
{ label: 'Status', value: 'status' },
{ label: 'Video', value: 'video' },
{ label: 'Audio', value: 'audio' },
],
},
{
label: 'Sticky',
key: 'sticky',
type: 'dropdown',
required: false,
description: '',
variables: true,
options: [
{ label: 'False', value: 'false' },
{ label: 'True', value: 'true' },
],
},
{
label: 'Categories',
key: 'categoryIds',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Category',
key: 'categoryId',
type: 'dropdown',
required: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listCategories',
},
],
},
},
],
},
{
label: 'Tags',
key: 'tagIds',
type: 'dynamic',
required: false,
description: '',
fields: [
{
label: 'Tag',
key: 'tagId',
type: 'dropdown',
required: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listTags',
},
],
},
},
],
},
{
label: 'Status',
key: 'status',
type: 'dropdown',
required: false,
description: '',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listStatuses',
},
],
},
},
{
label: 'Date',
key: 'date',
type: 'string',
required: false,
description: "Post publish date in the site's timezone",
variables: true,
},
],
async run($) {
const {
postId,
title,
content,
excerpt,
password,
author,
featuredMedia,
commentStatus,
pingStatus,
format,
sticky,
categoryIds,
tagIds,
status,
date,
} = $.step.parameters;
const allCategoryIds = categoryIds
?.map((categoryId) => categoryId.categoryId)
.filter(Boolean);
const allTagIds = tagIds?.map((tagId) => tagId.tagId).filter(Boolean);
let body = {
title,
content,
excerpt,
password,
author,
featured_media: featuredMedia,
comment_status: commentStatus,
ping_status: pingStatus,
format,
sticky,
categories: allCategoryIds,
tags: allTagIds,
status,
date,
};
body = omitBy(body, isEmpty);
const response = await $.http.post(
`?rest_route=/wp/v2/posts/${postId}`,
body
);
$.setActionItem({ raw: response.data });
},
});

View File

@@ -1,3 +1,15 @@
import listCategories from './list-categories/index.js';
import listMedia from './list-media/index.js';
import listPosts from './list-posts/index.js';
import listStatuses from './list-statuses/index.js'; import listStatuses from './list-statuses/index.js';
import listTags from './list-tags/index.js';
import listUsers from './list-users/index.js';
export default [listStatuses]; export default [
listCategories,
listMedia,
listPosts,
listStatuses,
listTags,
listUsers,
];

View File

@@ -0,0 +1,40 @@
export default {
name: 'List categories',
key: 'listCategories',
async run($) {
const categories = {
data: [],
};
const params = {
page: 1,
per_page: 100,
order: 'desc',
};
let totalPages = 1;
do {
const { data, headers } = await $.http.get(
'?rest_route=/wp/v2/categories',
{
params,
}
);
params.page = params.page + 1;
totalPages = Number(headers['x-wp-totalpages']);
if (data) {
for (const category of data) {
categories.data.push({
value: category.id,
name: category.name,
});
}
}
} while (params.page <= totalPages);
return categories;
},
};

View File

@@ -0,0 +1,37 @@
export default {
name: 'List media',
key: 'listMedia',
async run($) {
const media = {
data: [],
};
const params = {
page: 1,
per_page: 100,
order: 'desc',
};
let totalPages = 1;
do {
const { data, headers } = await $.http.get('?rest_route=/wp/v2/media', {
params,
});
params.page = params.page + 1;
totalPages = Number(headers['x-wp-totalpages']);
if (data) {
for (const medium of data) {
media.data.push({
value: medium.id,
name: medium.slug,
});
}
}
} while (params.page <= totalPages);
return media;
},
};

View File

@@ -0,0 +1,38 @@
export default {
name: 'List posts',
key: 'listPosts',
async run($) {
const posts = {
data: [],
};
const params = {
page: 1,
per_page: 100,
order: 'desc',
status: ['publish', 'future', 'draft', 'pending', 'private'],
};
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) {
for (const post of data) {
posts.data.push({
value: post.id,
name: `${post.title.rendered} (${post.status})`,
});
}
}
} while (params.page <= totalPages);
return posts;
},
};

View File

@@ -0,0 +1,37 @@
export default {
name: 'List tags',
key: 'listTags',
async run($) {
const tags = {
data: [],
};
const params = {
page: 1,
per_page: 100,
order: 'desc',
};
let totalPages = 1;
do {
const { data, headers } = await $.http.get('?rest_route=/wp/v2/tags', {
params,
});
params.page = params.page + 1;
totalPages = Number(headers['x-wp-totalpages']);
if (data) {
for (const tag of data) {
tags.data.push({
value: tag.id,
name: tag.name,
});
}
}
} while (params.page <= totalPages);
return tags;
},
};

View File

@@ -0,0 +1,37 @@
export default {
name: 'List users',
key: 'listUsers',
async run($) {
const users = {
data: [],
};
const params = {
page: 1,
per_page: 100,
order: 'desc',
};
let totalPages = 1;
do {
const { data, headers } = await $.http.get('?rest_route=/wp/v2/users', {
params,
});
params.page = params.page + 1;
totalPages = Number(headers['x-wp-totalpages']);
if (data) {
for (const user of data) {
users.data.push({
value: user.id,
name: user.name,
});
}
}
} while (params.page <= totalPages);
return users;
},
};

View File

@@ -4,6 +4,7 @@ import setBaseUrl from './common/set-base-url.js';
import auth from './auth/index.js'; import auth from './auth/index.js';
import triggers from './triggers/index.js'; import triggers from './triggers/index.js';
import dynamicData from './dynamic-data/index.js'; import dynamicData from './dynamic-data/index.js';
import actions from './actions/index.js';
export default defineApp({ export default defineApp({
name: 'WordPress', name: 'WordPress',
@@ -18,4 +19,5 @@ export default defineApp({
auth, auth,
triggers, triggers,
dynamicData, dynamicData,
actions,
}); });

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development', isDev: appEnv === 'development',
isTest: appEnv === 'test', isTest: appEnv === 'test',
isProd: appEnv === 'production', isProd: appEnv === 'production',
version: '0.12.0', version: '0.11.0',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development', postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public', postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
@@ -97,12 +97,8 @@ const appConfig = {
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true', disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true', disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK, additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkIcon: process.env.ADDITIONAL_DRAWER_LINK_ICON,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT, additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
disableSeedUser: process.env.DISABLE_SEED_USER === 'true', disableSeedUser: process.env.DISABLE_SEED_USER === 'true',
httpProxy: process.env.http_proxy,
httpsProxy: process.env.https_proxy,
noProxy: process.env.no_proxy,
}; };
if (!appConfig.encryptionKey) { if (!appConfig.encryptionKey) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = { const expectedPayload = {
data: { data: {
version: '0.12.0', version: '0.11.0',
}, },
meta: { meta: {
count: 1, count: 1,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
import axios from 'axios'; import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
import appConfig from '../config/app.js';
const config = axios.defaults; const config = axios.defaults;
const httpProxyUrl = appConfig.httpProxy; const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = appConfig.httpsProxy; const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl; const supportsProxy = httpProxyUrl || httpsProxyUrl;
const noProxyEnv = appConfig.noProxy; const noProxyEnv = process.env.no_proxy;
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : []; const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : [];
if (supportsProxy) { if (supportsProxy) {
@@ -30,12 +29,8 @@ function shouldSkipProxy(hostname) {
}); });
}; };
/** axiosWithProxyInstance.interceptors.request.use(function skipProxyIfInNoProxy(requestConfig) {
* The interceptors are executed in the reverse order they are added. const hostname = new URL(requestConfig.url).hostname;
*/
axiosWithProxyInstance.interceptors.request.use(
function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.baseURL).hostname;
if (supportsProxy && shouldSkipProxy(hostname)) { if (supportsProxy && shouldSkipProxy(hostname)) {
requestConfig.httpAgent = undefined; requestConfig.httpAgent = undefined;
@@ -43,30 +38,6 @@ axiosWithProxyInstance.interceptors.request.use(
} }
return requestConfig; return requestConfig;
}, });
undefined,
{ synchronous: true }
);
axiosWithProxyInstance.interceptors.request.use(
function removeBaseUrlForAbsoluteUrls(requestConfig) {
/**
* If the URL is an absolute URL, we remove its origin out of the URL
* and set it as baseURL. This lets us streamlines the requests made by Automatisch
* and requests made by app integrations.
*/
try {
const url = new URL(requestConfig.url);
requestConfig.baseURL = url.origin;
requestConfig.url = url.pathname + url.search;
return requestConfig;
} catch {
return requestConfig;
}
},
undefined,
{ synchronous: true}
);
export default axiosWithProxyInstance; export default axiosWithProxyInstance;

View File

@@ -1,119 +0,0 @@
import { beforeEach, describe, it, expect, vi } from 'vitest';
describe('Axios with proxy', () => {
beforeEach(() => {
vi.resetModules();
});
it('should have two interceptors by default', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
const requestInterceptors = axios.interceptors.request.handlers;
expect(requestInterceptors.length).toBe(2);
});
it('should have default interceptors in a certain order', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
const requestInterceptors = axios.interceptors.request.handlers;
const firstRequestInterceptor = requestInterceptors[0];
const secondRequestInterceptor = requestInterceptors[1];
expect(firstRequestInterceptor.fulfilled.name).toBe('skipProxyIfInNoProxy');
expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls');
});
describe('skipProxyIfInNoProxy', () => {
let appConfig, axios;
beforeEach(async() => {
appConfig = (await import('../config/app.js')).default;
vi.spyOn(appConfig, 'httpProxy', 'get').mockReturnValue('http://proxy.automatisch.io');
vi.spyOn(appConfig, 'httpsProxy', 'get').mockReturnValue('http://proxy.automatisch.io');
vi.spyOn(appConfig, 'noProxy', 'get').mockReturnValue('name.tld,automatisch.io');
axios = (await import('./axios-with-proxy.js')).default;
});
it('should skip proxy for hosts in no_proxy environment variable', async () => {
const skipProxyIfInNoProxy = axios.interceptors.request.handlers[0].fulfilled;
const mockRequestConfig = {
...axios.defaults,
baseURL: 'https://automatisch.io'
};
const interceptedRequestConfig = skipProxyIfInNoProxy(mockRequestConfig);
expect(interceptedRequestConfig.httpAgent).toBeUndefined();
expect(interceptedRequestConfig.httpsAgent).toBeUndefined();
expect(interceptedRequestConfig.proxy).toBe(false);
});
it('should not skip proxy for hosts not in no_proxy environment variable', async () => {
const skipProxyIfInNoProxy = axios.interceptors.request.handlers[0].fulfilled;
const mockRequestConfig = {
...axios.defaults,
// beware the intentional typo!
baseURL: 'https://automatish.io'
};
const interceptedRequestConfig = skipProxyIfInNoProxy(mockRequestConfig);
expect(interceptedRequestConfig.httpAgent).toBeDefined();
expect(interceptedRequestConfig.httpsAgent).toBeDefined();
expect(interceptedRequestConfig.proxy).toBe(false);
});
});
describe('removeBaseUrlForAbsoluteUrls', () => {
let axios;
beforeEach(async() => {
axios = (await import('./axios-with-proxy.js')).default;
});
it('should trim the baseUrl from absolute urls', async () => {
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
const mockRequestConfig = {
...axios.defaults,
url: 'https://automatisch.io/path'
};
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
expect(interceptedRequestConfig.url).toBe('/path');
});
it('should not mutate separate baseURL and urls', async () => {
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
const mockRequestConfig = {
...axios.defaults,
baseURL: 'https://automatisch.io',
url: '/path?query=1'
};
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
expect(interceptedRequestConfig.url).toBe('/path?query=1');
});
it('should not strip querystring from url', async () => {
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
const mockRequestConfig = {
...axios.defaults,
url: 'https://automatisch.io/path?query=1'
};
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
expect(interceptedRequestConfig.url).toBe('/path?query=1');
});
});
});

View File

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

View File

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

View File

@@ -1,38 +1,41 @@
import { URL } from 'node:url';
import HttpError from '../../errors/http.js'; import HttpError from '../../errors/http.js';
import axios from '../axios-with-proxy.js'; import axios from '../axios-with-proxy.js';
// Mutates the `toInstance` by copying the request interceptors from `fromInstance` const removeBaseUrlForAbsoluteUrls = (requestConfig) => {
const copyRequestInterceptors = (fromInstance, toInstance) => { try {
// Copy request interceptors const url = new URL(requestConfig.url);
fromInstance.interceptors.request.forEach(interceptor => { requestConfig.baseURL = url.origin;
toInstance.interceptors.request.use( requestConfig.url = url.pathname + url.search;
interceptor.fulfilled,
interceptor.rejected, return requestConfig;
{ } catch {
synchronous: interceptor.synchronous, return requestConfig;
runWhen: interceptor.runWhen
}
);
});
} }
};
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) { export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
const instance = axios.create({ const instance = axios.create({
baseURL, baseURL,
}); });
// 1. apply the beforeRequest functions from the app
instance.interceptors.request.use((requestConfig) => { instance.interceptors.request.use((requestConfig) => {
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => { const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
return beforeRequestFunc($, newConfig); return beforeRequestFunc($, newConfig);
}, requestConfig); }, newRequestConfig);
/**
* axios seems to want InternalAxiosRequestConfig returned not AxioRequestConfig
* anymore even though requests do require AxiosRequestConfig.
*
* Since both interfaces are very similar (InternalAxiosRequestConfig
* extends AxiosRequestConfig), we can utilize an assertion below
**/
return result; return result;
}); });
// 2. inherit the request inceptors from the parent instance
copyRequestInterceptors(axios, instance);
instance.interceptors.response.use( instance.interceptors.response.use(
(response) => response, (response) => response,
async (error) => { async (error) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,15 +59,6 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/carbone/connection' }, { text: 'Connection', link: '/apps/carbone/connection' },
], ],
}, },
{
text: 'Cryptography',
collapsible: true,
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/cryptography/actions' },
{ text: 'Connection', link: '/apps/cryptography/connection' },
],
},
{ {
text: 'Datastore', text: 'Datastore',
collapsible: true, collapsible: true,
@@ -527,6 +518,7 @@ export default defineConfig({
collapsible: true, collapsible: true,
collapsed: true, collapsed: true,
items: [ items: [
{ text: 'Actions', link: '/apps/wordpress/actions' },
{ text: 'Triggers', link: '/apps/wordpress/triggers' }, { text: 'Triggers', link: '/apps/wordpress/triggers' },
{ text: 'Connection', link: '/apps/wordpress/connection' }, { text: 'Connection', link: '/apps/wordpress/connection' },
], ],

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
---
favicon: /favicons/wordpress.svg
items:
- name: Create post
desc: Creates a new post.
- name: Create user
desc: Creates a new user.
- name: Delete post
desc: Deletes a post.
- name: Find post
desc: Finds a post.
- name: Find user
desc: Finds a user.
- name: Update post
desc: Updates a post.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
const path = require('node:path');
const { expect } = require('@playwright/test');
const { BasePage } = require('./base-page'); const { BasePage } = require('./base-page');
const { LoginPage } = require('./login-page');
export class AuthenticatedPage extends BasePage { export class AuthenticatedPage extends BasePage {
/** /**

View File

@@ -1,3 +1,4 @@
const path = require('node:path');
const { AuthenticatedPage } = require('./authenticated-page'); const { AuthenticatedPage } = require('./authenticated-page');
export class ConnectionsPage extends AuthenticatedPage { export class ConnectionsPage extends AuthenticatedPage {

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