Compare commits

..

119 Commits

Author SHA1 Message Date
Ali BARIN
6bafdf5257 refactor(http-client): inherit interceptors from parent instance 2024-07-25 07:52:27 +00:00
Ali BARIN
ade6282013 feat(formatter/text): add regex support in replace transfomer 2024-07-25 07:52:21 +00:00
Ali BARIN
bdd8da98c4 Merge pull request #1985 from automatisch/aut-1061
feat: use dynamic DOCS URL
2024-07-24 14:20:03 +02:00
Ali BARIN
730fdd32b1 feat: use dynamic DOCS URL 2024-07-23 09:48:20 +00:00
Ali BARIN
aa7f6694fc Merge pull request #1974 from kuba618/AUT-1115
test: add admin account setup tests
2024-07-22 17:03:55 +02:00
Jakub P.
8fba6704df Merge remote-tracking branch 'upstream/main' into AUT-1115 2024-07-22 16:32:09 +02:00
Jakub P.
37d02eba02 test: add teardown dependency to clean db after tests 2024-07-22 15:42:21 +02:00
Ali BARIN
75a87cf070 Merge pull request #1965 from automatisch/add-cryptography-app
feat: add cryptography app with createHmac and createSignature actions
2024-07-22 14:22:14 +02:00
Ömer Faruk Aydın
8acdc5853d Merge pull request #1978 from automatisch/AUT-1105
feat: refactor delete user mutation with the REST API endpoint
2024-07-22 14:11:35 +02:00
kasia.oczkowska
1aa1f441b3 feat: refactor delete user mutation with the REST API endpoint 2024-07-22 13:27:57 +02:00
Ömer Faruk Aydın
0e26032ac3 Merge pull request #1977 from automatisch/add-create-uuid-text-formatter
feat(formatter/text): add create uuid action
2024-07-22 13:24:14 +02:00
Ömer Faruk Aydın
871f25c6d9 Merge pull request #1975 from automatisch/add-get-current-timestamp
feat(formatter/date-time): add get current timestamp action
2024-07-22 13:23:06 +02:00
Ömer Faruk Aydın
d8199e7ba7 Merge pull request #1983 from automatisch/strict-installation-redirection
fix: safeguard redirect to /installation
2024-07-22 13:20:25 +02:00
Ali BARIN
52b938eabe Merge pull request #1980 from kuba618/AUT-1119
test: add removed user invitation test
2024-07-19 11:42:29 +02:00
Ali BARIN
94fddf3d9b Merge pull request #1979 from kuba618/AUT-1118
chore: add linter to tests
2024-07-19 11:41:34 +02:00
Ali BARIN
02c98c1ece Merge pull request #1982 from automatisch/remove-deleted-connection
fix(AppConnectionRow): invalidate app connections upon deletion
2024-07-19 11:38:31 +02:00
Ali BARIN
6ba77667e9 Merge pull request #1981 from automatisch/AUT-1103
feat: use create access token api endpoint instead of login mutation
2024-07-19 11:38:17 +02:00
Ali BARIN
e201a5b806 fix: safeguard redirect to /installation 2024-07-19 09:31:58 +00:00
Ali BARIN
adac68c407 fix(AppConnectionRow): invalidate app connections upon deletion 2024-07-19 08:43:49 +00:00
kasia.oczkowska
d051275e54 feat: use create access token api endpoint instead of login mutation 2024-07-18 13:05:59 +01:00
Jakub P.
2afc00364a test: add removed user invitation test 2024-07-18 12:40:48 +02:00
Jakub P.
0a1461231b chore: add linter to tests 2024-07-18 12:39:04 +02:00
Jakub P.
129327f40d test: add admin account setup tests 2024-07-18 12:29:35 +02:00
Ali BARIN
58819aad94 feat(cryptography/actions): add output encoding field 2024-07-17 18:44:00 +02:00
Ali BARIN
949a2543f5 feat(cryptography): add create signature action 2024-07-17 18:44:00 +02:00
Ali BARIN
0f9d732667 feat: add cryptography app with createHmac action 2024-07-17 18:44:00 +02:00
Ali BARIN
b7df175950 feat(formatter/text): add create uuid action 2024-07-17 16:07:23 +00:00
Ali BARIN
4f7ce9874f feat(formatter/date-time): add get current timestamp action 2024-07-17 15:56:26 +00:00
Ömer Faruk Aydın
f63cc80383 Merge pull request #1973 from automatisch/AUT-1109
feat: refactor reset password mutation with the REST API endpoint
2024-07-17 17:24:55 +02:00
kasia.oczkowska
dd0a1328e8 feat: refactor reset password mutation with the REST API endpoint 2024-07-17 15:05:46 +01:00
Ali BARIN
27610c002c Merge pull request #1972 from automatisch/AUT-1107
feat: refactor forgot password mutation with the REST API endpoint
2024-07-17 15:32:37 +02:00
kasia.oczkowska
46ec9b5229 feat: refactor forgot password mutation with the REST API endpoint 2024-07-17 13:23:45 +01:00
Ali BARIN
778559d537 Merge pull request #1971 from automatisch/fix-email-worker
fix(email): correct sandbox safeguard check
2024-07-17 13:53:12 +02:00
Ali BARIN
1e64b8a903 fix(email): correct sandbox safeguard check 2024-07-17 10:25:01 +02:00
Ali BARIN
0e000693d6 Merge pull request #1970 from automatisch/rest-reset-password
feat: Implement reset password rest API endpoint
2024-07-16 16:45:20 +02:00
Faruk AYDIN
cf9e09ea7a feat: Implement reset password rest API endpoint 2024-07-16 16:22:10 +02:00
Ali BARIN
eac2f729a5 Merge pull request #1969 from automatisch/fix-migrations-rollback
fix: Remove step constraint down migration
2024-07-16 16:04:07 +02:00
Faruk AYDIN
9578cd27dd fix: Remove step contraint down migration 2024-07-16 15:52:24 +02:00
Ali BARIN
9304ffffc9 Merge pull request #1968 from automatisch/rest-forgot-password
feat: Implement forgot password rest API endpoint
2024-07-16 15:52:07 +02:00
Faruk AYDIN
3fd628cb05 feat: Implement forgot password rest API endpoint 2024-07-16 15:43:15 +02:00
Ömer Faruk Aydın
fe68b70bd9 Merge pull request #1967 from automatisch/rest-delete-user
feat: Implement rest API endpoint to delete user
2024-07-16 15:42:01 +02:00
Faruk AYDIN
ec2863d218 feat: Implement rest API endpoint to delete user 2024-07-16 15:18:01 +02:00
Ali BARIN
dc56e7f883 Merge pull request #1966 from kuba618/AUT-1099
test: add accept invitation invalid token tests
2024-07-16 13:53:32 +02:00
Jakub P.
2f1f537e00 test: add accept invitation invalid token tests 2024-07-16 12:08:50 +02:00
Ali BARIN
b7b3a3025b Merge pull request #1964 from automatisch/AUT-1095
feat: create onboarding UX flow
2024-07-15 16:11:25 +02:00
Ali BARIN
edb9526538 feat(InstallationForm): rephrase submit button 2024-07-15 12:24:35 +00:00
Ali BARIN
62117ece06 Merge pull request #1939 from automatisch/aut-1035
feat(salesforce): add find partially matching record action
2024-07-15 14:01:21 +02:00
Ali BARIN
913a2a0ac1 Merge pull request #1953 from automatisch/add-security-icon-in-additional-drawer
feat(Layout): support security icon in additional drawer link
2024-07-15 14:01:10 +02:00
Ali BARIN
92ec3d07a3 feat(salesforce/find-partially-matching-record): sanitize user inputs 2024-07-15 09:04:16 +00:00
Ali BARIN
67ee7899fd feat(salesforce): add find partially matching record action 2024-07-15 09:04:16 +00:00
Ali BARIN
b0ceb3fe7e feat(InstallationForm): use thinner font weight in success alert 2024-07-15 09:00:13 +00:00
kasia.oczkowska
ba6b4c6854 feat: create onboarding UX flow 2024-07-12 14:55:30 +01:00
Ali BARIN
61e90aed60 chore: upgrade @playwright/test with 1.45.1 2024-07-12 13:40:51 +00:00
Ali BARIN
45b7a399f2 feat(Layout): support security icon in additional drawer link 2024-07-12 13:40:22 +00:00
Ali BARIN
673ed25598 Merge pull request #1963 from automatisch/add-parse-stringified-json-action
feat(formatter): add parse stringified json action
2024-07-12 12:44:49 +02:00
Ali BARIN
14fc460174 Merge pull request #1962 from automatisch/missing-power-input-label
fix: bring back the label for PowerInput
2024-07-12 12:38:54 +02:00
Ali BARIN
211b4537f9 Merge pull request #1961 from automatisch/fix-undefined-datastore-entry-throwing
fix(datastore): stop undefined datastore entry throwing
2024-07-12 12:37:53 +02:00
Ali BARIN
a683e059aa feat(formatter): add parse stringified json action 2024-07-12 10:37:16 +00:00
kasia.oczkowska
d2d2cea567 fix: return Item in renderItemFactory 2024-07-12 11:31:43 +01:00
kasia.oczkowska
e5bda65a35 fix: bring back the label for PowerInput 2024-07-12 11:27:52 +01:00
Ali BARIN
6aaef9df4b fix(datastore): stop undefined datastore entry throwing 2024-07-12 08:58:39 +00:00
Ali BARIN
7fba4d72b0 Merge pull request #1958 from automatisch/AUT-1091
fix: introduce fix for logo being stretched
2024-07-11 15:35:48 +02:00
Ali BARIN
a6dc2ed4ba Merge pull request #1956 from automatisch/AUT-1007
feat: limit vertical scroll to a reasonable boundary
2024-07-11 15:34:04 +02:00
Ali BARIN
20a40eb2f6 Merge pull request #1959 from automatisch/AUT-1088
fix: introduce fix for undefined id on dialog close when adding a connection
2024-07-11 13:23:59 +02:00
kasia.oczkowska
ec4ac9d075 fix: introduce fix for undefined id on dialog close when adding a connection 2024-07-11 12:15:51 +01:00
kasia.oczkowska
b2c14f4226 fix: introduce fix for logo being stretched 2024-07-11 11:28:45 +01:00
Ömer Faruk Aydın
099dfbd0b0 Merge pull request #1954 from automatisch/user-invitation
feat: Implement user invitation functionality
2024-07-11 12:18:20 +02:00
Ali BARIN
a9f5736c12 test: cover accept invitation flow 2024-07-11 11:38:23 +02:00
Ali BARIN
2bd4dd3ab0 test(e2e-tests): cover password field removal 2024-07-11 11:38:23 +02:00
Ali BARIN
186850c256 test: cover user status in user serializer 2024-07-11 11:38:23 +02:00
Ali BARIN
9922033d33 feat: add accept-invitation page 2024-07-11 11:38:23 +02:00
Faruk AYDIN
3c3e6e4144 feat: Implement user invitation backend functionality 2024-07-11 11:38:23 +02:00
Faruk AYDIN
0e4ac3b7f3 feat: Add status column to user model 2024-07-11 11:38:23 +02:00
Ali BARIN
c9813e0316 Merge pull request #1950 from automatisch/AUT-1074
fix: fix error for only Create role when publishing flow
2024-07-11 11:02:08 +02:00
Ali BARIN
577c0cfab9 Merge pull request #1957 from automatisch/AUT-677
feat: add missing propTypes
2024-07-11 10:54:17 +02:00
kasia.oczkowska
0966f9d715 feat: disable publish button when no flow or permissions 2024-07-11 09:39:31 +01:00
kasia.oczkowska
3f5df118a0 feat: add missing propTypes 2024-07-11 08:54:32 +01:00
kasia.oczkowska
e05c1b26f1 feat: limit vertical scroll to a reasonable boundary 2024-07-10 14:52:07 +01:00
Ömer Faruk Aydın
725b38c697 Merge pull request #1955 from automatisch/repository-structure
docs: Adjust repository to the new packages structure
2024-07-09 13:14:27 +02:00
Faruk AYDIN
402a0fdf3b docs: Adjust repository to the new packages structure 2024-07-08 21:11:25 +02:00
Ali BARIN
078364ffa1 Merge pull request #1951 from automatisch/AUT-1069
fix: set default locale for luxon's  DateTime
2024-07-08 15:19:14 +02:00
kasia.oczkowska
f64d5ec4fc fix: set default locale for luxon's DateTime 2024-07-04 15:14:28 +01:00
kasia.oczkowska
0666174501 fix: fix error for only Create role when publishing flow 2024-07-04 14:08:35 +01:00
Ali BARIN
12194a50e1 Merge pull request #1948 from automatisch/AUT-1076
feat: block access to flow creation for users without permissions
2024-07-04 10:44:15 +02:00
kasia.oczkowska
82ee592699 feat: block access to flow creation for users without permissions 2024-07-04 08:57:03 +01:00
Ali BARIN
1b4fb2ce6e Merge pull request #1947 from automatisch/AUT-1070
feat: use try-catch block for updating flow status
2024-07-03 15:37:02 +02:00
kasia.oczkowska
ebea8d12d1 feat: use try-catch block for updating flow status 2024-07-03 14:04:11 +01:00
Ali BARIN
f842dd77df Merge pull request #1945 from automatisch/remove-access-tokens-in-perm-user-deletion
fix: delete access token before hard deleting user
2024-07-03 13:16:20 +02:00
Ali BARIN
a6ec7a6c99 Merge pull request #1944 from automatisch/fix-reset-password
fix(user): make resetPasswordToken relevant fields nullable
2024-07-03 13:16:12 +02:00
Ali BARIN
369c72282c fix: delete access token before hard deleting user 2024-07-01 11:41:11 +00:00
Ali BARIN
6f30c1a509 fix(user): make resetPasswordToken relevant fields nullable 2024-07-01 10:26:17 +00:00
Ali BARIN
abfd1116c7 Merge pull request #1941 from automatisch/AUT-1038
fix: persist value in ControlledCustomAutocomplete when it depends on other fields
2024-06-21 14:32:09 +02:00
kasia.oczkowska
017854955d fix: persist value in ControlledCustomAutocomplete when it depends on other fields 2024-06-21 10:37:39 +01:00
Ali BARIN
1405cddea1 Merge pull request #1940 from automatisch/AUT-1039
Revert "feat: persist parameters values in FlowSubstep (#1505)"
2024-06-20 15:57:47 +02:00
kasia.oczkowska
00dd3164c9 Revert "feat: persist parameters values in FlowSubstep (#1505)"
This reverts commit 017a881494.
2024-06-20 14:48:51 +01:00
Ali BARIN
d5cbc0f611 Merge pull request #1578 from automatisch/AUT-620
feat: improve UI display depending on user permissions
2024-06-20 12:04:31 +02:00
kasia.oczkowska
5d2e9ccc67 feat: improve UI display depending on user persmissions 2024-06-20 09:52:00 +00:00
kattoczko
017a881494 feat: persist parameters values in FlowSubstep (#1505)
* feat: persist parameters values in FlowSubstep

* feat: add missing import

* feat: use usePrevious hook
2024-06-20 11:13:45 +02:00
Alex Maslakov
52994970e6 fix(DynamicField): display long variables better
* Fix issue #1933

Problem with rendering in DynamicField component with long variables #1933

* Fix issue #1933 (2)

* Fix issue #1933 (3)
2024-06-20 09:55:08 +02:00
Ali BARIN
ebae629e5c Merge pull request #1931 from automatisch/AUT-1010
feat: improve nodes and edges state update
2024-06-19 16:01:24 +02:00
kasia.oczkowska
4d79220b0c refactor: fix spelling and wording errors 2024-06-14 12:39:42 +01:00
kasia.oczkowska
96fba7fbb8 feat: improve nodes and edges state update 2024-06-14 12:39:42 +01:00
Ali BARIN
e0d610071d Merge pull request #1917 from automatisch/AUT-1009
feat: hide react-flow attribution
2024-06-05 15:35:09 +02:00
kasia.oczkowska
ab0966c005 feat: hide react-flow attribution 2024-06-05 13:39:49 +01:00
Ali BARIN
751eb41e72 Merge pull request #1817 from automatisch/new-editor-feature-flag
feat: introduce feature flag for new flow editor
2024-06-04 12:45:00 +02:00
kasia.oczkowska
f08dc25711 feat: introduce style and behavior improvements 2024-06-04 07:18:18 +00:00
kasia.oczkowska
737eb31776 feat: introduce custom edges, auto layout improvements and node data updates 2024-06-04 07:17:38 +00:00
kasia.oczkowska
d6abf283bc feat: introduce automatic layout for new flow editor 2024-06-04 07:17:38 +00:00
kasia.oczkowska
bac4ab5aa4 feat: introduce feature flag for new flow editor 2024-06-04 07:17:38 +00:00
Ali BARIN
b5839390fd Merge pull request #1911 from automatisch/render-yaml-fix
fix(render.yaml): correct docker contexts
2024-06-03 11:21:31 +02:00
Ali BARIN
d19271dae1 fix(render.yaml): correct docker contexts 2024-06-03 10:23:28 +02:00
Ali BARIN
ef5a09314e Merge pull request #1907 from automatisch/fix-admin-apps-undefined-error
fix(AdminApplicationSettings): handle undefined appConfig object
2024-05-31 13:52:13 +02:00
Rıdvan Akca
ba52e298eb fix(AdminApplicationSettings): handle undefined appConfig object 2024-05-31 13:26:06 +02:00
Ali BARIN
b3c3998189 Merge pull request #1895 from automatisch/fix-appkey-error-in-flowrow
fix: remove unnecessary appKey in FlowRow and FlowContextMenu
2024-05-31 12:54:13 +02:00
Ömer Faruk Aydın
782f9b5c04 Merge pull request #1900 from automatisch/release/v0.12.0
release(v0.12.0): Update version to 0.12.0
2024-05-28 12:05:52 +02:00
Faruk AYDIN
3079d8c605 Update version to 0.12.0 2024-05-28 11:13:54 +02:00
Rıdvan Akca
c5202d7b3e fix: remove unnecessary appKey in FlowRow and FlowContextMenu 2024-05-24 14:11:22 +02:00
Ali BARIN
fbae83f4de Merge pull request #1874 from automatisch/make-value-column-text-in-datastore
fix(datastore): make value column text
2024-05-23 10:13:47 +02:00
Ali BARIN
1dc9646894 fix(datastore): make value column text 2024-05-09 20:31:47 +00:00
233 changed files with 4247 additions and 800 deletions

View File

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

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 430 390" fill="#1185fd" aria-hidden="true">
<path d="M180 141.964C163.699 110.262 119.308 51.1817 78.0347 22.044C38.4971 -5.86834 23.414 -1.03207 13.526 3.43594C2.08093 8.60755 0 26.1785 0 36.5164C0 46.8542 5.66748 121.272 9.36416 133.694C21.5786 174.738 65.0603 188.607 105.104 184.156C107.151 183.852 109.227 183.572 111.329 183.312C109.267 183.642 107.19 183.924 105.104 184.156C46.4204 192.847 -5.69621 214.233 62.6582 290.33C137.848 368.18 165.705 273.637 180 225.702C194.295 273.637 210.76 364.771 295.995 290.33C360 225.702 313.58 192.85 254.896 184.158C252.81 183.926 250.733 183.645 248.671 183.315C250.773 183.574 252.849 183.855 254.896 184.158C294.94 188.61 338.421 174.74 350.636 133.697C354.333 121.275 360 46.8568 360 36.519C360 26.1811 357.919 8.61012 346.474 3.43851C336.586 -1.02949 321.503 -5.86576 281.965 22.0466C240.692 51.1843 196.301 110.262 180 141.964Z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 956 B

View File

@@ -1,34 +0,0 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
import refreshToken from './refresh-token.js';
export default {
fields: [
{
key: 'handle',
label: 'Your Bluesky Handle',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: '',
clickToCopy: false,
},
{
key: 'password',
label: 'Your Bluesky Password',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: '',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
refreshToken,
};

View File

@@ -1,8 +0,0 @@
import getCurrentUser from '../common/get-current-user.js';
const isStillVerified = async ($) => {
const currentUser = await getCurrentUser($);
return !!currentUser.did;
};
export default isStillVerified;

View File

@@ -1,24 +0,0 @@
const refreshToken = async ($) => {
const { refreshJwt } = $.auth.data;
const { data } = await $.http.post(
'/com.atproto.server.refreshSession',
null,
{
headers: {
Authorization: `Bearer ${refreshJwt}`,
},
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
await $.auth.set({
accessJwt: data.accessJwt,
refreshJwt: data.refreshJwt,
did: data.did,
});
};
export default refreshToken;

View File

@@ -1,20 +0,0 @@
const verifyCredentials = async ($) => {
const handle = $.auth.data.handle;
const password = $.auth.data.password;
const body = {
identifier: handle,
password,
};
const { data } = await $.http.post('/com.atproto.server.createSession', body);
await $.auth.set({
accessJwt: data.accessJwt,
refreshJwt: data.refreshJwt,
did: data.did,
screenName: data.handle,
});
};
export default verifyCredentials;

View File

@@ -1,12 +0,0 @@
const addAuthHeader = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingAuthHeader)
return requestConfig;
if ($.auth.data?.accessJwt) {
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessJwt}`;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -1,15 +0,0 @@
const getCurrentUser = async ($) => {
const handle = $.auth.data.handle;
const params = {
actor: handle,
};
const { data: currentUser } = await $.http.get('/app.bsky.actor.getProfile', {
params,
});
return currentUser;
};
export default getCurrentUser;

View File

@@ -1,16 +0,0 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import auth from './auth/index.js';
export default defineApp({
name: 'Bluesky',
key: 'bluesky',
iconUrl: '{BASE_URL}/apps/bluesky/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/bluesky/connection',
supportsConnections: true,
baseUrl: 'https://bluesky.app',
apiBaseUrl: 'https://bsky.social/xrpc',
primaryColor: '1185fd',
beforeRequest: [addAuthHeader],
auth,
});

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

@@ -1,8 +1,10 @@
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({
@@ -16,7 +18,16 @@ export default defineAction({
type: 'dropdown', type: 'dropdown',
required: true, required: true,
variables: true, variables: true,
options: [{ label: 'Format Date / Time', value: 'formatDateTime' }], options: [
{
label: 'Get current timestamp',
value: 'getCurrentTimestamp',
},
{
label: 'Format Date / Time',
value: 'formatDateTime',
},
],
additionalFields: { additionalFields: {
type: 'query', type: 'query',
name: 'getDynamicFields', name: 'getDynamicFields',

View File

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

View File

@@ -14,6 +14,8 @@ 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,
@@ -30,6 +32,8 @@ const transformers = {
encodeUri, encodeUri,
trimWhitespace, trimWhitespace,
useDefaultValue, useDefaultValue,
parseStringifiedJson,
createUuid,
}; };
export default defineAction({ export default defineAction({
@@ -47,19 +51,21 @@ export default defineAction({
options: [ options: [
{ label: 'Base64 to String', value: 'base64ToString' }, { label: 'Base64 to String', value: 'base64ToString' },
{ label: 'Capitalize', value: 'capitalize' }, { label: 'Capitalize', value: 'capitalize' },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
{ label: 'Create UUID', value: 'createUuid' },
{ label: 'Encode URI', value: 'encodeUri' },
{ {
label: 'Encode URI Component', label: 'Encode URI Component',
value: 'encodeUriComponent', value: 'encodeUriComponent',
}, },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
{ label: 'Extract Email Address', value: 'extractEmailAddress' }, { label: 'Extract Email Address', value: 'extractEmailAddress' },
{ label: 'Extract Number', value: 'extractNumber' }, { label: 'Extract Number', value: 'extractNumber' },
{ label: 'Lowercase', value: 'lowercase' }, { label: 'Lowercase', value: 'lowercase' },
{ label: 'Parse stringified JSON', value: 'parseStringifiedJson' },
{ label: 'Pluralize', value: 'pluralize' }, { label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' }, { label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' }, { label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Encode URI', value: 'encodeUri' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' }, { label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' }, { label: 'Use Default Value', value: 'useDefaultValue' },
], ],

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
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]; export default [listTransformOptions, listReplaceRegexOptions];

View File

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

View File

@@ -12,6 +12,7 @@ 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';
@@ -38,6 +39,7 @@ const options = {
formatNumber, formatNumber,
formatPhoneNumber, formatPhoneNumber,
formatDateTime, formatDateTime,
parseStringifiedJson,
}; };
export default { export default {

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
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, createAttachment, executeQuery]; export default [findRecord, findPartiallyMatchingRecord, createAttachment, executeQuery];

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development', isDev: appEnv === 'development',
isTest: appEnv === 'test', isTest: appEnv === 'test',
isProd: appEnv === 'production', isProd: appEnv === 'production',
version: '0.11.0', version: '0.12.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,8 +97,12 @@ const appConfig = {
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true', disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true', disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK, additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkIcon: process.env.ADDITIONAL_DRAWER_LINK_ICON,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT, additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
disableSeedUser: process.env.DISABLE_SEED_USER === 'true', disableSeedUser: process.env.DISABLE_SEED_USER === 'true',
httpProxy: process.env.http_proxy,
httpsProxy: process.env.https_proxy,
noProxy: process.env.no_proxy,
}; };
if (!appConfig.encryptionKey) { if (!appConfig.encryptionKey) {

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ 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 () => {
@@ -48,4 +49,18 @@ describe('GET /api/v1/automatisch/config', () => {
expect(response.body).toEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return additional environment variables', async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue('link');
vi.spyOn(appConfig, 'additionalDrawerLinkIcon', 'get').mockReturnValue('icon');
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue('text');
expect(appConfig.disableNotificationsPage).toEqual(true);
expect(appConfig.disableFavicon).toEqual(true);
expect(appConfig.additionalDrawerLink).toEqual('link');
expect(appConfig.additionalDrawerLinkIcon).toEqual('icon');
expect(appConfig.additionalDrawerLinkText).toEqual('text');
});
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
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();
});
}

View File

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

View File

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

View File

@@ -10,15 +10,11 @@ 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';
@@ -46,15 +42,11 @@ const mutationResolvers = {
deleteFlow, deleteFlow,
deleteRole, deleteRole,
deleteStep, deleteStep,
deleteUser,
duplicateFlow, duplicateFlow,
executeFlow, executeFlow,
forgotPassword,
generateAuthUrl, generateAuthUrl,
login,
registerUser, registerUser,
resetConnection, resetConnection,
resetPassword,
updateAppAuthClient, updateAppAuthClient,
updateAppConfig, updateAppConfig,
updateConfig, updateConfig,

View File

@@ -1,10 +1,16 @@
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, password } = params.input; const { fullName, email } = params.input;
const existingUser = await User.query().findOne({ const existingUser = await User.query().findOne({
email: email.toLowerCase(), email: email.toLowerCase(),
@@ -17,7 +23,7 @@ const createUser = async (_parent, params, context) => {
const userPayload = { const userPayload = {
fullName, fullName,
email, email,
password, status: 'invited',
}; };
try { try {
@@ -32,7 +38,29 @@ const createUser = async (_parent, params, context) => {
const user = await User.query().insert(userPayload); const user = await User.query().insert(userPayload);
return user; await user.generateInvitationToken();
const jobName = `Invitation Email - ${user.id}`;
const acceptInvitationUrl = `${appConfig.webAppUrl}/accept-invitation?token=${user.invitationToken}`;
const jobPayload = {
email: user.email,
subject: 'You are invited!',
template: 'invitation-instructions',
params: {
fullName: user.fullName,
acceptInvitationUrl,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
await emailQueue.add(jobName, jobPayload, jobOptions);
return { user, acceptInvitationUrl };
}; };
export default createUser; export default createUser;

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,21 +8,17 @@ 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): User createUser(input: CreateUserInput): UserWithAcceptInvitationUrl
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
@@ -154,11 +150,6 @@ enum ArgumentEnumType {
string string
} }
type Auth {
user: User
token: String
}
type AuthenticationStep { type AuthenticationStep {
type: String type: String
name: String name: String
@@ -375,7 +366,6 @@ input DeleteStepInput {
input CreateUserInput { input CreateUserInput {
fullName: String! fullName: String!
email: String! email: String!
password: String!
role: UserRoleInput! role: UserRoleInput!
} }
@@ -390,10 +380,6 @@ input UpdateUserInput {
role: UserRoleInput role: UserRoleInput
} }
input DeleteUserInput {
id: String!
}
input RegisterUserInput { input RegisterUserInput {
fullName: String! fullName: String!
email: String! email: String!
@@ -406,20 +392,6 @@ 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!
@@ -520,6 +492,11 @@ type User {
updatedAt: String updatedAt: String
} }
type UserWithAcceptInvitationUrl {
user: User
acceptInvitationUrl: String
}
type Role { type Role {
id: String id: String
name: String name: String

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { DateTime } from 'luxon'; import { DateTime, Duration } 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,6 +21,13 @@ 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';
@@ -33,8 +40,21 @@ 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' },
resetPasswordToken: { type: 'string' }, status: {
resetPasswordTokenSentAt: { type: 'string' }, 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' },
@@ -202,6 +222,13 @@ 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,
@@ -210,7 +237,53 @@ class User extends Base {
}); });
} }
async isResetPasswordTokenValid() { async acceptInvitation(password) {
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;
} }
@@ -222,6 +295,18 @@ 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);
@@ -381,7 +466,7 @@ class User extends Base {
email, email,
password, password,
fullName, fullName,
roleId: adminRole.id roleId: adminRole.id,
}); });
await Config.markInstallationCompleted(); await Config.markInstallationCompleted();

View File

@@ -4,6 +4,7 @@ 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();
@@ -16,4 +17,11 @@ router.get(
asyncHandler(getUserAction) asyncHandler(getUserAction)
); );
router.delete(
'/:userId',
authenticateUser,
authorizeAdmin,
asyncHandler(deleteUserAction)
);
export default router; export default router;

View File

@@ -9,6 +9,9 @@ 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();
@@ -49,4 +52,9 @@ router.get(
asyncHandler(getPlanAndUsageAction) asyncHandler(getPlanAndUsageAction)
); );
router.post('/invitation', asyncHandler(acceptInvitationAction));
router.post('/forgot-password', asyncHandler(forgotPasswordAction));
router.post('/reset-password', asyncHandler(resetPasswordAction));
export default router; export default router;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,12 +50,6 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/appwrite/connection' }, { text: 'Connection', link: '/apps/appwrite/connection' },
], ],
}, },
{
text: 'Bluesky',
collapsible: true,
collapsed: true,
items: [{ text: 'Connection', link: '/apps/bluesky/connection' }],
},
{ {
text: 'Carbone', text: 'Carbone',
collapsible: true, collapsible: true,
@@ -65,6 +59,15 @@ 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,

View File

@@ -1,10 +0,0 @@
# Bluesky
:::info
This page explains the steps you need to follow to set up the Bluesky connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Enter your `Bluesky Handle` from the page to the `Your Bluesky Handle` field on Automatisch.
1. Enter your `Bluesky Password` from the page to the `Your Bluesky Password` field on Automatisch.
1. Click **Submit** button on Automatisch.
1. Congrats! Start using your new Bluesky connection within the flows.

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 430 390" fill="#1185fd" aria-hidden="true">
<path d="M180 141.964C163.699 110.262 119.308 51.1817 78.0347 22.044C38.4971 -5.86834 23.414 -1.03207 13.526 3.43594C2.08093 8.60755 0 26.1785 0 36.5164C0 46.8542 5.66748 121.272 9.36416 133.694C21.5786 174.738 65.0603 188.607 105.104 184.156C107.151 183.852 109.227 183.572 111.329 183.312C109.267 183.642 107.19 183.924 105.104 184.156C46.4204 192.847 -5.69621 214.233 62.6582 290.33C137.848 368.18 165.705 273.637 180 225.702C194.295 273.637 210.76 364.771 295.995 290.33C360 225.702 313.58 192.85 254.896 184.158C252.81 183.926 250.733 183.645 248.671 183.315C250.773 183.574 252.849 183.855 254.896 184.158C294.94 188.61 338.421 174.74 350.636 133.697C354.333 121.275 360 46.8568 360 36.519C360 26.1811 357.919 8.61012 346.474 3.43851C336.586 -1.02949 321.503 -5.86576 281.965 22.0466C240.692 51.1843 196.301 110.262 180 141.964Z">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 956 B

View File

@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,11 @@ 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) {
@@ -25,7 +26,6 @@ export class AdminCreateUserPage extends AuthenticatedPage {
return { return {
fullName: faker.person.fullName(), fullName: faker.person.fullName(),
email: faker.internet.email().toLowerCase(), email: faker.internet.email().toLowerCase(),
password: faker.internet.password(),
}; };
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,6 +87,7 @@ 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({
@@ -108,6 +109,7 @@ 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);
@@ -121,6 +123,7 @@ export class AdminUsersPage extends AuthenticatedPage {
async getRowsPerPage() { async getRowsPerPage() {
return await this.page.evaluate(() => { return await this.page.evaluate(() => {
// eslint-disable-next-line no-undef
const node = document.querySelector('[data-rows-per-page]'); const node = document.querySelector('[data-rows-per-page]');
if (node) { if (node) {
const count = Number(node.dataset.rowsPerPage); const count = Number(node.dataset.rowsPerPage);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,10 @@ const { ExecutionsPage } = require('./executions-page');
const { FlowEditorPage } = require('./flow-editor-page'); const { FlowEditorPage } = require('./flow-editor-page');
const { UserInterfacePage } = require('./user-interface-page'); const { UserInterfacePage } = require('./user-interface-page');
const { LoginPage } = require('./login-page'); const { LoginPage } = require('./login-page');
const { AcceptInvitation } = require('./accept-invitation-page');
const { adminFixtures } = require('./admin'); const { adminFixtures } = require('./admin');
const { AdminSetupPage } = require('./admin-setup-page');
const { AdminCreateUserPage } = require('./admin/create-user-page');
exports.test = test.extend({ exports.test = test.extend({
page: async ({ page }, use) => { page: async ({ page }, use) => {
@@ -46,6 +49,21 @@ exports.publicTest = test.extend({
await use(loginPage); await use(loginPage);
}, },
acceptInvitationPage: async ({ page }, use) => {
const acceptInvitationPage = new AcceptInvitation(page);
await use(acceptInvitationPage);
},
adminSetupPage: async ({ page }, use) => {
const adminSetupPage = new AdminSetupPage(page);
await use(adminSetupPage);
},
adminCreateUserPage: async ({page}, use) => {
const adminCreateUserPage = new AdminCreateUserPage(page);
await use(adminCreateUserPage);
}
}); });
expect.extend({ expect.extend({

View File

@@ -0,0 +1,11 @@
const { Client } = require('pg');
const client = new Client({
host: process.env.POSTGRES_HOST,
user: process.env.POSTGRES_USERNAME,
port: process.env.POSTGRES_PORT,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE
});
exports.client = client;

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