Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1c76b02c78 | ||
![]() |
bc67e9c4c4 |
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,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,
|
|
||||||
});
|
|
21
packages/backend/src/apps/firefly-iii/assets/favicon.svg
Normal file
21
packages/backend/src/apps/firefly-iii/assets/favicon.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
- maskable-icon.svg
|
||||||
|
- Copyright (c) 2022 james@firefly-iii.org
|
||||||
|
-
|
||||||
|
- This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<svg height="377.95276" width="377.95276" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h377.95276v377.95276h-377.95276z" fill="#cd5029" stroke-width="1.96129"/><g transform="matrix(.77452773 0 0 .77452773 21.636074 21.374655)"><path d="m140.49013 78.646381 2.249 53.017999s-40.103 29.566-45.538 68l-16.001 1.231s-11.539 2.564-11.539 14.103v37.18s3.846 11.538 12.82 11.538l16.487-.319s8 30.5 36.5 50.5v25.5s-2 8.5 15.5 11 40.75 2.25 44.5-1.5 3.75-4.5 3.75-9c0 0 21.25 5 60.25 0v5s3.5 7 29 7 33-3 37.5-12v-25s37.009-36.264 35.75-91.75c-1.083-47.75-15.901-64.299-35.806-82.96-22.67-21.254-69.944-31.165-117.944-25.353.001-.001-24.341-43.937999-67.478-36.187999z" fill="#fff"/><circle cx="135.46912" cy="214.39638" fill="#cd5029" r="9.5"/><path d="m360.08113 190.51238s-18.218-8.742-40.662 3.996c0 0-26.711-8.987-40.99 2.593-14.828 12.025-16.299 26.115-15.525 42.785 0 0 12.837-43.915 45.252-32.571 0 0-22.947 40.43 12.761 47.508 0 0 8.436-.05 15.401-4.256 6.644-4.011 11.842-11.433 9.711-24.814 0 0-4.348-13.336-15.569-21.42 0 0 11.042-7.806 31.988-2.209z" fill="#cd5029"/><path d="m320.19013 213.01938s-16.689 31.461 5.607 29.767c0 0 11.838-5.656 4.887-17.127-7.147-11.796-10.494-12.64-10.494-12.64z" fill="#fff"/></g><path d="m188.97638 175.70052s4.01698 13.60604-3.69586 21.52748c-7.713 7.92145-6.8792 16.6767-3.75227 20.84588 3.12692 4.16917 2.91831 7.29593.41674 9.58905-2.50141 2.29312-4.58608 3.96073-6.04523.20846-1.45916-3.75228-3.12676-3.75228-3.75228-5.62834-.62552-1.87605-1.87622-5.21142-1.87622-5.21142s-3.96072 6.25384-6.46229 10.00611c-2.50157 3.75228-2.50141 9.58922-.83381 12.71598 1.66761 3.12676 1.04226 6.87903-.20845 12.09046-1.2507 5.21143.4169 13.13288 6.25369 16.2598 5.83678 3.12692 12.92459 5.62833 16.05135 8.5468s10.42301 5.62833 19.80362 3.54382c9.3806-2.0845 21.26294-11.67355 23.34744-18.13585 0 0 5.41988-6.04523 4.37763-13.96668s-4.79469-7.71316-6.4623-13.75839c-1.6676-6.04523 3.60854-4.55469-.8338-14.93382 0 0-1.98012-4.94005-9.50352-8.49899-4.83404-2.28661-1.54469-12.63061-10.09149-23.05347s-16.73295-12.14688-16.73295-12.14688z" fill="#ffa284" stroke-width=".162598"/></svg>
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
|
export default async function generateAuthUrl($) {
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value;
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
client_id: $.auth.data.clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
response_type: 'code',
|
||||||
|
});
|
||||||
|
const instanceUrl = $.auth.data.instanceUrl;
|
||||||
|
|
||||||
|
const url = `${instanceUrl}/oauth/authorize?${searchParams.toString()}`;
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}
|
58
packages/backend/src/apps/firefly-iii/auth/index.js
Normal file
58
packages/backend/src/apps/firefly-iii/auth/index.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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/firefly-iii/connections/add',
|
||||||
|
placeholder: null,
|
||||||
|
description: '',
|
||||||
|
clickToCopy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'instanceUrl',
|
||||||
|
label: 'Instance URL',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
readOnly: false,
|
||||||
|
value: null,
|
||||||
|
placeholder: null,
|
||||||
|
description: '',
|
||||||
|
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,
|
||||||
|
};
|
@@ -0,0 +1,8 @@
|
|||||||
|
import getCurrentUser from '../common/get-current-user.js';
|
||||||
|
|
||||||
|
const isStillVerified = async ($) => {
|
||||||
|
const currentUser = await getCurrentUser($);
|
||||||
|
return !!currentUser.attributes.email;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isStillVerified;
|
20
packages/backend/src/apps/firefly-iii/auth/refresh-token.js
Normal file
20
packages/backend/src/apps/firefly-iii/auth/refresh-token.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { URLSearchParams } from 'node:url';
|
||||||
|
|
||||||
|
const refreshToken = async ($) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: $.auth.data.clientId,
|
||||||
|
client_secret: $.auth.data.clientSecret,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: $.auth.data.refreshToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await $.http.post(`/oauth/token`, params.toString());
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
tokenType: data.token_type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default refreshToken;
|
@@ -0,0 +1,35 @@
|
|||||||
|
import getCurrentUser from '../common/get-current-user.js';
|
||||||
|
|
||||||
|
const verifyCredentials = async ($) => {
|
||||||
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
|
(field) => field.key == 'oAuthRedirectUrl'
|
||||||
|
);
|
||||||
|
const redirectUri = oauthRedirectUrlField.value;
|
||||||
|
const { data } = await $.http.post('/oauth/token', {
|
||||||
|
client_id: $.auth.data.clientId,
|
||||||
|
client_secret: $.auth.data.clientSecret,
|
||||||
|
code: $.auth.data.code,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
idToken: data.id_token,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
resourceName: currentUser.resourceName,
|
||||||
|
screenName: currentUser.attributes.email,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verifyCredentials;
|
@@ -0,0 +1,9 @@
|
|||||||
|
const addAuthHeader = ($, requestConfig) => {
|
||||||
|
if ($.auth.data?.accessToken) {
|
||||||
|
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default addAuthHeader;
|
@@ -0,0 +1,10 @@
|
|||||||
|
const getCurrentUser = async ($) => {
|
||||||
|
const { data: currentUser } = await $.http.get('/api/v1/about/user', {
|
||||||
|
Headers: {
|
||||||
|
Accept: 'application/vnd.api+json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return currentUser.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getCurrentUser;
|
10
packages/backend/src/apps/firefly-iii/common/set-base-url.js
Normal file
10
packages/backend/src/apps/firefly-iii/common/set-base-url.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const setBaseUrl = ($, requestConfig) => {
|
||||||
|
const instanceUrl = $.auth.data.instanceUrl;
|
||||||
|
if (instanceUrl) {
|
||||||
|
requestConfig.baseURL = instanceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setBaseUrl;
|
19
packages/backend/src/apps/firefly-iii/index.js
Normal file
19
packages/backend/src/apps/firefly-iii/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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';
|
||||||
|
import triggers from './triggers/index.js';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Firefly III',
|
||||||
|
key: 'firefly-iii',
|
||||||
|
baseUrl: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
iconUrl: '{BASE_URL}/apps/firefly-iii/assets/favicon.svg',
|
||||||
|
authDocUrl: '{DOCS_URL}/apps/firefly-iii/connection',
|
||||||
|
primaryColor: 'CD5029',
|
||||||
|
supportsConnections: true,
|
||||||
|
beforeRequest: [setBaseUrl, addAuthHeader],
|
||||||
|
auth,
|
||||||
|
triggers,
|
||||||
|
});
|
3
packages/backend/src/apps/firefly-iii/triggers/index.js
Normal file
3
packages/backend/src/apps/firefly-iii/triggers/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import transactionCreated from './transaction-created/index.js';
|
||||||
|
|
||||||
|
export default [transactionCreated];
|
@@ -0,0 +1,89 @@
|
|||||||
|
import Crypto from 'crypto';
|
||||||
|
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||||
|
|
||||||
|
export default defineTrigger({
|
||||||
|
name: 'Transaction created',
|
||||||
|
key: 'transactionCreated',
|
||||||
|
type: 'webhook',
|
||||||
|
description: 'Triggers when a new transaction is created.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Title of the webhook',
|
||||||
|
key: 'title',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
description: '',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const dataItem = {
|
||||||
|
raw: $.request.body,
|
||||||
|
meta: {
|
||||||
|
internalId: Crypto.randomUUID(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$.pushTriggerItem(dataItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
async testRun($) {
|
||||||
|
const { data: transactions } = await $.http.get(`/api/v1/transactions`);
|
||||||
|
|
||||||
|
if (transactions.data.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: transaction } = await $.http.get(
|
||||||
|
`/api/v1/transactions/${transactions.data[0].id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastTransaction = transaction.data;
|
||||||
|
|
||||||
|
if (!lastTransaction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedWebhookEvent = {
|
||||||
|
url: '',
|
||||||
|
uuid: Crypto.randomUUID(),
|
||||||
|
content: lastTransaction.attributes,
|
||||||
|
trigger: 'STORE_TRANSACTION',
|
||||||
|
user_id: lastTransaction.attributes.user,
|
||||||
|
version: '',
|
||||||
|
response: 'TRANSACTIONS',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataItem = {
|
||||||
|
raw: computedWebhookEvent,
|
||||||
|
meta: {
|
||||||
|
internalId: computedWebhookEvent.uuid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$.pushTriggerItem(dataItem);
|
||||||
|
},
|
||||||
|
|
||||||
|
async registerHook($) {
|
||||||
|
const title = $.step.parameters.title;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
active: true,
|
||||||
|
title: title || `Flow ID: ${$.flow.id}`,
|
||||||
|
trigger: 'STORE_TRANSACTION',
|
||||||
|
response: 'TRANSACTIONS',
|
||||||
|
delivery: 'JSON',
|
||||||
|
url: $.webhookUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await $.http.post('/api/v1/webhooks', payload);
|
||||||
|
const id = response.data.data.id;
|
||||||
|
|
||||||
|
await $.flow.setRemoteWebhookId(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
async unregisterHook($) {
|
||||||
|
await $.http.delete(`/api/v1/webhooks/${$.flow.remoteWebhookId}`);
|
||||||
|
},
|
||||||
|
});
|
@@ -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',
|
||||||
|
@@ -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.1',
|
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,17 +1,12 @@
|
|||||||
import appConfig from '../../../../config/app.js';
|
import appConfig from '../../../../config/app.js';
|
||||||
import { hasValidLicense } from '../../../../helpers/license.ee.js';
|
import { hasValidLicense } from '../../../../helpers/license.ee.js';
|
||||||
import { renderObject } from '../../../../helpers/renderer.js';
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
import Config from '../../../../models/config.js';
|
|
||||||
|
|
||||||
export default async (request, response) => {
|
export default async (request, response) => {
|
||||||
const installationCompleted = await Config.isInstallationCompleted();
|
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
docsUrl: appConfig.docsUrl,
|
|
||||||
installationCompleted,
|
|
||||||
isCloud: appConfig.isCloud,
|
isCloud: appConfig.isCloud,
|
||||||
isEnterprise: await hasValidLicense(),
|
|
||||||
isMation: appConfig.isMation,
|
isMation: appConfig.isMation,
|
||||||
|
isEnterprise: await hasValidLicense(),
|
||||||
};
|
};
|
||||||
|
|
||||||
renderObject(response, info);
|
renderObject(response, info);
|
||||||
|
@@ -1,18 +1,15 @@
|
|||||||
import { vi, expect, describe, it } from 'vitest';
|
import { vi, expect, describe, it } from 'vitest';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import appConfig from '../../../../config/app.js';
|
import appConfig from '../../../../config/app.js';
|
||||||
import Config from '../../../../models/config.js';
|
|
||||||
import app from '../../../../app.js';
|
import app from '../../../../app.js';
|
||||||
import infoMock from '../../../../../test/mocks/rest/api/v1/automatisch/info.js';
|
import infoMock from '../../../../../test/mocks/rest/api/v1/automatisch/info.js';
|
||||||
import * as license from '../../../../helpers/license.ee.js';
|
import * as license from '../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
describe('GET /api/v1/automatisch/info', () => {
|
describe('GET /api/v1/automatisch/info', () => {
|
||||||
it('should return Automatisch info', async () => {
|
it('should return Automatisch info', async () => {
|
||||||
vi.spyOn(Config, 'isInstallationCompleted').mockResolvedValue(true);
|
|
||||||
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.1',
|
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,20 +1,15 @@
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -24,22 +19,18 @@ export function createInstance(customConfig = {}, { requestInterceptor, response
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.proxy = false;
|
config.proxy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = axios.create(config);
|
const axiosWithProxyInstance = axios.create(config);
|
||||||
|
|
||||||
function shouldSkipProxy(hostname) {
|
function shouldSkipProxy(hostname) {
|
||||||
return noProxyHosts.some(noProxyHost => {
|
return noProxyHosts.some(noProxyHost => {
|
||||||
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
|
return hostname.endsWith(noProxyHost) || hostname === noProxyHost;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
axiosWithProxyInstance.interceptors.request.use(function skipProxyIfInNoProxy(requestConfig) {
|
||||||
* The interceptors are executed in the reverse order they are added.
|
const hostname = new URL(requestConfig.url).hostname;
|
||||||
*/
|
|
||||||
instance.interceptors.request.use(
|
|
||||||
function skipProxyIfInNoProxy(requestConfig) {
|
|
||||||
const hostname = new URL(requestConfig.baseURL).hostname;
|
|
||||||
|
|
||||||
if (supportsProxy && shouldSkipProxy(hostname)) {
|
if (supportsProxy && shouldSkipProxy(hostname)) {
|
||||||
requestConfig.httpAgent = undefined;
|
requestConfig.httpAgent = undefined;
|
||||||
@@ -47,56 +38,6 @@ export function createInstance(customConfig = {}, { requestInterceptor, response
|
|||||||
}
|
}
|
||||||
|
|
||||||
return requestConfig;
|
return requestConfig;
|
||||||
},
|
});
|
||||||
(error) => Promise.reject(error)
|
|
||||||
);
|
|
||||||
|
|
||||||
// not always we have custom request interceptors
|
export default axiosWithProxyInstance;
|
||||||
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(
|
|
||||||
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();
|
|
||||||
|
|
||||||
export default defaultInstance;
|
|
||||||
|
@@ -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,37 +20,18 @@ 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
|
|
||||||
try {
|
|
||||||
const parsedValue = JSON.parse(computedValue);
|
|
||||||
|
|
||||||
if (typeof parsedValue === 'number') {
|
|
||||||
throw new Error('Use original unparsed value.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
[key]: parsedValue,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
[key]: computedValue,
|
[key]: computedValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return {
|
return {
|
||||||
|
@@ -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,8 +1,44 @@
|
|||||||
|
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({
|
||||||
|
baseURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.interceptors.request.use((requestConfig) => {
|
||||||
|
const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig);
|
||||||
|
|
||||||
|
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
|
||||||
|
return beforeRequestFunc($, newConfig);
|
||||||
|
}, newRequestConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* axios seems to want InternalAxiosRequestConfig returned not AxioRequestConfig
|
||||||
|
* anymore even though requests do require AxiosRequestConfig.
|
||||||
|
*
|
||||||
|
* Since both interfaces are very similar (InternalAxiosRequestConfig
|
||||||
|
* extends AxiosRequestConfig), we can utilize an assertion below
|
||||||
|
**/
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error) => {
|
||||||
const { config, response } = error;
|
const { config, response } = error;
|
||||||
// Do not destructure `status` from `error.response` because it might not exist
|
// Do not destructure `status` from `error.response` because it might not exist
|
||||||
const status = response?.status;
|
const status = response?.status;
|
||||||
@@ -25,19 +61,8 @@ export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
||||||
};
|
};
|
||||||
|
@@ -1,11 +1,9 @@
|
|||||||
const infoMock = () => {
|
const infoMock = () => {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
docsUrl: 'https://automatisch.io/docs',
|
|
||||||
installationCompleted: true,
|
|
||||||
isCloud: false,
|
isCloud: false,
|
||||||
isEnterprise: true,
|
|
||||||
isMation: false,
|
isMation: false,
|
||||||
|
isEnterprise: true,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
@@ -23,7 +23,6 @@ const getCurrentUserMock = (currentUser, role, permissions) => {
|
|||||||
name: role.name,
|
name: role.name,
|
||||||
updatedAt: role.updatedAt.getTime(),
|
updatedAt: role.updatedAt.getTime(),
|
||||||
},
|
},
|
||||||
status: currentUser.status,
|
|
||||||
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
|
trialExpiryDate: currentUser.trialExpiryDate.toISOString(),
|
||||||
updatedAt: currentUser.updatedAt.getTime(),
|
updatedAt: currentUser.updatedAt.getTime(),
|
||||||
},
|
},
|
||||||
|
@@ -59,15 +59,6 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/carbone/connection' },
|
{ text: 'Connection', link: '/apps/carbone/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: 'Cryptography',
|
|
||||||
collapsible: true,
|
|
||||||
collapsed: true,
|
|
||||||
items: [
|
|
||||||
{ text: 'Actions', link: '/apps/cryptography/actions' },
|
|
||||||
{ text: 'Connection', link: '/apps/cryptography/connection' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: 'Datastore',
|
text: 'Datastore',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
@@ -131,6 +122,15 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/filter/connection' },
|
{ text: 'Connection', link: '/apps/filter/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Firefly III',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Triggers', link: '/apps/firefly-iii/triggers' },
|
||||||
|
{ text: 'Connection', link: '/apps/firefly-iii/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Flickr',
|
text: 'Flickr',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
@@ -14,7 +14,7 @@ connection in Automatisch. If any of the steps are outdated, please let us know!
|
|||||||
7. Click on the **Select all** and then click on the **Create** button.
|
7. Click on the **Select all** and then click on the **Create** button.
|
||||||
8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch.
|
8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch.
|
||||||
9. Write any screen name to be displayed in Automatisch.
|
9. Write any screen name to be displayed in Automatisch.
|
||||||
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatisch.
|
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatsich.
|
||||||
11. If you are using self-hosted Appwrite project, you can paste the instance url into **Appwrite instance URL** field in Automatisch.
|
11. If you are using self-hosted Appwrite project, you can paste the instace url into **Appwrite instance URL** field in Automatisch.
|
||||||
12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL.
|
12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL.
|
||||||
13. Start using Appwrite integration with Automatisch!
|
13. Start using Appwrite integration with Automatisch!
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
favicon: /favicons/appwrite.svg
|
favicon: /favicons/appwrite.svg
|
||||||
items:
|
items:
|
||||||
- name: New documents
|
- name: New documets
|
||||||
desc: Triggers when a new document is created.
|
desc: Triggers when a new document is created.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
favicon: /favicons/cryptography.svg
|
|
||||||
items:
|
|
||||||
- name: Create HMAC
|
|
||||||
desc: Create a Hash-based Message Authentication Code (HMAC) using the specified algorithm, secret key, and message.
|
|
||||||
- name: Create Signature
|
|
||||||
desc: Create a digital signature using the specified algorithm, secret key, and message.
|
|
||||||
---
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import CustomListing from '../../components/CustomListing.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<CustomListing />
|
|
@@ -1,3 +0,0 @@
|
|||||||
# Cryptography
|
|
||||||
|
|
||||||
Cryptography is a built-in app shipped with Automatisch, allowing you to perform cryptographic operations without needing to connect to any external services.
|
|
17
packages/docs/pages/apps/firefly-iii/connection.md
Normal file
17
packages/docs/pages/apps/firefly-iii/connection.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Firefly III
|
||||||
|
|
||||||
|
:::info
|
||||||
|
This page explains the steps you need to follow to set up the Firefly III
|
||||||
|
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||||
|
:::
|
||||||
|
|
||||||
|
1. Go to your dashboard.
|
||||||
|
2. Click on the **Options** tab on the left, and click on the **Profile** button.
|
||||||
|
3. Go to **Oauth** tab and click on the **Create New Client** button.
|
||||||
|
4. Enter a name for your project.
|
||||||
|
5. Copy **OAuth Redirect URL** from Automatisch to **Redirect URL** field, and click on the **Create** button.
|
||||||
|
6. Copy the **Client ID** value from the following popup to the `Client ID` field on Automatisch.
|
||||||
|
7. Copy the **Secret** value from the following popup to the `Client Secret` field on Automatisch.
|
||||||
|
8. Fill **Instance URL** with your Firefly III instance url.
|
||||||
|
9. Click **Submit** button on Automatisch.
|
||||||
|
10. Congrats! Start using your new Firefly III connection within the flows.
|
12
packages/docs/pages/apps/firefly-iii/triggers.md
Normal file
12
packages/docs/pages/apps/firefly-iii/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/firefly-iii.svg
|
||||||
|
items:
|
||||||
|
- name: Transaction created
|
||||||
|
desc: Triggers when a new transaction is created.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
@@ -5,8 +5,6 @@ items:
|
|||||||
desc: Creates an attachment of a specified object by given parent ID.
|
desc: Creates an attachment of a specified object by given parent ID.
|
||||||
- name: Find record
|
- name: Find record
|
||||||
desc: Finds a record of a specified object by a field and value.
|
desc: Finds a record of a specified object by a field and value.
|
||||||
- name: Find partially matching record
|
|
||||||
desc: Finds a record of a specified object by a field containing a value.
|
|
||||||
- name: Execute query
|
- name: Execute query
|
||||||
desc: Executes a SOQL query in Salesforce.
|
desc: Executes a SOQL query in Salesforce.
|
||||||
---
|
---
|
||||||
|
@@ -6,12 +6,16 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
|
|||||||
.
|
.
|
||||||
├── packages
|
├── packages
|
||||||
│ ├── backend
|
│ ├── backend
|
||||||
|
│ ├── cli
|
||||||
│ ├── docs
|
│ ├── docs
|
||||||
│ ├── e2e-tests
|
│ ├── e2e-tests
|
||||||
|
│ ├── types
|
||||||
│ └── web
|
│ └── web
|
||||||
```
|
```
|
||||||
|
|
||||||
- `backend` - The backend package contains the backend application and all integrations.
|
- `backend` - The backend package contains the backend application and all integrations.
|
||||||
|
- `cli` - The cli package contains the CLI application of Automatisch.
|
||||||
- `docs` - The docs package contains the documentation website.
|
- `docs` - The docs package contains the documentation website.
|
||||||
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
||||||
|
- `types` - The types package contains the shared types for both the backend and web packages.
|
||||||
- `web` - The web package contains the frontend application of Automatisch.
|
- `web` - The web package contains the frontend application of Automatisch.
|
||||||
|
@@ -12,6 +12,7 @@ The following integrations are currently supported by Automatisch.
|
|||||||
- [Disqus](/apps/disqus/triggers)
|
- [Disqus](/apps/disqus/triggers)
|
||||||
- [Dropbox](/apps/dropbox/actions)
|
- [Dropbox](/apps/dropbox/actions)
|
||||||
- [Filter](/apps/filter/actions)
|
- [Filter](/apps/filter/actions)
|
||||||
|
- [Firefly III](/apps/firefly-iii/triggers)
|
||||||
- [Flickr](/apps/flickr/triggers)
|
- [Flickr](/apps/flickr/triggers)
|
||||||
- [Formatter](/apps/formatter/actions)
|
- [Formatter](/apps/formatter/actions)
|
||||||
- [Ghost](/apps/ghost/triggers)
|
- [Ghost](/apps/ghost/triggers)
|
||||||
|
@@ -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 |
21
packages/docs/pages/public/favicons/firefly-iii.svg
Normal file
21
packages/docs/pages/public/favicons/firefly-iii.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
- maskable-icon.svg
|
||||||
|
- Copyright (c) 2022 james@firefly-iii.org
|
||||||
|
-
|
||||||
|
- This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as
|
||||||
|
- published by the Free Software Foundation, either version 3 of the
|
||||||
|
- License, or (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<svg height="377.95276" width="377.95276" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h377.95276v377.95276h-377.95276z" fill="#cd5029" stroke-width="1.96129"/><g transform="matrix(.77452773 0 0 .77452773 21.636074 21.374655)"><path d="m140.49013 78.646381 2.249 53.017999s-40.103 29.566-45.538 68l-16.001 1.231s-11.539 2.564-11.539 14.103v37.18s3.846 11.538 12.82 11.538l16.487-.319s8 30.5 36.5 50.5v25.5s-2 8.5 15.5 11 40.75 2.25 44.5-1.5 3.75-4.5 3.75-9c0 0 21.25 5 60.25 0v5s3.5 7 29 7 33-3 37.5-12v-25s37.009-36.264 35.75-91.75c-1.083-47.75-15.901-64.299-35.806-82.96-22.67-21.254-69.944-31.165-117.944-25.353.001-.001-24.341-43.937999-67.478-36.187999z" fill="#fff"/><circle cx="135.46912" cy="214.39638" fill="#cd5029" r="9.5"/><path d="m360.08113 190.51238s-18.218-8.742-40.662 3.996c0 0-26.711-8.987-40.99 2.593-14.828 12.025-16.299 26.115-15.525 42.785 0 0 12.837-43.915 45.252-32.571 0 0-22.947 40.43 12.761 47.508 0 0 8.436-.05 15.401-4.256 6.644-4.011 11.842-11.433 9.711-24.814 0 0-4.348-13.336-15.569-21.42 0 0 11.042-7.806 31.988-2.209z" fill="#cd5029"/><path d="m320.19013 213.01938s-16.689 31.461 5.607 29.767c0 0 11.838-5.656 4.887-17.127-7.147-11.796-10.494-12.64-10.494-12.64z" fill="#fff"/></g><path d="m188.97638 175.70052s4.01698 13.60604-3.69586 21.52748c-7.713 7.92145-6.8792 16.6767-3.75227 20.84588 3.12692 4.16917 2.91831 7.29593.41674 9.58905-2.50141 2.29312-4.58608 3.96073-6.04523.20846-1.45916-3.75228-3.12676-3.75228-3.75228-5.62834-.62552-1.87605-1.87622-5.21142-1.87622-5.21142s-3.96072 6.25384-6.46229 10.00611c-2.50157 3.75228-2.50141 9.58922-.83381 12.71598 1.66761 3.12676 1.04226 6.87903-.20845 12.09046-1.2507 5.21143.4169 13.13288 6.25369 16.2598 5.83678 3.12692 12.92459 5.62833 16.05135 8.5468s10.42301 5.62833 19.80362 3.54382c9.3806-2.0845 21.26294-11.67355 23.34744-18.13585 0 0 5.41988-6.04523 4.37763-13.96668s-4.79469-7.71316-6.4623-13.75839c-1.6676-6.04523 3.60854-4.55469-.8338-14.93382 0 0-1.98012-4.94005-9.50352-8.49899-4.83404-2.28661-1.54469-12.63061-10.09149-23.05347s-16.73295-12.14688-16.73295-12.14688z" fill="#ffa284" stroke-width=".162598"/></svg>
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1,5 +0,0 @@
|
|||||||
POSTGRES_DB=automatisch
|
|
||||||
POSTGRES_USER=automatisch_user
|
|
||||||
POSTGRES_PASSWORD=automatisch_password
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
POSTGRES_HOST=localhost
|
|
@@ -1,6 +0,0 @@
|
|||||||
node_modules
|
|
||||||
build
|
|
||||||
|
|
||||||
.eslintrc.js
|
|
||||||
|
|
||||||
playwright-report/*
|
|
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"semi": [
|
|
||||||
2,
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@@ -44,14 +44,6 @@ and it should install the associated browsers for the test running. For more inf
|
|||||||
|
|
||||||
We recommend using [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) maintained by Microsoft. This lets you run playwright tests from within the code editor, giving you access to additional tools, such as easily running subsets of tests.
|
We recommend using [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) maintained by Microsoft. This lets you run playwright tests from within the code editor, giving you access to additional tools, such as easily running subsets of tests.
|
||||||
|
|
||||||
[Global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown) are part of the tests.
|
|
||||||
|
|
||||||
By running `yarn test` setup and teardown actions will take place.
|
|
||||||
|
|
||||||
If you need to setup Admin account (if you didn't seed the DB with the admin account or have clean DB) you should run `auth.setup.js` file.
|
|
||||||
|
|
||||||
If you want to clean the database (drop tables) and perform required migrations run `global.teardown.js`.
|
|
||||||
|
|
||||||
# Test failures
|
# Test failures
|
||||||
|
|
||||||
If there are failing tests in the test suite, this can be caused by a myriad of reasons, but one of the best places to start is either running the test in a headed browser, looking at the associated trace file for the failed test, or checking out the output of a failed GitHub Action.
|
If there are failing tests in the test suite, this can be caused by a myriad of reasons, but one of the best places to start is either running the test in a headed browser, looking at the associated trace file for the failed test, or checking out the output of a failed GitHub Action.
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
const { expect } = require('@playwright/test');
|
|
||||||
const { BasePage } = require('./base-page');
|
|
||||||
|
|
||||||
export class AcceptInvitation extends BasePage {
|
|
||||||
path = '/accept-invitation';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
constructor(page) {
|
|
||||||
super(page);
|
|
||||||
|
|
||||||
this.page = page;
|
|
||||||
this.passwordTextField = this.page.getByTestId('password-text-field');
|
|
||||||
this.passwordConfirmationTextField = this.page.getByTestId('confirm-password-text-field');
|
|
||||||
this.submitButton = this.page.getByTestId('submit-button');
|
|
||||||
this.pageTitle = this.page.getByTestId('accept-invitation-form-title');
|
|
||||||
this.formErrorMessage = this.page.getByTestId('accept-invitation-form-error');
|
|
||||||
}
|
|
||||||
|
|
||||||
async open(token) {
|
|
||||||
return await this.page.goto(`${this.path}?token=${token}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async acceptInvitation(
|
|
||||||
password
|
|
||||||
) {
|
|
||||||
await this.passwordTextField.fill(password);
|
|
||||||
await this.passwordConfirmationTextField.fill(password);
|
|
||||||
|
|
||||||
await this.submitButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fillPasswordField(password) {
|
|
||||||
await this.passwordTextField.fill(password);
|
|
||||||
await this.passwordConfirmationTextField.fill(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
async excpectSubmitButtonToBeDisabled() {
|
|
||||||
await expect(this.submitButton).toBeDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
async expectAlertToBeVisible() {
|
|
||||||
await expect(this.formErrorMessage).toBeVisible();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,75 +0,0 @@
|
|||||||
import { BasePage } from "./base-page";
|
|
||||||
const { faker } = require('@faker-js/faker');
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
export class AdminSetupPage extends BasePage {
|
|
||||||
path = '/installation';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
constructor(page) {
|
|
||||||
super(page);
|
|
||||||
|
|
||||||
this.fullNameTextField = this.page.getByTestId('fullName-text-field');
|
|
||||||
this.emailTextField = this.page.getByTestId('email-text-field');
|
|
||||||
this.passwordTextField = this.page.getByTestId('password-text-field');
|
|
||||||
this.repeatPasswordTextField = this.page.getByTestId('repeat-password-text-field');
|
|
||||||
this.createAdminButton = this.page.getByTestId('signUp-button');
|
|
||||||
this.invalidFields = this.page.locator('p.Mui-error');
|
|
||||||
this.successAlert = this.page.getByTestId('success-alert');
|
|
||||||
}
|
|
||||||
|
|
||||||
async open() {
|
|
||||||
return await this.page.goto(this.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fillValidUserData() {
|
|
||||||
await this.fullNameTextField.fill(process.env.LOGIN_EMAIL);
|
|
||||||
await this.emailTextField.fill(process.env.LOGIN_EMAIL);
|
|
||||||
await this.passwordTextField.fill(process.env.LOGIN_PASSWORD);
|
|
||||||
await this.repeatPasswordTextField.fill(process.env.LOGIN_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fillInvalidUserData() {
|
|
||||||
await this.fullNameTextField.fill('');
|
|
||||||
await this.emailTextField.fill('abcde');
|
|
||||||
await this.passwordTextField.fill('');
|
|
||||||
await this.repeatPasswordTextField.fill('a');
|
|
||||||
}
|
|
||||||
|
|
||||||
async fillNotMatchingPasswordUserData() {
|
|
||||||
const testUser = this.generateUser();
|
|
||||||
await this.fullNameTextField.fill(testUser.fullName);
|
|
||||||
await this.emailTextField.fill(testUser.email);
|
|
||||||
await this.passwordTextField.fill(testUser.password);
|
|
||||||
await this.repeatPasswordTextField.fill(testUser.wronglyRepeatedPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
async submitAdminForm() {
|
|
||||||
await this.createAdminButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
async expectInvalidFields(errorCount) {
|
|
||||||
await expect(await this.invalidFields.all()).toHaveLength(errorCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
async expectSuccessAlertToBeVisible() {
|
|
||||||
await expect(await this.successAlert).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
async expectSuccessMessageToContainLoginLink() {
|
|
||||||
await expect(await this.successAlert.locator('a')).toHaveAttribute('href', '/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
generateUser() {
|
|
||||||
faker.seed(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER));
|
|
||||||
|
|
||||||
return {
|
|
||||||
fullName: faker.person.fullName(),
|
|
||||||
email: faker.internet.email(),
|
|
||||||
password: faker.internet.password(),
|
|
||||||
wronglyRepeatedPassword: faker.internet.password()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@@ -11,11 +11,10 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
|||||||
super(page);
|
super(page);
|
||||||
this.fullNameInput = page.getByTestId('full-name-input');
|
this.fullNameInput = page.getByTestId('full-name-input');
|
||||||
this.emailInput = page.getByTestId('email-input');
|
this.emailInput = page.getByTestId('email-input');
|
||||||
|
this.passwordInput = page.getByTestId('password-input');
|
||||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
this.roleInput = page.getByTestId('role.id-autocomplete');
|
||||||
this.createButton = page.getByTestId('create-button');
|
this.createButton = page.getByTestId('create-button');
|
||||||
this.pageTitle = page.getByTestId('create-user-title');
|
this.pageTitle = page.getByTestId('create-user-title');
|
||||||
this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert');
|
|
||||||
this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
seed(seed) {
|
seed(seed) {
|
||||||
@@ -26,6 +25,7 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
|||||||
return {
|
return {
|
||||||
fullName: faker.person.fullName(),
|
fullName: faker.person.fullName(),
|
||||||
email: faker.internet.email().toLowerCase(),
|
email: faker.internet.email().toLowerCase(),
|
||||||
|
password: faker.internet.password(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,6 @@ export class DeleteUserModal {
|
|||||||
async close () {
|
async close () {
|
||||||
await this.page.click('body', {
|
await this.page.click('body', {
|
||||||
position: { x: 10, y: 10 }
|
position: { x: 10, y: 10 }
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
const { AdminCreateRolePage } = require('./create-role-page');
|
const { AdminCreateRolePage } = require('./create-role-page')
|
||||||
|
|
||||||
export class AdminEditRolePage extends AdminCreateRolePage {
|
export class AdminEditRolePage extends AdminCreateRolePage {
|
||||||
constructor (page) {
|
constructor (page) {
|
||||||
|
@@ -23,7 +23,6 @@ export class AdminEditUserPage extends AuthenticatedPage {
|
|||||||
*/
|
*/
|
||||||
async waitForLoad(fullName) {
|
async waitForLoad(fullName) {
|
||||||
return await this.page.waitForFunction((fullName) => {
|
return await this.page.waitForFunction((fullName) => {
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const el = document.querySelector("[data-test='full-name-input']");
|
const el = document.querySelector("[data-test='full-name-input']");
|
||||||
return el && el.value === fullName;
|
return el && el.value === fullName;
|
||||||
}, fullName);
|
}, fullName);
|
||||||
|
@@ -25,5 +25,5 @@ export const adminFixtures = {
|
|||||||
adminCreateRolePage: async ({ page}, use) => {
|
adminCreateRolePage: async ({ page}, use) => {
|
||||||
await use(new AdminCreateRolePage(page));
|
await use(new AdminCreateRolePage(page));
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
|
@@ -87,7 +87,6 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
await this.firstPageButton.click();
|
await this.firstPageButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (await this.usersLoader.isVisible()) {
|
if (await this.usersLoader.isVisible()) {
|
||||||
await this.usersLoader.waitFor({
|
await this.usersLoader.waitFor({
|
||||||
@@ -109,7 +108,6 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
|
|
||||||
async getTotalRows() {
|
async getTotalRows() {
|
||||||
return await this.page.evaluate(() => {
|
return await this.page.evaluate(() => {
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const node = document.querySelector('[data-total-count]');
|
const node = document.querySelector('[data-total-count]');
|
||||||
if (node) {
|
if (node) {
|
||||||
const count = Number(node.dataset.totalCount);
|
const count = Number(node.dataset.totalCount);
|
||||||
@@ -123,7 +121,6 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
|
|
||||||
async getRowsPerPage() {
|
async getRowsPerPage() {
|
||||||
return await this.page.evaluate(() => {
|
return await this.page.evaluate(() => {
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const node = document.querySelector('[data-rows-per-page]');
|
const node = document.querySelector('[data-rows-per-page]');
|
||||||
if (node) {
|
if (node) {
|
||||||
const count = Number(node.dataset.rowsPerPage);
|
const count = Number(node.dataset.rowsPerPage);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user