Compare commits
1 Commits
v0.13.0
...
changedete
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c095d3138b |
3
.github/workflows/playwright.yml
vendored
3
.github/workflows/playwright.yml
vendored
@@ -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
|
||||||
|
@@ -1,92 +0,0 @@
|
|||||||
import defineAction from '../../../../helpers/define-action.js';
|
|
||||||
|
|
||||||
export default defineAction({
|
|
||||||
name: 'Create record',
|
|
||||||
key: 'createRecord',
|
|
||||||
description: 'Creates a new record with fields that automatically populate.',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
label: 'Base',
|
|
||||||
key: 'baseId',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
description: 'Base in which to create the record.',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listBases',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Table',
|
|
||||||
key: 'tableId',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
dependsOn: ['parameters.baseId'],
|
|
||||||
description: '',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listTables',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.baseId',
|
|
||||||
value: '{parameters.baseId}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
additionalFields: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicFields',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listFields',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.baseId',
|
|
||||||
value: '{parameters.baseId}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.tableId',
|
|
||||||
value: '{parameters.tableId}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const { baseId, tableId, ...rest } = $.step.parameters;
|
|
||||||
|
|
||||||
const fields = Object.entries(rest).reduce((result, [key, value]) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
result[key] = value.map((item) => item.value);
|
|
||||||
} else if (value !== '') {
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
typecast: true,
|
|
||||||
fields,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data } = await $.http.post(`/v0/${baseId}/${tableId}`, body);
|
|
||||||
|
|
||||||
$.setActionItem({
|
|
||||||
raw: data,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,174 +0,0 @@
|
|||||||
import defineAction from '../../../../helpers/define-action.js';
|
|
||||||
import { URLSearchParams } from 'url';
|
|
||||||
|
|
||||||
export default defineAction({
|
|
||||||
name: 'Find record',
|
|
||||||
key: 'findRecord',
|
|
||||||
description:
|
|
||||||
"Finds a record using simple field search or use Airtable's formula syntax to find a matching record.",
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
label: 'Base',
|
|
||||||
key: 'baseId',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
description: 'Base in which to create the record.',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listBases',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Table',
|
|
||||||
key: 'tableId',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
dependsOn: ['parameters.baseId'],
|
|
||||||
description: '',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listTables',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.baseId',
|
|
||||||
value: '{parameters.baseId}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Search by field',
|
|
||||||
key: 'tableField',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: false,
|
|
||||||
dependsOn: ['parameters.baseId', 'parameters.tableId'],
|
|
||||||
description: '',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listTableFields',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.baseId',
|
|
||||||
value: '{parameters.baseId}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.tableId',
|
|
||||||
value: '{parameters.tableId}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Search Value',
|
|
||||||
key: 'searchValue',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
variables: true,
|
|
||||||
description:
|
|
||||||
'The value of unique identifier for the record. For date values, please use the ISO format (e.g., "YYYY-MM-DD").',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Search for exact match?',
|
|
||||||
key: 'exactMatch',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
description: '',
|
|
||||||
variables: true,
|
|
||||||
options: [
|
|
||||||
{ label: 'Yes', value: 'true' },
|
|
||||||
{ label: 'No', value: 'false' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Search Formula',
|
|
||||||
key: 'searchFormula',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
variables: true,
|
|
||||||
description:
|
|
||||||
'Instead, you have the option to use an Airtable search formula for locating records according to sophisticated criteria and across various fields.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Limit to View',
|
|
||||||
key: 'limitToView',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: false,
|
|
||||||
dependsOn: ['parameters.baseId', 'parameters.tableId'],
|
|
||||||
description:
|
|
||||||
'You have the choice to restrict the search to a particular view ID if desired.',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listTableViews',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.baseId',
|
|
||||||
value: '{parameters.baseId}',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.tableId',
|
|
||||||
value: '{parameters.tableId}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const {
|
|
||||||
baseId,
|
|
||||||
tableId,
|
|
||||||
tableField,
|
|
||||||
searchValue,
|
|
||||||
exactMatch,
|
|
||||||
searchFormula,
|
|
||||||
limitToView,
|
|
||||||
} = $.step.parameters;
|
|
||||||
|
|
||||||
let filterByFormula;
|
|
||||||
|
|
||||||
if (tableField && searchValue) {
|
|
||||||
filterByFormula =
|
|
||||||
exactMatch === 'true'
|
|
||||||
? `{${tableField}} = '${searchValue}'`
|
|
||||||
: `LOWER({${tableField}}) = LOWER('${searchValue}')`;
|
|
||||||
} else {
|
|
||||||
filterByFormula = searchFormula;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
filterByFormula,
|
|
||||||
view: limitToView,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data } = await $.http.post(
|
|
||||||
`/v0/${baseId}/${tableId}/listRecords`,
|
|
||||||
body
|
|
||||||
);
|
|
||||||
|
|
||||||
$.setActionItem({
|
|
||||||
raw: data,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,4 +0,0 @@
|
|||||||
import createRecord from './create-record/index.js';
|
|
||||||
import findRecord from './find-record/index.js';
|
|
||||||
|
|
||||||
export default [createRecord, findRecord];
|
|
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="256px" height="215px" viewBox="0 0 256 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
|
||||||
<g>
|
|
||||||
<path d="M114.25873,2.70101695 L18.8604023,42.1756384 C13.5552723,44.3711638 13.6102328,51.9065311 18.9486282,54.0225085 L114.746142,92.0117514 C123.163769,95.3498757 132.537419,95.3498757 140.9536,92.0117514 L236.75256,54.0225085 C242.08951,51.9065311 242.145916,44.3711638 236.83934,42.1756384 L141.442459,2.70101695 C132.738459,-0.900338983 122.961284,-0.900338983 114.25873,2.70101695" fill="#FFBF00"></path>
|
|
||||||
<path d="M136.349071,112.756863 L136.349071,207.659101 C136.349071,212.173089 140.900664,215.263892 145.096461,213.600615 L251.844122,172.166219 C254.281184,171.200072 255.879376,168.845451 255.879376,166.224705 L255.879376,71.3224678 C255.879376,66.8084791 251.327783,63.7176768 247.131986,65.3809537 L140.384325,106.815349 C137.94871,107.781496 136.349071,110.136118 136.349071,112.756863" fill="#26B5F8"></path>
|
|
||||||
<path d="M111.422771,117.65355 L79.742409,132.949912 L76.5257763,134.504714 L9.65047684,166.548104 C5.4112904,168.593211 0.000578531073,165.503855 0.000578531073,160.794612 L0.000578531073,71.7210757 C0.000578531073,70.0173017 0.874160452,68.5463864 2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill="#ED3049"></path>
|
|
||||||
<path d="M111.422771,117.65355 L79.742409,132.949912 L2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill-opacity="0.25" fill="#000000"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,38 +0,0 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
import { URLSearchParams } from 'url';
|
|
||||||
import authScope from '../common/auth-scope.js';
|
|
||||||
|
|
||||||
export default async function generateAuthUrl($) {
|
|
||||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
|
||||||
(field) => field.key == 'oAuthRedirectUrl'
|
|
||||||
);
|
|
||||||
const redirectUri = oauthRedirectUrlField.value;
|
|
||||||
const state = crypto.randomBytes(100).toString('base64url');
|
|
||||||
const codeVerifier = crypto.randomBytes(96).toString('base64url');
|
|
||||||
const codeChallenge = crypto
|
|
||||||
.createHash('sha256')
|
|
||||||
.update(codeVerifier)
|
|
||||||
.digest('base64')
|
|
||||||
.replace(/=/g, '')
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_');
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
client_id: $.auth.data.clientId,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
response_type: 'code',
|
|
||||||
scope: authScope.join(' '),
|
|
||||||
state,
|
|
||||||
code_challenge: codeChallenge,
|
|
||||||
code_challenge_method: 'S256',
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = `https://airtable.com/oauth2/v1/authorize?${searchParams.toString()}`;
|
|
||||||
|
|
||||||
await $.auth.set({
|
|
||||||
url,
|
|
||||||
originalCodeChallenge: codeChallenge,
|
|
||||||
originalState: state,
|
|
||||||
codeVerifier,
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,48 +0,0 @@
|
|||||||
import generateAuthUrl from './generate-auth-url.js';
|
|
||||||
import verifyCredentials from './verify-credentials.js';
|
|
||||||
import refreshToken from './refresh-token.js';
|
|
||||||
import isStillVerified from './is-still-verified.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: 'oAuthRedirectUrl',
|
|
||||||
label: 'OAuth Redirect URL',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
readOnly: true,
|
|
||||||
value: '{WEB_APP_URL}/app/airtable/connections/add',
|
|
||||||
placeholder: null,
|
|
||||||
description:
|
|
||||||
'When asked to input a redirect URL in Airtable, enter the URL above.',
|
|
||||||
clickToCopy: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'clientId',
|
|
||||||
label: 'Client ID',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
readOnly: false,
|
|
||||||
value: null,
|
|
||||||
placeholder: null,
|
|
||||||
description: null,
|
|
||||||
clickToCopy: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'clientSecret',
|
|
||||||
label: 'Client Secret',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
readOnly: false,
|
|
||||||
value: null,
|
|
||||||
placeholder: null,
|
|
||||||
description: null,
|
|
||||||
clickToCopy: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
generateAuthUrl,
|
|
||||||
verifyCredentials,
|
|
||||||
isStillVerified,
|
|
||||||
refreshToken,
|
|
||||||
};
|
|
@@ -1,8 +0,0 @@
|
|||||||
import getCurrentUser from '../common/get-current-user.js';
|
|
||||||
|
|
||||||
const isStillVerified = async ($) => {
|
|
||||||
const currentUser = await getCurrentUser($);
|
|
||||||
return !!currentUser.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default isStillVerified;
|
|
@@ -1,40 +0,0 @@
|
|||||||
import { URLSearchParams } from 'node:url';
|
|
||||||
|
|
||||||
import authScope from '../common/auth-scope.js';
|
|
||||||
|
|
||||||
const refreshToken = async ($) => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
client_id: $.auth.data.clientId,
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: $.auth.data.refreshToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicAuthToken = Buffer.from(
|
|
||||||
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
|
||||||
).toString('base64');
|
|
||||||
|
|
||||||
const { data } = await $.http.post(
|
|
||||||
'https://airtable.com/oauth2/v1/token',
|
|
||||||
params.toString(),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Authorization: `Basic ${basicAuthToken}`,
|
|
||||||
},
|
|
||||||
additionalProperties: {
|
|
||||||
skipAddingAuthHeader: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await $.auth.set({
|
|
||||||
accessToken: data.access_token,
|
|
||||||
refreshToken: data.refresh_token,
|
|
||||||
expiresIn: data.expires_in,
|
|
||||||
refreshExpiresIn: data.refresh_expires_in,
|
|
||||||
scope: authScope.join(' '),
|
|
||||||
tokenType: data.token_type,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default refreshToken;
|
|
@@ -1,56 +0,0 @@
|
|||||||
import getCurrentUser from '../common/get-current-user.js';
|
|
||||||
|
|
||||||
const verifyCredentials = async ($) => {
|
|
||||||
if ($.auth.data.originalState !== $.auth.data.state) {
|
|
||||||
throw new Error("The 'state' parameter does not match.");
|
|
||||||
}
|
|
||||||
if ($.auth.data.originalCodeChallenge !== $.auth.data.code_challenge) {
|
|
||||||
throw new Error("The 'code challenge' parameter does not match.");
|
|
||||||
}
|
|
||||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
|
||||||
(field) => field.key == 'oAuthRedirectUrl'
|
|
||||||
);
|
|
||||||
const redirectUri = oauthRedirectUrlField.value;
|
|
||||||
const basicAuthToken = Buffer.from(
|
|
||||||
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
|
||||||
).toString('base64');
|
|
||||||
|
|
||||||
const { data } = await $.http.post(
|
|
||||||
'https://airtable.com/oauth2/v1/token',
|
|
||||||
{
|
|
||||||
code: $.auth.data.code,
|
|
||||||
client_id: $.auth.data.clientId,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code_verifier: $.auth.data.codeVerifier,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Authorization: `Basic ${basicAuthToken}`,
|
|
||||||
},
|
|
||||||
additionalProperties: {
|
|
||||||
skipAddingAuthHeader: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await $.auth.set({
|
|
||||||
accessToken: data.access_token,
|
|
||||||
tokenType: data.token_type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentUser = await getCurrentUser($);
|
|
||||||
|
|
||||||
await $.auth.set({
|
|
||||||
clientId: $.auth.data.clientId,
|
|
||||||
clientSecret: $.auth.data.clientSecret,
|
|
||||||
scope: $.auth.data.scope,
|
|
||||||
expiresIn: data.expires_in,
|
|
||||||
refreshExpiresIn: data.refresh_expires_in,
|
|
||||||
refreshToken: data.refresh_token,
|
|
||||||
screenName: currentUser.email,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default verifyCredentials;
|
|
@@ -1,12 +0,0 @@
|
|||||||
const addAuthHeader = ($, requestConfig) => {
|
|
||||||
if (
|
|
||||||
!requestConfig.additionalProperties?.skipAddingAuthHeader &&
|
|
||||||
$.auth.data?.accessToken
|
|
||||||
) {
|
|
||||||
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default addAuthHeader;
|
|
@@ -1,12 +0,0 @@
|
|||||||
const authScope = [
|
|
||||||
'data.records:read',
|
|
||||||
'data.records:write',
|
|
||||||
'data.recordComments:read',
|
|
||||||
'data.recordComments:write',
|
|
||||||
'schema.bases:read',
|
|
||||||
'schema.bases:write',
|
|
||||||
'user.email:read',
|
|
||||||
'webhook:manage',
|
|
||||||
];
|
|
||||||
|
|
||||||
export default authScope;
|
|
@@ -1,6 +0,0 @@
|
|||||||
const getCurrentUser = async ($) => {
|
|
||||||
const { data: currentUser } = await $.http.get('/v0/meta/whoami');
|
|
||||||
return currentUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getCurrentUser;
|
|
@@ -1,6 +0,0 @@
|
|||||||
import listBases from './list-bases/index.js';
|
|
||||||
import listTableFields from './list-table-fields/index.js';
|
|
||||||
import listTableViews from './list-table-views/index.js';
|
|
||||||
import listTables from './list-tables/index.js';
|
|
||||||
|
|
||||||
export default [listBases, listTableFields, listTableViews, listTables];
|
|
@@ -1,28 +0,0 @@
|
|||||||
export default {
|
|
||||||
name: 'List bases',
|
|
||||||
key: 'listBases',
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const bases = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const params = {};
|
|
||||||
|
|
||||||
do {
|
|
||||||
const { data } = await $.http.get('/v0/meta/bases', { params });
|
|
||||||
params.offset = data.offset;
|
|
||||||
|
|
||||||
if (data?.bases) {
|
|
||||||
for (const base of data.bases) {
|
|
||||||
bases.data.push({
|
|
||||||
value: base.id,
|
|
||||||
name: base.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (params.offset);
|
|
||||||
|
|
||||||
return bases;
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,39 +0,0 @@
|
|||||||
export default {
|
|
||||||
name: 'List table fields',
|
|
||||||
key: 'listTableFields',
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const tableFields = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
const { baseId, tableId } = $.step.parameters;
|
|
||||||
|
|
||||||
if (!baseId) {
|
|
||||||
return tableFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {};
|
|
||||||
|
|
||||||
do {
|
|
||||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
params.offset = data.offset;
|
|
||||||
|
|
||||||
if (data?.tables) {
|
|
||||||
for (const table of data.tables) {
|
|
||||||
if (table.id === tableId) {
|
|
||||||
table.fields.forEach((field) => {
|
|
||||||
tableFields.data.push({
|
|
||||||
value: field.name,
|
|
||||||
name: field.name,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (params.offset);
|
|
||||||
|
|
||||||
return tableFields;
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,39 +0,0 @@
|
|||||||
export default {
|
|
||||||
name: 'List table views',
|
|
||||||
key: 'listTableViews',
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const tableViews = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
const { baseId, tableId } = $.step.parameters;
|
|
||||||
|
|
||||||
if (!baseId) {
|
|
||||||
return tableViews;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {};
|
|
||||||
|
|
||||||
do {
|
|
||||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
params.offset = data.offset;
|
|
||||||
|
|
||||||
if (data?.tables) {
|
|
||||||
for (const table of data.tables) {
|
|
||||||
if (table.id === tableId) {
|
|
||||||
table.views.forEach((view) => {
|
|
||||||
tableViews.data.push({
|
|
||||||
value: view.id,
|
|
||||||
name: view.name,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (params.offset);
|
|
||||||
|
|
||||||
return tableViews;
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,35 +0,0 @@
|
|||||||
export default {
|
|
||||||
name: 'List tables',
|
|
||||||
key: 'listTables',
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const tables = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
const baseId = $.step.parameters.baseId;
|
|
||||||
|
|
||||||
if (!baseId) {
|
|
||||||
return tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {};
|
|
||||||
|
|
||||||
do {
|
|
||||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
params.offset = data.offset;
|
|
||||||
|
|
||||||
if (data?.tables) {
|
|
||||||
for (const table of data.tables) {
|
|
||||||
tables.data.push({
|
|
||||||
value: table.id,
|
|
||||||
name: table.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (params.offset);
|
|
||||||
|
|
||||||
return tables;
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,3 +0,0 @@
|
|||||||
import listFields from './list-fields/index.js';
|
|
||||||
|
|
||||||
export default [listFields];
|
|
@@ -1,86 +0,0 @@
|
|||||||
const hasValue = (value) => value !== null && value !== undefined;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'List fields',
|
|
||||||
key: 'listFields',
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const options = [];
|
|
||||||
const { baseId, tableId } = $.step.parameters;
|
|
||||||
|
|
||||||
if (!hasValue(baseId) || !hasValue(tableId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`);
|
|
||||||
|
|
||||||
const selectedTable = data.tables.find((table) => table.id === tableId);
|
|
||||||
|
|
||||||
if (!selectedTable) return;
|
|
||||||
|
|
||||||
selectedTable.fields.forEach((field) => {
|
|
||||||
if (field.type === 'singleSelect') {
|
|
||||||
options.push({
|
|
||||||
label: field.name,
|
|
||||||
key: field.name,
|
|
||||||
type: 'dropdown',
|
|
||||||
required: false,
|
|
||||||
variables: true,
|
|
||||||
options: field.options.choices.map((choice) => ({
|
|
||||||
label: choice.name,
|
|
||||||
value: choice.id,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
} else if (field.type === 'multipleSelects') {
|
|
||||||
options.push({
|
|
||||||
label: field.name,
|
|
||||||
key: field.name,
|
|
||||||
type: 'dynamic',
|
|
||||||
required: false,
|
|
||||||
variables: true,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: 'Value',
|
|
||||||
key: 'value',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: false,
|
|
||||||
variables: true,
|
|
||||||
options: field.options.choices.map((choice) => ({
|
|
||||||
label: choice.name,
|
|
||||||
value: choice.id,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else if (field.type === 'checkbox') {
|
|
||||||
options.push({
|
|
||||||
label: field.name,
|
|
||||||
key: field.name,
|
|
||||||
type: 'dropdown',
|
|
||||||
required: false,
|
|
||||||
variables: true,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: 'Yes',
|
|
||||||
value: 'true',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'No',
|
|
||||||
value: 'false',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
options.push({
|
|
||||||
label: field.name,
|
|
||||||
key: field.name,
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
variables: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,22 +0,0 @@
|
|||||||
import defineApp from '../../helpers/define-app.js';
|
|
||||||
import addAuthHeader from './common/add-auth-header.js';
|
|
||||||
import auth from './auth/index.js';
|
|
||||||
import actions from './actions/index.js';
|
|
||||||
import dynamicData from './dynamic-data/index.js';
|
|
||||||
import dynamicFields from './dynamic-fields/index.js';
|
|
||||||
|
|
||||||
export default defineApp({
|
|
||||||
name: 'Airtable',
|
|
||||||
key: 'airtable',
|
|
||||||
baseUrl: 'https://airtable.com',
|
|
||||||
apiBaseUrl: 'https://api.airtable.com',
|
|
||||||
iconUrl: '{BASE_URL}/apps/airtable/assets/favicon.svg',
|
|
||||||
authDocUrl: '{DOCS_URL}/apps/airtable/connection',
|
|
||||||
primaryColor: 'FFBF00',
|
|
||||||
supportsConnections: true,
|
|
||||||
beforeRequest: [addAuthHeader],
|
|
||||||
auth,
|
|
||||||
actions,
|
|
||||||
dynamicData,
|
|
||||||
dynamicFields,
|
|
||||||
});
|
|
@@ -1 +0,0 @@
|
|||||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="132" height="24" fill="none" viewBox="0 0 132 24"><path fill="#19191C" d="M38.557 19.495c2.16 0 3.25-1.113 3.725-1.87h.214c.094.805.664 1.562 1.779 1.562h2.111V16.82h-.545c-.38 0-.57-.213-.57-.545V6.776h-2.8v1.516h-.213c-.545-.758-1.684-1.824-3.772-1.824-3.321 0-5.789 2.748-5.789 6.514s2.515 6.513 5.86 6.513m.498-2.7c-1.969 0-3.51-1.445-3.51-3.79 0-2.297 1.494-3.86 3.487-3.86 1.898 0 3.487 1.397 3.487 3.86 0 2.108-1.352 3.79-3.463 3.79M48.04 24h2.799v-6.376h.213c.522.758 1.637 1.871 3.844 1.871 3.321 0 5.741-2.795 5.741-6.513 0-3.743-2.586-6.514-5.931-6.514-2.135 0-3.18 1.16-3.678 1.8h-.213V6.776h-2.776V24m6.263-7.134c-1.922 0-3.512-1.42-3.512-3.884 0-2.108 1.353-3.885 3.464-3.885 1.97 0 3.511 1.54 3.511 3.885 0 2.297-1.494 3.884-3.463 3.884M62.082 24h2.8v-6.376h.213c.522.758 1.637 1.871 3.843 1.871 3.321 0 5.51-2.795 5.51-6.513 0-3.743-2.355-6.514-5.7-6.514-2.135 0-3.179 1.16-3.677 1.8h-.214V6.776h-2.775zm6.263-7.134c-1.922 0-3.511-1.42-3.511-3.884 0-2.108 1.352-3.885 3.463-3.885 1.97 0 3.512 1.54 3.512 3.885 0 2.297-1.495 3.884-3.464 3.884m9.805 2.61h3.961l2.254-9.735h.143l2.253 9.735H90.7l3.153-12.412h-2.821l-2.254 9.759h-.214l-2.253-9.759h-3.725l-2.278 9.759h-.213l-2.23-9.759h-2.99l3.274 12.412m17.123 0h2.8V13.34c0-2.345 1.09-3.79 3.131-3.79h1.233V6.756h-.925c-1.59 0-2.8 1.09-3.274 2.132h-.19V7.064h-2.775zm21.057 0h2.183v-2.487h-2.159c-.854 0-1.21-.38-1.21-1.256V9.528h3.511V7.064h-3.511V3.582h-2.657v3.482h-2.325v2.464h2.159v6.229c0 2.63 1.589 3.719 4.009 3.719m9.693.019c2.586 0 4.864-1.279 5.67-3.86l-2.562-.616c-.451 1.373-1.755 2.084-3.131 2.084-2.041 0-3.393-1.326-3.417-3.41h9.419v-.782c0-3.695-2.301-6.443-6.097-6.443-3.346 0-6.216 2.63-6.216 6.537 0 3.79 2.538 6.49 6.334 6.49m-3.416-7.84c.166-1.492 1.518-2.747 3.298-2.747 1.708 0 3.108 1.066 3.25 2.747h-6.548"/><path fill="#19191C" fill-rule="evenodd" d="M108.916 19.476h-2.8V9.528h-2.182V7.064h4.982z" clip-rule="evenodd"/><path fill="#19191C" d="M107.309 5.342c1.02 0 1.779-.758 1.779-1.753 0-.971-.759-1.73-1.779-1.73-1.021 0-1.78.759-1.78 1.73 0 .995.759 1.753 1.78 1.753"/><path fill="#FD366E" d="M24.443 16.432v5.478H10.752c-3.989 0-7.472-2.203-9.335-5.478A11.041 11.041 0 0 1 0 11.695v-1.48a10.97 10.97 0 0 1 .381-2.247C1.661 3.368 5.82 0 10.751 0c4.934 0 9.092 3.37 10.371 7.967h-5.854c-.96-1.499-2.624-2.49-4.516-2.49s-3.555.991-4.516 2.49a5.47 5.47 0 0 0-.67 1.494 5.562 5.562 0 0 0-.202 1.494 5.5 5.5 0 0 0 1.69 3.983 5.32 5.32 0 0 0 3.698 1.494h13.69"/><path fill="#FD366E" d="M24.443 9.46v5.478h-9.994a5.5 5.5 0 0 0 1.691-3.983 5.56 5.56 0 0 0-.203-1.494h8.506"/></svg>
|
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,5 +0,0 @@
|
|||||||
const verifyCredentials = async ($) => {
|
|
||||||
await $.http.get('/v1/users');
|
|
||||||
};
|
|
||||||
|
|
||||||
export default verifyCredentials;
|
|
@@ -1,16 +0,0 @@
|
|||||||
const addAuthHeader = ($, requestConfig) => {
|
|
||||||
requestConfig.headers['Content-Type'] = 'application/json';
|
|
||||||
|
|
||||||
if ($.auth.data?.apiKey && $.auth.data?.projectId) {
|
|
||||||
requestConfig.headers['X-Appwrite-Project'] = $.auth.data.projectId;
|
|
||||||
requestConfig.headers['X-Appwrite-Key'] = $.auth.data.apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($.auth.data?.host) {
|
|
||||||
requestConfig.headers['Host'] = $.auth.data.host;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default addAuthHeader;
|
|
@@ -1,4 +0,0 @@
|
|||||||
import listCollections from './list-collections/index.js';
|
|
||||||
import listDatabases from './list-databases/index.js';
|
|
||||||
|
|
||||||
export default [listCollections, listDatabases];
|
|
@@ -1,44 +0,0 @@
|
|||||||
export default {
|
|
||||||
name: 'List collections',
|
|
||||||
key: 'listCollections',
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const collections = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
const databaseId = $.step.parameters.databaseId;
|
|
||||||
|
|
||||||
if (!databaseId) {
|
|
||||||
return collections;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
queries: [
|
|
||||||
JSON.stringify({
|
|
||||||
method: 'orderAsc',
|
|
||||||
attribute: 'name',
|
|
||||||
}),
|
|
||||||
JSON.stringify({
|
|
||||||
method: 'limit',
|
|
||||||
values: [100],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data } = await $.http.get(
|
|
||||||
`/v1/databases/${databaseId}/collections`,
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data?.collections) {
|
|
||||||
for (const collection of data.collections) {
|
|
||||||
collections.data.push({
|
|
||||||
value: collection.$id,
|
|
||||||
name: collection.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return collections;
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,36 +0,0 @@
|
|||||||
export default {
|
|
||||||
name: 'List databases',
|
|
||||||
key: 'listDatabases',
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const databases = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
queries: [
|
|
||||||
JSON.stringify({
|
|
||||||
method: 'orderAsc',
|
|
||||||
attribute: 'name',
|
|
||||||
}),
|
|
||||||
JSON.stringify({
|
|
||||||
method: 'limit',
|
|
||||||
values: [100],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data } = await $.http.get('/v1/databases', { params });
|
|
||||||
|
|
||||||
if (data?.databases) {
|
|
||||||
for (const database of data.databases) {
|
|
||||||
databases.data.push({
|
|
||||||
value: database.$id,
|
|
||||||
name: database.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return databases;
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,21 +0,0 @@
|
|||||||
import defineApp from '../../helpers/define-app.js';
|
|
||||||
import addAuthHeader from './common/add-auth-header.js';
|
|
||||||
import setBaseUrl from './common/set-base-url.js';
|
|
||||||
import auth from './auth/index.js';
|
|
||||||
import triggers from './triggers/index.js';
|
|
||||||
import dynamicData from './dynamic-data/index.js';
|
|
||||||
|
|
||||||
export default defineApp({
|
|
||||||
name: 'Appwrite',
|
|
||||||
key: 'appwrite',
|
|
||||||
baseUrl: 'https://appwrite.io',
|
|
||||||
apiBaseUrl: 'https://cloud.appwrite.io',
|
|
||||||
iconUrl: '{BASE_URL}/apps/appwrite/assets/favicon.svg',
|
|
||||||
authDocUrl: '{DOCS_URL}/apps/appwrite/connection',
|
|
||||||
primaryColor: 'FD366E',
|
|
||||||
supportsConnections: true,
|
|
||||||
beforeRequest: [setBaseUrl, addAuthHeader],
|
|
||||||
auth,
|
|
||||||
triggers,
|
|
||||||
dynamicData,
|
|
||||||
});
|
|
@@ -1,3 +0,0 @@
|
|||||||
import newDocuments from './new-documents/index.js';
|
|
||||||
|
|
||||||
export default [newDocuments];
|
|
@@ -1,104 +0,0 @@
|
|||||||
import defineTrigger from '../../../../helpers/define-trigger.js';
|
|
||||||
|
|
||||||
export default defineTrigger({
|
|
||||||
name: 'New documents',
|
|
||||||
key: 'newDocuments',
|
|
||||||
pollInterval: 15,
|
|
||||||
description: 'Triggers when a new document is created.',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
label: 'Database',
|
|
||||||
key: 'databaseId',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
description: '',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listDatabases',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Collection',
|
|
||||||
key: 'collectionId',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
dependsOn: ['parameters.databaseId'],
|
|
||||||
description: '',
|
|
||||||
variables: true,
|
|
||||||
source: {
|
|
||||||
type: 'query',
|
|
||||||
name: 'getDynamicData',
|
|
||||||
arguments: [
|
|
||||||
{
|
|
||||||
name: 'key',
|
|
||||||
value: 'listCollections',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters.databaseId',
|
|
||||||
value: '{parameters.databaseId}',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
async run($) {
|
|
||||||
const { databaseId, collectionId } = $.step.parameters;
|
|
||||||
|
|
||||||
const limit = 1;
|
|
||||||
let lastDocumentId = undefined;
|
|
||||||
let offset = 0;
|
|
||||||
let documentCount = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
const params = {
|
|
||||||
queries: [
|
|
||||||
JSON.stringify({
|
|
||||||
method: 'orderDesc',
|
|
||||||
attribute: '$createdAt',
|
|
||||||
}),
|
|
||||||
JSON.stringify({
|
|
||||||
method: 'limit',
|
|
||||||
values: [limit],
|
|
||||||
}),
|
|
||||||
// An invalid cursor shouldn't be sent.
|
|
||||||
lastDocumentId &&
|
|
||||||
JSON.stringify({
|
|
||||||
method: 'cursorAfter',
|
|
||||||
values: [lastDocumentId],
|
|
||||||
}),
|
|
||||||
].filter(Boolean),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data } = await $.http.get(
|
|
||||||
`/v1/databases/${databaseId}/collections/${collectionId}/documents`,
|
|
||||||
{ params }
|
|
||||||
);
|
|
||||||
|
|
||||||
const documents = data?.documents;
|
|
||||||
documentCount = documents?.length;
|
|
||||||
offset = offset + limit;
|
|
||||||
lastDocumentId = documents[documentCount - 1]?.$id;
|
|
||||||
|
|
||||||
if (!documentCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const document of documents) {
|
|
||||||
$.pushTriggerItem({
|
|
||||||
raw: document,
|
|
||||||
meta: {
|
|
||||||
internalId: document.$id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} while (documentCount === limit);
|
|
||||||
},
|
|
||||||
});
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.9 KiB |
@@ -16,14 +16,14 @@ export default {
|
|||||||
clickToCopy: false,
|
clickToCopy: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'projectId',
|
key: 'instanceUrl',
|
||||||
label: 'Project ID',
|
label: 'Instance URL',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: null,
|
placeholder: null,
|
||||||
description: 'Project ID of your Appwrite project.',
|
description: null,
|
||||||
clickToCopy: false,
|
clickToCopy: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,28 +34,7 @@ export default {
|
|||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: null,
|
placeholder: null,
|
||||||
description: 'API key of your Appwrite project.',
|
description: 'Changedetection API key of your account.',
|
||||||
clickToCopy: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'instanceUrl',
|
|
||||||
label: 'Appwrite instance URL',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
readOnly: false,
|
|
||||||
placeholder: '',
|
|
||||||
description: '',
|
|
||||||
clickToCopy: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'host',
|
|
||||||
label: 'Host Name',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
readOnly: false,
|
|
||||||
value: null,
|
|
||||||
placeholder: null,
|
|
||||||
description: 'Host name of your Appwrite project.',
|
|
||||||
clickToCopy: false,
|
clickToCopy: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
@@ -0,0 +1,10 @@
|
|||||||
|
const verifyCredentials = async ($) => {
|
||||||
|
await $.http.get('/v1/systeminfo');
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
screenName: $.auth.data.screenName,
|
||||||
|
apiKey: $.auth.data.apiKey,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
@@ -0,0 +1,9 @@
|
|||||||
|
const addAuthHeader = ($, requestConfig) => {
|
||||||
|
if ($.auth.data?.apiKey) {
|
||||||
|
requestConfig.headers['x-api-key'] = $.auth.data.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
@@ -1,10 +1,7 @@
|
|||||||
const setBaseUrl = ($, requestConfig) => {
|
const setBaseUrl = ($, requestConfig) => {
|
||||||
const instanceUrl = $.auth.data.instanceUrl;
|
const instanceUrl = $.auth.data.instanceUrl;
|
||||||
|
|
||||||
if (instanceUrl) {
|
if (instanceUrl) {
|
||||||
requestConfig.baseURL = instanceUrl;
|
requestConfig.baseURL = `${instanceUrl}/api`;
|
||||||
} else if ($.app.apiBaseUrl) {
|
|
||||||
requestConfig.baseURL = $.app.apiBaseUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestConfig;
|
return requestConfig;
|
17
packages/backend/src/apps/changedetection/index.js
Normal file
17
packages/backend/src/apps/changedetection/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import defineApp from '../../helpers/define-app.js';
|
||||||
|
import addAuthHeader from './common/add-auth-header.js';
|
||||||
|
import auth from './auth/index.js';
|
||||||
|
import setBaseUrl from './common/set-base-url.js';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Changedetection',
|
||||||
|
key: 'changedetection',
|
||||||
|
iconUrl: '{BASE_URL}/apps/changedetection/assets/favicon.svg',
|
||||||
|
authDocUrl: '{DOCS_URL}/apps/changedetection/connection',
|
||||||
|
supportsConnections: true,
|
||||||
|
baseUrl: 'https://changedetection.io',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
primaryColor: '3056d3',
|
||||||
|
beforeRequest: [setBaseUrl, addAuthHeader],
|
||||||
|
auth,
|
||||||
|
});
|
@@ -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
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
@@ -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
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,4 +0,0 @@
|
|||||||
import createHmac from './create-hmac/index.js';
|
|
||||||
import createRsaSha256Signature from './create-rsa-sha256-signature/index.js';
|
|
||||||
|
|
||||||
export default [createHmac, createRsaSha256Signature];
|
|
@@ -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 |
@@ -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,
|
|
||||||
});
|
|
@@ -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',
|
||||||
|
@@ -5,24 +5,11 @@ const formatDateTime = ($) => {
|
|||||||
|
|
||||||
const fromFormat = $.step.parameters.fromFormat;
|
const fromFormat = $.step.parameters.fromFormat;
|
||||||
const fromTimezone = $.step.parameters.fromTimezone;
|
const fromTimezone = $.step.parameters.fromTimezone;
|
||||||
let inputDateTime;
|
|
||||||
|
|
||||||
if (fromFormat === 'X') {
|
const inputDateTime = DateTime.fromFormat(input, fromFormat, {
|
||||||
inputDateTime = DateTime.fromSeconds(Number(input), fromFormat, {
|
zone: fromTimezone,
|
||||||
zone: fromTimezone,
|
setZone: true,
|
||||||
setZone: true,
|
});
|
||||||
});
|
|
||||||
} else if (fromFormat === 'x') {
|
|
||||||
inputDateTime = DateTime.fromMillis(Number(input), fromFormat, {
|
|
||||||
zone: fromTimezone,
|
|
||||||
setZone: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
inputDateTime = DateTime.fromFormat(input, fromFormat, {
|
|
||||||
zone: fromTimezone,
|
|
||||||
setZone: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const toFormat = $.step.parameters.toFormat;
|
const toFormat = $.step.parameters.toFormat;
|
||||||
const toTimezone = $.step.parameters.toTimezone;
|
const toTimezone = $.step.parameters.toTimezone;
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
const getCurrentTimestamp = () => {
|
|
||||||
return Date.now();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getCurrentTimestamp;
|
|
@@ -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' },
|
||||||
],
|
],
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
const createUuidV4 = () => {
|
|
||||||
return uuidv4();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createUuidV4;
|
|
@@ -1,7 +0,0 @@
|
|||||||
const parseStringifiedJson = ($) => {
|
|
||||||
const input = $.step.parameters.input;
|
|
||||||
|
|
||||||
return JSON.parse(input);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parseStringifiedJson;
|
|
@@ -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);
|
||||||
};
|
};
|
||||||
|
@@ -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];
|
||||||
|
@@ -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 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
@@ -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 {
|
||||||
|
@@ -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;
|
|
@@ -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;
|
||||||
|
@@ -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 });
|
|
||||||
},
|
|
||||||
});
|
|
@@ -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];
|
||||||
|
@@ -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.13.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) {
|
||||||
|
@@ -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();
|
|
||||||
};
|
|
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -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);
|
||||||
|
@@ -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')
|
||||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
|||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
data: {
|
data: {
|
||||||
version: '0.13.0',
|
version: '0.11.0',
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
@@ -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();
|
|
||||||
};
|
|
@@ -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();
|
|
||||||
};
|
|
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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.';
|
|
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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();
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
export async function up(knex) {
|
|
||||||
return knex.schema.alterTable('datastore', (table) => {
|
|
||||||
table.text('value').alter();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex) {
|
|
||||||
return knex.schema.alterTable('datastore', (table) => {
|
|
||||||
table.string('value').alter();
|
|
||||||
});
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
});
|
|
||||||
}
|
|
@@ -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');
|
|
||||||
});
|
|
||||||
}
|
|
@@ -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,
|
||||||
|
@@ -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;
|
||||||
|
24
packages/backend/src/graphql/mutations/delete-user.ee.js
Normal file
24
packages/backend/src/graphql/mutations/delete-user.ee.js
Normal 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;
|
43
packages/backend/src/graphql/mutations/forgot-password.ee.js
Normal file
43
packages/backend/src/graphql/mutations/forgot-password.ee.js
Normal 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;
|
17
packages/backend/src/graphql/mutations/login.js
Normal file
17
packages/backend/src/graphql/mutations/login.js
Normal 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;
|
23
packages/backend/src/graphql/mutations/reset-password.ee.js
Normal file
23
packages/backend/src/graphql/mutations/reset-password.ee.js
Normal 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;
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,102 +1,43 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export function createInstance(customConfig = {}, { requestInterceptor, responseErrorInterceptor } = {}) {
|
const config = axios.defaults;
|
||||||
const config = {
|
const httpProxyUrl = process.env.http_proxy;
|
||||||
...axios.defaults,
|
const httpsProxyUrl = process.env.https_proxy;
|
||||||
...customConfig
|
const supportsProxy = httpProxyUrl || httpsProxyUrl;
|
||||||
};
|
const noProxyEnv = process.env.no_proxy;
|
||||||
const httpProxyUrl = appConfig.httpProxy;
|
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : [];
|
||||||
const httpsProxyUrl = appConfig.httpsProxy;
|
|
||||||
const supportsProxy = httpProxyUrl || httpsProxyUrl;
|
|
||||||
const noProxyEnv = appConfig.noProxy;
|
|
||||||
const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : [];
|
|
||||||
|
|
||||||
if (supportsProxy) {
|
if (supportsProxy) {
|
||||||
if (httpProxyUrl) {
|
if (httpProxyUrl) {
|
||||||
config.httpAgent = new HttpProxyAgent(httpProxyUrl);
|
config.httpAgent = new HttpProxyAgent(httpProxyUrl);
|
||||||
}
|
|
||||||
|
|
||||||
if (httpsProxyUrl) {
|
|
||||||
config.httpsAgent = new HttpsProxyAgent(httpsProxyUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.proxy = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = axios.create(config);
|
if (httpsProxyUrl) {
|
||||||
|
config.httpsAgent = new HttpsProxyAgent(httpsProxyUrl);
|
||||||
function shouldSkipProxy(hostname) {
|
|
||||||
return noProxyHosts.some(noProxyHost => {
|
|
||||||
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interceptors are executed in the reverse order they are added.
|
|
||||||
*/
|
|
||||||
instance.interceptors.request.use(
|
|
||||||
function skipProxyIfInNoProxy(requestConfig) {
|
|
||||||
const hostname = new URL(requestConfig.baseURL).hostname;
|
|
||||||
|
|
||||||
if (supportsProxy && shouldSkipProxy(hostname)) {
|
|
||||||
requestConfig.httpAgent = undefined;
|
|
||||||
requestConfig.httpsAgent = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestConfig;
|
|
||||||
},
|
|
||||||
(error) => Promise.reject(error)
|
|
||||||
);
|
|
||||||
|
|
||||||
// not always we have custom request interceptors
|
|
||||||
if (requestInterceptor) {
|
|
||||||
instance.interceptors.request.use(
|
|
||||||
async function customInterceptor(requestConfig) {
|
|
||||||
let newRequestConfig = requestConfig;
|
|
||||||
|
|
||||||
for (const interceptor of requestInterceptor) {
|
|
||||||
newRequestConfig = await interceptor(newRequestConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRequestConfig;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.interceptors.request.use(
|
config.proxy = false;
|
||||||
function removeBaseUrlForAbsoluteUrls(requestConfig) {
|
|
||||||
/**
|
|
||||||
* If the URL is an absolute URL, we remove its origin out of the URL
|
|
||||||
* and set it as baseURL. This lets us streamlines the requests made by Automatisch
|
|
||||||
* and requests made by app integrations.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
const url = new URL(requestConfig.url);
|
|
||||||
requestConfig.baseURL = url.origin;
|
|
||||||
requestConfig.url = url.pathname + url.search;
|
|
||||||
|
|
||||||
return requestConfig;
|
|
||||||
} catch (err) {
|
|
||||||
return requestConfig;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => Promise.reject(error)
|
|
||||||
);
|
|
||||||
|
|
||||||
// not always we have custom response error interceptor
|
|
||||||
if (responseErrorInterceptor) {
|
|
||||||
instance.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
responseErrorInterceptor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultInstance = createInstance();
|
const axiosWithProxyInstance = axios.create(config);
|
||||||
|
|
||||||
export default defaultInstance;
|
function shouldSkipProxy(hostname) {
|
||||||
|
return noProxyHosts.some(noProxyHost => {
|
||||||
|
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosWithProxyInstance.interceptors.request.use(function skipProxyIfInNoProxy(requestConfig) {
|
||||||
|
const hostname = new URL(requestConfig.url).hostname;
|
||||||
|
|
||||||
|
if (supportsProxy && shouldSkipProxy(hostname)) {
|
||||||
|
requestConfig.httpAgent = undefined;
|
||||||
|
requestConfig.httpsAgent = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default axiosWithProxyInstance;
|
||||||
|
@@ -1,169 +0,0 @@
|
|||||||
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
|
||||||
|
|
||||||
describe('Custom default axios with proxy', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have two interceptors by default', async () => {
|
|
||||||
const axios = (await import('./axios-with-proxy.js')).default;
|
|
||||||
const requestInterceptors = axios.interceptors.request.handlers;
|
|
||||||
|
|
||||||
expect(requestInterceptors.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have default interceptors in a certain order', async () => {
|
|
||||||
const axios = (await import('./axios-with-proxy.js')).default;
|
|
||||||
|
|
||||||
const requestInterceptors = axios.interceptors.request.handlers;
|
|
||||||
const firstRequestInterceptor = requestInterceptors[0];
|
|
||||||
const secondRequestInterceptor = requestInterceptors[1];
|
|
||||||
|
|
||||||
expect(firstRequestInterceptor.fulfilled.name).toBe('skipProxyIfInNoProxy');
|
|
||||||
expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw with invalid url (consisting of path alone)', async () => {
|
|
||||||
const axios = (await import('./axios-with-proxy.js')).default;
|
|
||||||
|
|
||||||
await expect(() => axios('/just-a-path')).rejects.toThrowError('Invalid URL');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with skipProxyIfInNoProxy interceptor', () => {
|
|
||||||
let appConfig, axios;
|
|
||||||
beforeEach(async() => {
|
|
||||||
appConfig = (await import('../config/app.js')).default;
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'httpProxy', 'get').mockReturnValue('http://proxy.automatisch.io');
|
|
||||||
vi.spyOn(appConfig, 'httpsProxy', 'get').mockReturnValue('http://proxy.automatisch.io');
|
|
||||||
vi.spyOn(appConfig, 'noProxy', 'get').mockReturnValue('name.tld,automatisch.io');
|
|
||||||
|
|
||||||
axios = (await import('./axios-with-proxy.js')).default;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip proxy for hosts in no_proxy environment variable', async () => {
|
|
||||||
const skipProxyIfInNoProxy = axios.interceptors.request.handlers[0].fulfilled;
|
|
||||||
|
|
||||||
const mockRequestConfig = {
|
|
||||||
...axios.defaults,
|
|
||||||
baseURL: 'https://automatisch.io'
|
|
||||||
};
|
|
||||||
|
|
||||||
const interceptedRequestConfig = skipProxyIfInNoProxy(mockRequestConfig);
|
|
||||||
|
|
||||||
expect(interceptedRequestConfig.httpAgent).toBeUndefined();
|
|
||||||
expect(interceptedRequestConfig.httpsAgent).toBeUndefined();
|
|
||||||
expect(interceptedRequestConfig.proxy).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not skip proxy for hosts not in no_proxy environment variable', async () => {
|
|
||||||
const skipProxyIfInNoProxy = axios.interceptors.request.handlers[0].fulfilled;
|
|
||||||
|
|
||||||
const mockRequestConfig = {
|
|
||||||
...axios.defaults,
|
|
||||||
// beware the intentional typo!
|
|
||||||
baseURL: 'https://automatish.io'
|
|
||||||
};
|
|
||||||
|
|
||||||
const interceptedRequestConfig = skipProxyIfInNoProxy(mockRequestConfig);
|
|
||||||
|
|
||||||
expect(interceptedRequestConfig.httpAgent).toBeDefined();
|
|
||||||
expect(interceptedRequestConfig.httpsAgent).toBeDefined();
|
|
||||||
expect(interceptedRequestConfig.proxy).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with removeBaseUrlForAbsoluteUrls interceptor', () => {
|
|
||||||
let axios;
|
|
||||||
beforeEach(async() => {
|
|
||||||
axios = (await import('./axios-with-proxy.js')).default;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trim the baseUrl from absolute urls', async () => {
|
|
||||||
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
|
|
||||||
|
|
||||||
const mockRequestConfig = {
|
|
||||||
...axios.defaults,
|
|
||||||
url: 'https://automatisch.io/path'
|
|
||||||
};
|
|
||||||
|
|
||||||
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
|
|
||||||
|
|
||||||
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
|
|
||||||
expect(interceptedRequestConfig.url).toBe('/path');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not mutate separate baseURL and urls', async () => {
|
|
||||||
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
|
|
||||||
|
|
||||||
const mockRequestConfig = {
|
|
||||||
...axios.defaults,
|
|
||||||
baseURL: 'https://automatisch.io',
|
|
||||||
url: '/path?query=1'
|
|
||||||
};
|
|
||||||
|
|
||||||
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
|
|
||||||
|
|
||||||
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
|
|
||||||
expect(interceptedRequestConfig.url).toBe('/path?query=1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not strip querystring from url', async () => {
|
|
||||||
const removeBaseUrlForAbsoluteUrls = axios.interceptors.request.handlers[1].fulfilled;
|
|
||||||
|
|
||||||
const mockRequestConfig = {
|
|
||||||
...axios.defaults,
|
|
||||||
url: 'https://automatisch.io/path?query=1'
|
|
||||||
};
|
|
||||||
|
|
||||||
const interceptedRequestConfig = removeBaseUrlForAbsoluteUrls(mockRequestConfig);
|
|
||||||
|
|
||||||
expect(interceptedRequestConfig.baseURL).toBe('https://automatisch.io');
|
|
||||||
expect(interceptedRequestConfig.url).toBe('/path?query=1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with extra requestInterceptors', () => {
|
|
||||||
it('should apply extra request interceptors in the middle', async () => {
|
|
||||||
const { createInstance } = await import('./axios-with-proxy.js');
|
|
||||||
|
|
||||||
const interceptor = (config) => {
|
|
||||||
config.test = true;
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = createInstance({}, {
|
|
||||||
requestInterceptor: [
|
|
||||||
interceptor
|
|
||||||
]
|
|
||||||
});
|
|
||||||
const requestInterceptors = instance.interceptors.request.handlers;
|
|
||||||
const customInterceptor = requestInterceptors[1].fulfilled;
|
|
||||||
|
|
||||||
expect(requestInterceptors.length).toBe(3);
|
|
||||||
await expect(customInterceptor({})).resolves.toStrictEqual({ test: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with a custom interceptor setting a baseURL and a request to path', async () => {
|
|
||||||
const { createInstance } = await import('./axios-with-proxy.js');
|
|
||||||
|
|
||||||
const interceptor = (config) => {
|
|
||||||
config.baseURL = 'http://localhost';
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = createInstance({}, {
|
|
||||||
requestInterceptor: [
|
|
||||||
interceptor
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await instance.get('/just-a-path');
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.config.baseURL).toBe('http://localhost');
|
|
||||||
expect(error.config.url).toBe('/just-a-path');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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);
|
||||||
|
@@ -11,7 +11,6 @@ export default function computeParameters(parameters, executionSteps) {
|
|||||||
const computedValue = parts
|
const computedValue = parts
|
||||||
.map((part) => {
|
.map((part) => {
|
||||||
const isVariable = part.match(variableRegExp);
|
const isVariable = part.match(variableRegExp);
|
||||||
|
|
||||||
if (isVariable) {
|
if (isVariable) {
|
||||||
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '');
|
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '');
|
||||||
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
|
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
|
||||||
@@ -21,32 +20,17 @@ export default function computeParameters(parameters, executionSteps) {
|
|||||||
});
|
});
|
||||||
const data = executionStep?.dataOut;
|
const data = executionStep?.dataOut;
|
||||||
const dataValue = get(data, keyPath);
|
const dataValue = get(data, keyPath);
|
||||||
|
|
||||||
// Covers both arrays and objects
|
|
||||||
if (typeof dataValue === 'object') {
|
|
||||||
return JSON.stringify(dataValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataValue;
|
return dataValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return part;
|
return part;
|
||||||
}).join('');
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
// challenge the input to see if it is stringifies object or array
|
return {
|
||||||
try {
|
...result,
|
||||||
const parsedValue = JSON.parse(computedValue);
|
[key]: computedValue,
|
||||||
|
};
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
[key]: parsedValue,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
[key]: computedValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
@@ -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 }) => {
|
||||||
|
@@ -1,43 +1,68 @@
|
|||||||
|
import { URL } from 'node:url';
|
||||||
import HttpError from '../../errors/http.js';
|
import HttpError from '../../errors/http.js';
|
||||||
import { createInstance } from '../axios-with-proxy.js';
|
import axios from '../axios-with-proxy.js';
|
||||||
|
|
||||||
|
const removeBaseUrlForAbsoluteUrls = (requestConfig) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(requestConfig.url);
|
||||||
|
requestConfig.baseURL = url.origin;
|
||||||
|
requestConfig.url = url.pathname + url.search;
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
} catch {
|
||||||
|
return requestConfig;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
|
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
|
||||||
async function interceptResponseError(error) {
|
const instance = axios.create({
|
||||||
const { config, response } = error;
|
baseURL,
|
||||||
// Do not destructure `status` from `error.response` because it might not exist
|
});
|
||||||
const status = response?.status;
|
|
||||||
|
|
||||||
if (
|
instance.interceptors.request.use((requestConfig) => {
|
||||||
// TODO: provide a `shouldRefreshToken` function in the app
|
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
|
||||||
(status === 401 || status === 403) &&
|
|
||||||
$.app.auth &&
|
|
||||||
$.app.auth.refreshToken &&
|
|
||||||
!$.app.auth.isRefreshTokenRequested
|
|
||||||
) {
|
|
||||||
$.app.auth.isRefreshTokenRequested = true;
|
|
||||||
await $.app.auth.refreshToken($);
|
|
||||||
|
|
||||||
// retry the previous request before the expired token error
|
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
|
||||||
const newResponse = await instance.request(config);
|
return beforeRequestFunc($, newConfig);
|
||||||
$.app.auth.isRefreshTokenRequested = false;
|
}, newRequestConfig);
|
||||||
|
|
||||||
return newResponse;
|
/**
|
||||||
|
* axios seems to want InternalAxiosRequestConfig returned not AxioRequestConfig
|
||||||
|
* anymore even though requests do require AxiosRequestConfig.
|
||||||
|
*
|
||||||
|
* Since both interfaces are very similar (InternalAxiosRequestConfig
|
||||||
|
* extends AxiosRequestConfig), we can utilize an assertion below
|
||||||
|
**/
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error) => {
|
||||||
|
const { config, response } = error;
|
||||||
|
// Do not destructure `status` from `error.response` because it might not exist
|
||||||
|
const status = response?.status;
|
||||||
|
|
||||||
|
if (
|
||||||
|
// TODO: provide a `shouldRefreshToken` function in the app
|
||||||
|
(status === 401 || status === 403) &&
|
||||||
|
$.app.auth &&
|
||||||
|
$.app.auth.refreshToken &&
|
||||||
|
!$.app.auth.isRefreshTokenRequested
|
||||||
|
) {
|
||||||
|
$.app.auth.isRefreshTokenRequested = true;
|
||||||
|
await $.app.auth.refreshToken($);
|
||||||
|
|
||||||
|
// retry the previous request before the expired token error
|
||||||
|
const newResponse = await instance.request(config);
|
||||||
|
$.app.auth.isRefreshTokenRequested = false;
|
||||||
|
|
||||||
|
return newResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpError(error);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
throw new HttpError(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const instance = createInstance(
|
|
||||||
{
|
|
||||||
baseURL,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
requestInterceptor: beforeRequest.map((originalBeforeRequest) => {
|
|
||||||
return async (requestConfig) => await originalBeforeRequest($, requestConfig);
|
|
||||||
}),
|
|
||||||
responseErrorInterceptor: interceptResponseError,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
@@ -63,8 +63,6 @@ export default async (flowId, request, response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (testRun) {
|
if (testRun) {
|
||||||
response.status(204).end();
|
|
||||||
|
|
||||||
// in case of testing, we do not process the whole process.
|
// in case of testing, we do not process the whole process.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -76,12 +74,6 @@ export default async (flowId, request, response) => {
|
|||||||
executionId,
|
executionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (actionStep.appKey === 'filter' && !actionExecutionStep.dataOut) {
|
|
||||||
response.status(422).end();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actionStep.key === 'respondWith' && !response.headersSent) {
|
if (actionStep.key === 'respondWith' && !response.headersSent) {
|
||||||
const { headers, statusCode, body } = actionExecutionStep.dataOut;
|
const { headers, statusCode, body } = actionExecutionStep.dataOut;
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
@@ -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;
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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>
|
|
@@ -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>
|
||||||
|
@@ -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 }
|
||||||
|
@@ -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!'
|
||||||
);
|
);
|
||||||
|
@@ -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(),
|
||||||
},
|
},
|
||||||
|
@@ -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(),
|
||||||
};
|
};
|
||||||
|
@@ -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,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user