Compare commits

...

218 Commits

Author SHA1 Message Date
Ali BARIN
7dee26c16d chore: allow custom seed user creds 2024-03-07 08:41:30 +00:00
Ali BARIN
3d6847a3a2 Merge pull request #1558 from automatisch/AUT-606
fix: remove unnecessery styling causing the problem with scrolling Edit/Create role pages
2024-03-07 00:38:20 +01:00
Ali BARIN
613abaec1b Merge pull request #1550 from automatisch/AUT-563
feat: render AdminSettingsLayout once for all child routes
2024-03-06 18:56:59 +01:00
Ömer Faruk Aydın
b51ae9ac38 Merge pull request #1698 from automatisch/rest-get-execution-steps
feat: Implement get execution steps API endpoint
2024-03-06 17:30:33 +01:00
Faruk AYDIN
c4fd03542b feat: Implement get execution steps API endpoint 2024-03-06 17:23:56 +01:00
Faruk AYDIN
e40d6c5ef0 feat: Implement execution step serializer 2024-03-06 17:23:56 +01:00
kasia.oczkowska
9f7dee3baa test: introuce test fix, select role by exact name 2024-03-06 16:28:17 +01:00
kasia.oczkowska
d3da62c04a feat: render AdminSettingsLayout once for all child routes 2024-03-06 16:28:17 +01:00
kasia.oczkowska
aa7bb3f8c9 fix: remove unnecessery styling causing the problem 2024-03-06 15:55:03 +01:00
Ali BARIN
3a9dfe339a Merge pull request #1689 from automatisch/fix-auth-in-rq
fix: initialize auth in RQ client
2024-03-06 15:52:17 +01:00
Ali BARIN
08830003a3 Merge pull request #1560 from automatisch/AUT-619
fix: add optional chaining to data.getTrialStatus check
2024-03-06 15:34:04 +01:00
kasia.oczkowska
efeeb6cb02 fix: add optional chaining to data.getTrialStatus check and introduce minor code improvements 2024-03-06 15:12:02 +01:00
Ömer Faruk Aydın
ac59ce2deb Merge pull request #1697 from automatisch/rest-get-executions
feat: Implement get executions API endpoint
2024-03-06 15:08:35 +01:00
Faruk AYDIN
3ff89a03ac feat: Implement get executions API endpoint 2024-03-06 14:46:19 +01:00
Faruk AYDIN
25e231cd7c feat: Extend execution serializer with status 2024-03-06 14:46:10 +01:00
Ömer Faruk Aydın
f4d8d909b0 Merge pull request #1695 from automatisch/sentry-enhancement
chore: Do not enable sentry for dev and test environments
2024-03-05 15:53:36 +01:00
Faruk AYDIN
35ea18a117 chore: Do not enable sentry for dev and test environments 2024-03-05 15:47:07 +01:00
Ömer Faruk Aydın
ee9433261b Merge pull request #1693 from automatisch/refactor-get-executions-tests
refactor: Add steps to get execution tests
2024-03-04 16:23:09 +01:00
Faruk AYDIN
1cb48c7760 refactor: Add steps to get execution tests 2024-03-04 16:14:11 +01:00
Ömer Faruk Aydın
172bf4bd51 Merge pull request #1692 from automatisch/remove-empty-associations
refactor: Remove empty associations from serializers
2024-03-04 16:14:00 +01:00
Faruk AYDIN
fe1039cfbc test: Add permissions to get current user tests 2024-03-04 16:07:18 +01:00
Faruk AYDIN
a9de79546b refactor: Remove empty associations from serializers 2024-03-04 16:06:50 +01:00
kasia.oczkowska
7afdf43872 feat: introduce propTypes 2024-03-04 15:21:07 +01:00
Rıdvan Akca
bfc7d5d0dd refactor(web): rewrite useAdminAppAuthClient with react-query (#1690) 2024-03-04 13:09:42 +01:00
Ömer Faruk Aydın
690832052a Merge pull request #1691 from automatisch/error-handler
feat: Send rest API errors to Sentry
2024-03-04 13:01:20 +01:00
Faruk AYDIN
f7c1a47d52 feat: Send rest API errors to Sentry 2024-03-04 12:19:53 +01:00
Ali BARIN
930843d065 fix: initialize auth in RQ client 2024-03-04 10:48:55 +00:00
Ömer Faruk Aydın
a9ee609502 Merge pull request #1688 from automatisch/rest-get-execution
feat: Implement get execution API endpoint
2024-03-03 19:06:10 +01:00
Faruk AYDIN
9fd2125923 test: Add tests for execution serializer 2024-03-03 18:58:54 +01:00
Faruk AYDIN
ede8703f9d feat: Implement get execution API endpoint 2024-03-03 18:53:14 +01:00
Faruk AYDIN
6d85623d9b feat: Add execution serializer 2024-03-03 18:52:32 +01:00
Faruk AYDIN
6236ee8f6d feat: Add authorizedExecutions to user model 2024-03-03 18:51:50 +01:00
Ömer Faruk Aydın
92665d80d6 Merge pull request #1687 from automatisch/get-action-substeps
feat: Implement get action substeps API endpoint
2024-03-02 16:30:43 +01:00
Faruk AYDIN
70ae0bc77e feat: Implement get action substeps API endpoint 2024-03-02 16:24:20 +01:00
Faruk AYDIN
e28c757352 feat: Add find action substeps method to App model 2024-03-02 16:24:20 +01:00
Ömer Faruk Aydın
7cdcf7ebab Merge pull request #1686 from automatisch/get-app-actions
feat: Implement get app actions API endpoint
2024-03-02 16:19:57 +01:00
Faruk AYDIN
7c368af5ed feat: Implement get app actions API endpoint 2024-03-02 16:13:13 +01:00
Faruk AYDIN
df2fbbabc6 feat: Implement action serializer 2024-03-02 16:12:51 +01:00
Faruk AYDIN
48141eb199 feat: Add find actions by key method to App model 2024-03-02 16:09:57 +01:00
Ömer Faruk Aydın
343fbb282c Merge pull request #1685 from automatisch/get-trigger-substeps
feat: Implement get trigger substeps API endpoint
2024-03-02 14:44:06 +01:00
Faruk AYDIN
cea9ed056b feat: Implement get trigger substeps API endpoint 2024-03-01 16:35:46 +01:00
Ali BARIN
73bd90c555 refactor(web): utilize REACT_APP_BACKEND_URL for GQL and REST API URLs 2024-03-01 15:14:22 +01:00
Ali BARIN
917de46c45 chore(web): build with API URL 2024-03-01 15:14:22 +01:00
Ali BARIN
b592092923 chore: update building web in e2e tests 2024-03-01 15:14:22 +01:00
Ali BARIN
760bc1c22a fix(useAutomatischInfo): pass signal correctly 2024-03-01 15:14:22 +01:00
Ali BARIN
1f8b81ee78 refactor: remove GetAutomatischInfo GQL query 2024-03-01 15:14:22 +01:00
Ali BARIN
70af7d05e3 refactor: remove AutomatischInfo context 2024-03-01 15:14:22 +01:00
Ali BARIN
14c04ee4ac refactor: rewrite useAutomatischInfo with RQ and REST API 2024-03-01 15:14:22 +01:00
Ali BARIN
83815d3caa feat: introduce react-query and react-query-devtools 2024-03-01 15:14:22 +01:00
Ali BARIN
487c621fa5 chore: update react and react-dom to 18 2024-03-01 15:14:22 +01:00
Ömer Faruk Aydın
304eab801c Merge pull request #1684 from automatisch/rest-get-triggers
feat: Implement get triggers API endpoint
2024-03-01 15:10:31 +01:00
Faruk AYDIN
dfe3add1cc feat: Implement get triggers API endpoint 2024-03-01 15:03:53 +01:00
Faruk AYDIN
a32bf5539e feat: Add find triggers by key method to App model 2024-03-01 15:02:56 +01:00
Faruk AYDIN
e944333e5f feat: Implement trigger serializer 2024-03-01 15:02:21 +01:00
Ömer Faruk Aydın
dad23a52b0 Merge pull request #1683 from automatisch/rest-get-apps
feat: Implement get apps API endpoint
2024-03-01 14:27:59 +01:00
Faruk AYDIN
53606c306d feat: Implement get apps API endpoint 2024-03-01 14:21:41 +01:00
Ömer Faruk Aydın
53b03f8231 Merge pull request #1680 from automatisch/available-apps
docs: Add datastore to available apps
2024-03-01 12:03:02 +01:00
Faruk AYDIN
ac5f6dc024 docs: Add datastore to available apps 2024-03-01 11:50:42 +01:00
Ali BARIN
2c15e0dd32 Merge pull request #1665 from automatisch/ts-removal-in-web
refactor(web): remove typescript
2024-02-29 11:43:07 +01:00
Ömer Faruk Aydın
007334fef0 Merge pull request #1671 from automatisch/helix-integration
feat: Use new API endpoint from Helix
2024-02-29 11:41:19 +01:00
Ali BARIN
b3ae2d2748 refactor(web): remove typescript 2024-02-29 09:38:32 +00:00
Ömer Faruk Aydın
7149c766d0 Merge pull request #1675 from automatisch/get-app-auth
feat: Implement get app auth API endpoint
2024-02-28 16:25:40 +01:00
Faruk AYDIN
5dca0191d2 feat: Implement get app auth API endpoint 2024-02-28 16:16:13 +01:00
Faruk AYDIN
356668a68d feat: Implement auth serializer 2024-02-28 16:16:13 +01:00
Faruk AYDIN
63c9442126 feat: Add find by auth method to App model 2024-02-28 16:16:13 +01:00
Faruk AYDIN
0031d7911d fix: Adjust description of app serializer test 2024-02-28 16:16:13 +01:00
Faruk AYDIN
31c92b43b4 feat: Add optional system prompt variable to Helix app 2024-02-28 14:18:55 +01:00
Faruk AYDIN
2667548041 feat: Introduce optional session ID for helix app 2024-02-28 13:44:04 +01:00
Faruk AYDIN
54282ba7e0 feat: Use new API endpoint from Helix 2024-02-28 02:23:50 +01:00
Ömer Faruk Aydın
7f324abd44 Merge pull request #1669 from automatisch/helix-attemtps
fix: Add artificial delay to Helix API attempts
2024-02-27 23:49:05 +01:00
Faruk AYDIN
65a0c3b40a fix: Add artificial delay to Helix API attempts 2024-02-27 23:44:52 +01:00
Ömer Faruk Aydın
2449baac5b Merge pull request #1668 from automatisch/document-datastore
docs: Add datastore app to the integration list
2024-02-27 22:10:52 +01:00
Faruk AYDIN
0ab03e1856 docs: Add datastore app to the integration list 2024-02-27 21:44:51 +01:00
Ömer Faruk Aydın
9a3f85106c Merge pull request #1667 from automatisch/fix-helix-integration
fix: Stop asking to helix server after 50 attempts
2024-02-27 19:17:04 +01:00
Faruk AYDIN
42c495d8ab fix: Stop asking to helix server after 50 attempts 2024-02-27 19:14:19 +01:00
Ömer Faruk Aydın
58def585f1 Merge pull request #1666 from automatisch/datastore-app
feat: Implement datastore built-in app
2024-02-27 19:13:26 +01:00
Faruk AYDIN
047034d831 fix: Remove min length validation from value of datastore 2024-02-27 19:05:27 +01:00
Faruk AYDIN
bdb2f24a81 feat: Implement datastore built-in app 2024-02-27 19:01:46 +01:00
Ömer Faruk Aydın
636870a075 Merge pull request #1661 from automatisch/get-app
feat: Introduce app serializer
2024-02-26 22:32:06 +01:00
Faruk AYDIN
8981174302 feat: Introduce app serializer 2024-02-26 22:25:03 +01:00
Faruk AYDIN
dd5f05334b feat: Allow renderer to use explicitly defined serializers 2024-02-26 22:17:21 +01:00
Ömer Faruk Aydın
929b626b51 Merge pull request #1660 from automatisch/rest-get-app
feat: Implement get app API endpoint
2024-02-26 21:44:23 +01:00
Faruk AYDIN
7d5b2ec81e feat: Implement get app API endpoint 2024-02-26 17:59:48 +01:00
Ömer Faruk Aydın
f0e2d36c34 Merge pull request #1657 from automatisch/timestamp-serializer
feat: Use timestamp for serializer date objects
2024-02-26 14:36:34 +01:00
Faruk AYDIN
94f171d757 feat: Use timestamp for serializer date objects 2024-02-26 14:11:56 +01:00
Ömer Faruk Aydın
04e06db430 Merge pull request #1656 from automatisch/api-controller-tests
test: Cover not found responses for API endpoint tests
2024-02-26 13:36:48 +01:00
Faruk AYDIN
d74b215169 test: Cover bad request responses for API endpoint tests 2024-02-26 13:30:30 +01:00
Faruk AYDIN
404ea94dd2 test: Cover not found responses for API endpoint tests 2024-02-26 01:40:20 +01:00
Faruk AYDIN
4afe7c6b46 feat: Handle bad request for invalid UUID 2024-02-26 01:26:04 +01:00
Ömer Faruk Aydın
60b20c4d01 Merge pull request #1655 from automatisch/add-async-handler
feat: Implement async handler for routes
2024-02-26 01:25:35 +01:00
Faruk AYDIN
58658c6b1a feat: Do not expose unknown error message to client 2024-02-26 01:07:31 +01:00
Faruk AYDIN
ec444317b3 feat: Catch not found error message for objection 2024-02-26 01:06:54 +01:00
Faruk AYDIN
8b4aee1afa feat: Implement async handler for routes 2024-02-26 01:03:15 +01:00
Ömer Faruk Aydın
51abd74304 Merge pull request #1654 from automatisch/get-flow
feat: Implement get flow API endpoint
2024-02-26 01:02:38 +01:00
Faruk AYDIN
b93b465f09 feat: Implement get flow API endpoint 2024-02-26 00:52:02 +01:00
Faruk AYDIN
5aad68ec62 test: Use nested serializers explicitly for serializer tests 2024-02-25 23:34:41 +01:00
Faruk AYDIN
74fbc937a1 feat: Introduce flow serializer 2024-02-25 23:31:22 +01:00
Faruk AYDIN
7e35f544eb feat: Introduce step serializer 2024-02-25 23:01:55 +01:00
Ömer Faruk Aydın
ed1c3cffc1 Merge pull request #1653 from automatisch/rest-automatisch-license
faet: Implement automatisch license API endpoint
2024-02-25 18:36:42 +01:00
Ömer Faruk Aydın
c4983a9f9b Merge pull request #1652 from automatisch/rest-automatisch-info
feat: Implement automatisch info API endpoint
2024-02-25 18:36:27 +01:00
Ömer Faruk Aydın
5b43262e7a Merge pull request #1651 from automatisch/remove-role-join
chore: No need to join role since we don't expose roleId anymore
2024-02-25 18:36:10 +01:00
Ömer Faruk Aydın
dad4408679 Merge pull request #1650 from automatisch/rest-get-invoices
feat: Implement get invoices API endpoint
2024-02-25 18:35:52 +01:00
Ömer Faruk Aydın
a78c4d12b4 Merge pull request #1607 from automatisch/AUT-681
feat: implement app-auth-client endpoint
2024-02-25 18:35:33 +01:00
Ömer Faruk Aydın
74664a9df8 Merge pull request #1649 from automatisch/move-get-user
feat: Move get user API endpoint to admin namespace
2024-02-25 18:34:48 +01:00
Ömer Faruk Aydın
fce5281a03 Merge pull request #1648 from automatisch/move-get-users
feat: Move get users API endpoint to admin namespace
2024-02-25 18:32:24 +01:00
Faruk AYDIN
de0bd2f486 faet: Implement automatisch license API endpoint 2024-02-25 03:28:20 +01:00
Faruk AYDIN
079fb5d108 feat: Implement automatisch info API endpoint 2024-02-25 03:27:17 +01:00
Faruk AYDIN
1c7435a32b chore: No need to join role since we don't expose roleId anymore 2024-02-25 02:02:02 +01:00
Faruk AYDIN
1afd374cf6 feat: Implement get invoices API endpoint 2024-02-25 01:31:36 +01:00
Faruk AYDIN
3adf549915 feat: Extract get invoices logic to user model 2024-02-25 01:30:29 +01:00
Faruk AYDIN
e94d669eca fix: Cover empty array case for renderer helper 2024-02-25 01:29:59 +01:00
Faruk AYDIN
5fac0b4689 test: Add tests for app auth client serializer 2024-02-24 02:51:34 +01:00
Faruk AYDIN
832d323a6e refactor: Remove redundant query string from get app auth client tests 2024-02-24 01:25:46 +01:00
Faruk AYDIN
03f1dbd5b2 feat: Add check enterprise middleware to get app auth clients 2024-02-24 01:24:41 +01:00
Faruk AYDIN
c0a216f109 refactor: Remove license check for admin, since it is middleware responsibility 2024-02-24 01:22:27 +01:00
Faruk AYDIN
ad67b13270 fix: Add missing middleware imports for admin app auth clients 2024-02-24 01:18:30 +01:00
Faruk AYDIN
5d420c08c6 fix: Remove forgotten character in the routes 2024-02-24 01:14:56 +01:00
Faruk AYDIN
3d8235c670 refactor: Use kebab-case for app auth client serializer filename 2024-02-24 01:10:59 +01:00
Faruk AYDIN
5a209f81d1 feat: Add missing middleware checks to admin app auth clients 2024-02-24 01:08:08 +01:00
Rıdvan Akca
d17d8e2805 feat: implement app-auth-client endpoint 2024-02-24 01:02:28 +01:00
Faruk AYDIN
ca7636e7bc feat: Move get user API endpoint to admin namespace 2024-02-24 00:40:54 +01:00
Faruk AYDIN
532cfc10d0 feat: Move get users API endpoint to admin namespace 2024-02-24 00:31:15 +01:00
Ömer Faruk Aydın
72d68c4377 Merge pull request #1647 from automatisch/error-logger-for-queues
feat: Add logger for errors happened in queues
2024-02-23 23:57:00 +01:00
Faruk AYDIN
00f5964aa4 feat: Add logger for errors happened in queues 2024-02-23 23:50:50 +01:00
Ömer Faruk Aydın
fcf345abab Merge pull request #1642 from automatisch/delete-step
fix: Allow permitted users to delete others steps
2024-02-23 14:15:29 +01:00
Faruk AYDIN
24ad43d3e4 fix: Allow permitted users to delete others steps 2024-02-23 13:45:50 +01:00
Ömer Faruk Aydın
9a7cdf42e1 Merge pull request #1641 from automatisch/remove-role-id-from-user-serializer
chore: Remove redundant roleId from user serializer
2024-02-23 11:28:20 +01:00
Ömer Faruk Aydın
c36b652d5b Merge pull request #1640 from automatisch/rest-get-notifications
feat: Implement get notifications API endpoint
2024-02-23 11:28:11 +01:00
Ömer Faruk Aydın
553070fc23 Merge pull request #1638 from automatisch/rest-get-payment-paddle-info
feat: Implement get paddle info API endpoint
2024-02-23 11:27:50 +01:00
Ömer Faruk Aydın
5d69f7e24f Merge pull request #1637 from automatisch/rest-get-payment-plans
feat: Implement get payment plans API endpoint
2024-02-23 11:27:36 +01:00
Ömer Faruk Aydın
bc0e2bada0 Merge pull request #1635 from automatisch/rest-get-role
feat: Implement get role API endpoint for admin
2024-02-23 11:27:22 +01:00
Ömer Faruk Aydın
80b6cc1d94 Merge pull request #1636 from automatisch/rest-get-permissions-catalog
feat: Implement permission catalog API endpoint
2024-02-23 11:20:34 +01:00
Ömer Faruk Aydın
bce3273e64 Merge pull request #1634 from automatisch/rest-get-roles
feat: Implement admin get roles API endpoint
2024-02-23 11:20:25 +01:00
Faruk AYDIN
3abf61152a chore: Remove redundant roleId from user serializer 2024-02-23 01:30:57 +01:00
Faruk AYDIN
14923d4cd6 feat: Implement get notifications API endpoint 2024-02-23 01:24:56 +01:00
Faruk AYDIN
6fdc4bf900 feat: Implement get paddle info API endpoint 2024-02-22 20:22:05 +01:00
Faruk AYDIN
d21e1f75b5 feat: Implement get payment plans API endpoint 2024-02-21 18:15:32 +01:00
Faruk AYDIN
84a0b37fcc feat: Implement permission catalog API endpoint 2024-02-21 17:52:51 +01:00
Faruk AYDIN
f135a0f09e feat: Implement get role API endpoint for admin 2024-02-21 17:39:05 +01:00
Faruk AYDIN
0f24c99456 feat: Add permissions to role serializer 2024-02-21 16:01:45 +01:00
Faruk AYDIN
9eae0ab947 fix: Move get saml auth provider mocks to correct namespace 2024-02-21 15:37:39 +01:00
Faruk AYDIN
3bf1f79c79 feat: Implement admin get roles API endpoint 2024-02-21 15:35:30 +01:00
Faruk AYDIN
b21074c871 fix: Move saml auth provider router to correct folder 2024-02-21 14:59:42 +01:00
Ömer Faruk Aydın
d7893d9a32 Merge pull request #1621 from automatisch/show-saml-auth-provider
feat: Implement API endpoint to show saml auth provider
2024-02-20 13:00:25 +01:00
Ömer Faruk Aydın
9cbdda330c Merge pull request #1620 from automatisch/fix-authorization-middleware
fix: Include http methods for route rules
2024-02-20 12:59:12 +01:00
Ömer Faruk Aydın
42a9bfd099 Merge pull request #1619 from automatisch/get-saml-auth-providers
feat: Implement get saml auth providers API endpoint
2024-02-20 12:59:03 +01:00
Faruk AYDIN
eb15bd01ca feat: Implement API endpoint to show saml auth provider 2024-02-19 23:41:37 +01:00
Faruk AYDIN
9e98aebeb3 fix: Include http methods for route rules 2024-02-19 22:22:04 +01:00
Faruk AYDIN
1361cbc826 chore: Remove get saml auth providers from authorization list 2024-02-19 22:19:37 +01:00
Faruk AYDIN
679d0808a9 refactor: Move saml auth providers endpoint to admin namespace 2024-02-19 22:18:15 +01:00
Faruk AYDIN
6fe9a548ad feat: Implement get saml auth providers API endpoint 2024-02-19 21:48:06 +01:00
Faruk AYDIN
2d6d2430d2 fix: Detect types also for not paginated arrays 2024-02-19 21:46:20 +01:00
Faruk AYDIN
a445538e81 feat: Implement isCheckEnterprise middleware 2024-02-19 21:22:36 +01:00
Faruk AYDIN
50d38ffbd8 chore: Make http log level lower than info 2024-02-19 21:14:54 +01:00
Faruk AYDIN
93bcdfd9c9 feat: Implement saml auth provider serializer 2024-02-19 17:59:18 +01:00
Faruk AYDIN
5be3b101a5 feat: Implement saml auth provider factory 2024-02-19 17:58:52 +01:00
Ömer Faruk Aydın
024c7476c7 Merge pull request #1616 from automatisch/node-18-in-devcontainer
refactor: use node 18 in devcontainer
2024-02-19 11:51:59 +01:00
Ali BARIN
30a7ffe93d refactor: use node 18 in devcontainer 2024-02-19 10:17:16 +00:00
kattoczko
e2d803ebf7 feat: do not let users access notifications page when it's turned off (#1583) 2024-02-16 16:15:01 +01:00
kattoczko
be7e67c940 feat: introduce 404 page (#1600) 2024-02-16 15:54:35 +01:00
kattoczko
ead4b13ba5 feat: Show /login directly on / without valid authentication (#1528) 2024-02-16 15:15:25 +01:00
Ömer Faruk Aydın
e02c42ee18 Merge pull request #1605 from automatisch/test-permission-serializer
test: Add tests for permission serializer
2024-02-16 12:41:09 +01:00
Ömer Faruk Aydın
d39886fdf8 Merge pull request #1604 from automatisch/test-role-serializer
test: Add tests for role serializer
2024-02-16 12:40:59 +01:00
Ömer Faruk Aydın
11a425f1de Merge pull request #1603 from automatisch/test-user-serializer
test: Add tests for user serializer
2024-02-16 12:40:48 +01:00
Ömer Faruk Aydın
f0e194e584 Merge pull request #1606 from automatisch/remove-redundant
chore: Remove redundant npm libraries
2024-02-16 12:37:34 +01:00
Faruk AYDIN
d4b9331cf2 chore: Remove redundant npm libraries 2024-02-16 01:01:50 +01:00
Faruk AYDIN
37e1acc5f1 test: Add tests for permission serializer 2024-02-16 00:19:53 +01:00
Faruk AYDIN
ffaf6a577d test: Add tests for role serializer 2024-02-16 00:18:53 +01:00
Faruk AYDIN
afdaf6ba39 test: Add tests for user serializer 2024-02-16 00:10:37 +01:00
Ömer Faruk Aydın
4c49367910 Merge pull request #1602 from automatisch/introduce-serializers
feat: Introduce serializers
2024-02-15 12:46:20 +01:00
Ömer Faruk Aydın
a506c4411d Merge pull request #1601 from automatisch/rest-get-trial
feat: Implement API endpoint for user trial info
2024-02-15 12:45:54 +01:00
Faruk AYDIN
1859c9854e chore: Add permission serializer to serializers 2024-02-15 02:21:26 +01:00
Faruk AYDIN
6ff29b9ae6 refactor: Use serializer for user model instead of formatJson 2024-02-15 02:19:24 +01:00
Faruk AYDIN
3578f6b849 feat: Extend renderer to use serializers 2024-02-15 02:15:44 +01:00
Faruk AYDIN
0347864fde feat: Introduce serializers 2024-02-15 02:15:19 +01:00
Faruk AYDIN
5f9786a2c7 chore: Adjust get user trial file to have .ee extension 2024-02-14 15:55:02 +01:00
Faruk AYDIN
75aeff1898 test: Cover removed user token for authentication tests 2024-02-14 15:48:49 +01:00
Faruk AYDIN
0afcdce6d3 refactor: Do not expose subscription info for get user trial 2024-02-14 14:23:54 +01:00
Faruk AYDIN
a591d0ea87 test: Add tests for get user trial action 2024-02-14 14:18:42 +01:00
Faruk AYDIN
0e111a3532 feat: Implement API endpoint for user trial info 2024-02-14 14:18:28 +01:00
Faruk AYDIN
b599466ffa feat: Add checkIsCloud middleware for routes 2024-02-14 13:06:58 +01:00
Faruk AYDIN
69727e78df fix: Throw not found error for authentication 2024-02-14 13:06:29 +01:00
Ömer Faruk Aydın
02ae67b147 Merge pull request #1597 from automatisch/rest-api-get-users
feat: Implement api/v1/users API endpoint
2024-02-14 11:38:09 +01:00
Faruk AYDIN
a769f78801 test: Add tests for api/v1/users API endpoint 2024-02-14 01:20:29 +01:00
Faruk AYDIN
d583e42428 refactor: Structure api mock data with folders 2024-02-14 01:02:42 +01:00
Faruk AYDIN
da732becb6 feat: Implement api/v1/users API endpoint 2024-02-14 00:52:17 +01:00
Faruk AYDIN
b89a4d58d9 feat: Add pagination for REST endpoints 2024-02-14 00:51:48 +01:00
Faruk AYDIN
09854147d1 feat: Extend renderer functionality to work with pagination 2024-02-14 00:51:16 +01:00
Ömer Faruk Aydın
3648c2bfe3 Merge pull request #1592 from automatisch/rest-api-get-user
feat: Implement api/v1/users/:userId API endpoint
2024-02-13 12:14:37 +01:00
Ömer Faruk Aydın
3f3ee032f6 Merge pull request #1591 from automatisch/rest-api-get-current-user
feat: Implement users/me API endpoint
2024-02-13 11:34:37 +01:00
Ömer Faruk Aydın
68e5d54331 Merge pull request #1590 from automatisch/rest-api-automatisch-version
feat: Implement automatisch version API endpoint
2024-02-13 10:42:44 +01:00
Faruk AYDIN
824c434b0b feat: Implement api/v1/users/:userId API endpoint 2024-02-13 03:44:44 +01:00
Faruk AYDIN
9f0e0ca656 feat: Implement users/me API endpoint 2024-02-13 02:06:24 +01:00
Faruk AYDIN
95f89ba03e refactor: Use objectionjs instead of knex for factories 2024-02-13 01:49:40 +01:00
Faruk AYDIN
697f72ecf4 refactor: Use api/v1 namespace for routes 2024-02-12 23:30:38 +01:00
Faruk AYDIN
4f03f2ab51 feat: Introduce renderer helper 2024-02-12 23:25:09 +01:00
Faruk AYDIN
c81531cb7a feat: Implement automatisch version API endpoint 2024-02-12 22:59:44 +01:00
Ömer Faruk Aydın
7b6e4aa153 Merge pull request #1589 from automatisch/healthcheck-api-endpoint
feat: Implement healthcheck api endpoint
2024-02-12 22:57:58 +01:00
Faruk AYDIN
f21039d19d feat: Implement healthcheck api endpoint 2024-02-12 22:50:57 +01:00
morihoos
8c936a91be fix(csp): remove illegal characters in directive names (#1585) 2024-02-08 17:41:33 +01:00
Ali BARIN
24451892ff feat: add custom additional drawer link (#1586) 2024-02-08 16:33:12 +01:00
morihoos
6bba2c82fe feat(config): add ability to override apiUrl in environment variables (#1581) 2024-02-07 16:17:03 +01:00
Ali BARIN
3320dc6bc4 Merge pull request #1582 from automatisch/toggle-favicon-and-notifications-page
feat: put favicon and notifications page behind feature flags
2024-02-07 14:46:59 +01:00
Ali BARIN
9d42fd9293 test(queries/get-config): incorporate feature flags
cover disableNotificationsPage and disableFavicon feature flags
2024-02-07 13:11:57 +00:00
Ali BARIN
e6b806616f feat: add DISABLE_FAVICON feature flag 2024-02-07 11:51:17 +00:00
Ali BARIN
6ec5872391 feat: add DISABLE_NOTIFICATIONS_PAGE feature flag 2024-02-07 11:47:44 +00:00
Ali BARIN
a26cf932a1 chore(devcontainer): upgrade node to 20 2024-02-07 11:46:12 +00:00
Ali BARIN
38a3e3ab9f Merge pull request #1570 from automatisch/prevent-public-registrations
fix: prevent registration on non-cloud
2024-01-30 14:11:05 +01:00
Ali BARIN
32b17c1418 fix: prevent registration on non-cloud 2024-01-30 13:03:52 +00:00
Ömer Faruk Aydın
44aa6a1579 Merge pull request #1569 from automatisch/compile-email
fix: Adjust dirname for compile email helper
2024-01-29 13:05:58 +01:00
Faruk AYDIN
2369aacd2a fix: Adjust dirname for compile email helper 2024-01-29 12:48:21 +01:00
Ömer Faruk Aydın
7dafc6364b Merge pull request #1552 from automatisch/dependabot/npm_and_yarn/vite-3.2.8
chore(deps): Bump vite from 3.2.7 to 3.2.8
2024-01-29 12:32:10 +01:00
Ömer Faruk Aydın
3d25fa0aeb Merge branch 'main' into dependabot/npm_and_yarn/vite-3.2.8 2024-01-29 11:14:49 +01:00
Ali BARIN
0297b0f296 Merge pull request #1559 from automatisch/base64-to-text
feat(formatter): add base64 to string action
2024-01-26 10:56:54 +01:00
Rıdvan Akca
4c7d09c3d8 feat(formatter): add base64 to string action 2024-01-26 12:41:05 +03:00
Ali BARIN
48a74826e8 Merge pull request #1557 from automatisch/text-to-base64
feat(formatter): add string to base64 action
2024-01-25 16:56:15 +01:00
Rıdvan Akca
ef34068ac4 feat(formatter): add string to base64 action 2024-01-25 18:52:44 +03:00
dependabot[bot]
3987a8db77 chore(deps): Bump vite from 3.2.7 to 3.2.8
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.7 to 3.2.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.8/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 22:40:35 +00:00
549 changed files with 7598 additions and 5652 deletions

View File

@@ -28,7 +28,7 @@ cd packages/web
rm -rf .env
echo "
PORT=$WEB_PORT
REACT_APP_GRAPHQL_URL=http://localhost:$BACKEND_PORT/graphql
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
" >> .env
cd $CURRENT_DIR

View File

@@ -8,7 +8,7 @@
"version": "latest"
},
"ghcr.io/devcontainers/features/node:1": {
"version": 16
"version": 18
},
"ghcr.io/devcontainers/features/common-utils:1": {
"username": "vscode",

View File

@@ -1,18 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
overrides: [
{
files: ['**/*.test.ts', '**/test/**/*.ts'],
rules: {
'@typescript-eslint/ban-ts-comment': ['off'],
},
},
],
};

View File

@@ -22,7 +22,7 @@ jobs:
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
- run: yarn lint
- run: cd packages/backend && yarn lint
- run: echo "🍏 This job's status is ${{ job.status }}."
start-backend-server:
runs-on: ubuntu-latest

View File

@@ -62,8 +62,9 @@ jobs:
run: yarn && yarn lerna bootstrap
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Build Automatisch
run: yarn lerna run --scope=@*/{web,cli} build
- name: Build Automatisch web
working-directory: ./packages/web
run: yarn build
env:
# Keep this until we clean up warnings in build processes
CI: false

View File

@@ -6,7 +6,6 @@
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {
@@ -21,8 +20,6 @@
]
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.9.1",
"@typescript-eslint/parser": "^5.9.1",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",

View File

@@ -18,8 +18,8 @@ async function fetchAdminRole() {
}
export async function createUser(
email = 'user@automatisch.io',
password = 'sample'
email = appConfig.seedUserEmail,
password = appConfig.seedUserPassword
) {
const UNIQUE_VIOLATION_CODE = '23505';

View File

@@ -11,7 +11,7 @@
"start:worker": "node src/worker.js",
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
"test": "APP_ENV=test vitest run",
"lint": "eslint . --ignore-path ../../.eslintignore",
"lint": "eslint .",
"db:create": "node ./bin/database/create.js",
"db:seed:user": "node ./bin/database/seed-user.js",
"db:drop": "node ./bin/database/drop.js",
@@ -33,19 +33,18 @@
"axios": "1.6.0",
"bcrypt": "^5.0.1",
"bullmq": "^3.0.0",
"copyfiles": "^2.4.1",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
"dotenv": "^10.0.0",
"express": "~4.18.2",
"express-async-handler": "^1.2.0",
"express-basic-auth": "^1.2.1",
"express-graphql": "^0.12.0",
"fast-xml-parser": "^4.0.11",
"graphql-middleware": "^6.1.15",
"graphql-shield": "^7.5.0",
"graphql-tools": "^8.2.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.7",
"http-errors": "~1.6.3",
"http-proxy-agent": "^7.0.0",
@@ -68,7 +67,6 @@
"pluralize": "^8.0.0",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"stripe": "^11.13.0",
"winston": "^3.7.1",
"xmlrpc": "^1.3.2"
},

View File

@@ -0,0 +1,27 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Get value',
key: 'getValue',
description: 'Get value from the persistent datastore.',
arguments: [
{
label: 'Key',
key: 'key',
type: 'string',
required: true,
description: 'The key of your value to get.',
variables: true,
},
],
async run($) {
const keyValuePair = await $.datastore.get({
key: $.step.parameters.key,
});
$.setActionItem({
raw: keyValuePair,
});
},
});

View File

@@ -0,0 +1,4 @@
import getValue from './get-value/index.js';
import setValue from './set-value/index.js';
export default [getValue, setValue];

View File

@@ -0,0 +1,36 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Set value',
key: 'setValue',
description: 'Set value to the persistent datastore.',
arguments: [
{
label: 'Key',
key: 'key',
type: 'string',
required: true,
description: 'The key of your value to set.',
variables: true,
},
{
label: 'Value',
key: 'value',
type: 'string',
required: true,
description: 'The value to set.',
variables: true,
},
],
async run($) {
const keyValuePair = await $.datastore.set({
key: $.step.parameters.key,
value: $.step.parameters.value,
});
$.setActionItem({
raw: keyValuePair,
});
},
});

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" id="icon">
<defs>
<style>.cls-1{fill:none;}</style>
</defs>
<title>datastore</title>
<circle cx="23" cy="23" r="1"/>
<rect x="8" y="22" width="12" height="2"/>
<circle cx="23" cy="9" r="1"/>
<rect x="8" y="8" width="12" height="2"/>
<path d="M26,14a2,2,0,0,0,2-2V6a2,2,0,0,0-2-2H6A2,2,0,0,0,4,6v6a2,2,0,0,0,2,2H8v4H6a2,2,0,0,0-2,2v6a2,2,0,0,0,2,2H26a2,2,0,0,0,2-2V20a2,2,0,0,0-2-2H24V14ZM6,6H26v6H6ZM26,26H6V20H26Zm-4-8H10V14H22Z"/>
<rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/>
</svg>

After

Width:  |  Height:  |  Size: 704 B

View File

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

View File

@@ -1,5 +1,6 @@
import defineAction from '../../../../helpers/define-action.js';
import base64ToString from './transformers/base64-to-string.js';
import capitalize from './transformers/capitalize.js';
import extractEmailAddress from './transformers/extract-email-address.js';
import extractNumber from './transformers/extract-number.js';
@@ -8,10 +9,12 @@ import lowercase from './transformers/lowercase.js';
import markdownToHtml from './transformers/markdown-to-html.js';
import pluralize from './transformers/pluralize.js';
import replace from './transformers/replace.js';
import stringToBase64 from './transformers/string-to-base64.js';
import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js';
const transformers = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -20,6 +23,7 @@ const transformers = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
};
@@ -37,6 +41,7 @@ export default defineAction({
required: true,
variables: true,
options: [
{ label: 'Base64 to String', value: 'base64ToString' },
{ label: 'Capitalize', value: 'capitalize' },
{ label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' },
{ label: 'Convert Markdown to HTML', value: 'markdownToHtml' },
@@ -45,6 +50,7 @@ export default defineAction({
{ label: 'Lowercase', value: 'lowercase' },
{ label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' },
{ label: 'Trim Whitespace', value: 'trimWhitespace' },
{ label: 'Use Default Value', value: 'useDefaultValue' },
],

View File

@@ -0,0 +1,8 @@
const base64ToString = ($) => {
const input = $.step.parameters.input;
const decodedString = Buffer.from(input, 'base64').toString('utf8');
return decodedString;
};
export default base64ToString;

View File

@@ -0,0 +1,8 @@
const stringtoBase64 = ($) => {
const input = $.step.parameters.input;
const base64String = Buffer.from(input).toString('base64');
return base64String;
};
export default stringtoBase64;

View File

@@ -1,3 +1,4 @@
import base64ToString from './text/base64-to-string.js';
import capitalize from './text/capitalize.js';
import extractEmailAddress from './text/extract-email-address.js';
import extractNumber from './text/extract-number.js';
@@ -6,6 +7,7 @@ import lowercase from './text/lowercase.js';
import markdownToHtml from './text/markdown-to-html.js';
import pluralize from './text/pluralize.js';
import replace from './text/replace.js';
import stringToBase64 from './text/string-to-base64.js';
import trimWhitespace from './text/trim-whitespace.js';
import useDefaultValue from './text/use-default-value.js';
import performMathOperation from './numbers/perform-math-operation.js';
@@ -15,6 +17,7 @@ import formatPhoneNumber from './numbers/format-phone-number.js';
import formatDateTime from './date-time/format-date-time.js';
const options = {
base64ToString,
capitalize,
extractEmailAddress,
extractNumber,
@@ -23,6 +26,7 @@ const options = {
markdownToHtml,
pluralize,
replace,
stringToBase64,
trimWhitespace,
useDefaultValue,
performMathOperation,

View File

@@ -0,0 +1,12 @@
const base64ToString = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted from Base64 to string.',
variables: true,
},
];
export default base64ToString;

View File

@@ -0,0 +1,12 @@
const stringToBase64 = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Text that will be converted to Base64.',
variables: true,
},
];
export default stringToBase64;

View File

@@ -1,4 +1,3 @@
import FormData from 'form-data';
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
@@ -6,45 +5,51 @@ export default defineAction({
key: 'newChat',
description: 'Create a new chat session for Helix AI.',
arguments: [
{
label: 'Session ID',
key: 'sessionId',
type: 'string',
required: false,
description:
'ID of the chat session to continue. Leave empty to start a new chat.',
variables: true,
},
{
label: 'System Prompt',
key: 'systemPrompt',
type: 'string',
required: false,
description:
'Optional system prompt to start the chat with. It will be used only for new chat sessions.',
variables: true,
},
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'Prompt to start the chat with.',
description: 'User input to start the chat with.',
variables: true,
},
],
async run($) {
const formData = new FormData();
formData.append('input', $.step.parameters.input);
formData.append('mode', 'inference');
formData.append('type', 'text');
const sessionResponse = await $.http.post('/api/v1/sessions', formData, {
headers: {
...formData.getHeaders(),
},
const response = await $.http.post('/api/v1/sessions/chat', {
session_id: $.step.parameters.sessionId,
system: $.step.parameters.systemPrompt,
messages: [
{
role: 'user',
content: {
content_type: 'text',
parts: [$.step.parameters.input],
},
},
],
});
const sessionId = sessionResponse.data.id;
let chatGenerated = false;
while (!chatGenerated) {
const response = await $.http.get(`/api/v1/sessions/${sessionId}`);
const message =
response.data.interactions[response.data.interactions.length - 1];
if (message.creator === 'system' && message.state === 'complete') {
$.setActionItem({
raw: message,
});
chatGenerated = true;
}
}
$.setActionItem({
raw: response.data,
});
},
});

View File

@@ -18,7 +18,9 @@ const port = process.env.PORT || '3000';
const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let apiUrl = new URL(`${protocol}://${host}:${port}`).toString();
let apiUrl = new URL(
process.env.API_URL || `${protocol}://${host}:${port}`
).toString();
apiUrl = apiUrl.substring(0, apiUrl.length - 1);
// use apiUrl by default, which has less priority over the following cases
@@ -88,6 +90,12 @@ const appConfig = {
licenseKey: process.env.LICENSE_KEY,
sentryDsn: process.env.SENTRY_DSN,
CI: process.env.CI === 'true',
disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true',
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
seedUserEmail: process.env.SEED_USER_EMAIL || 'user@automatisch.io',
seedUserPassword: process.env.SEED_USER_PASSWORD || 'sample',
};
if (!appConfig.encryptionKey) {

View File

@@ -0,0 +1,10 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import AppAuthClient from '../../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId)
.throwIfNotFound();
renderObject(response, appAuthClient);
};

View File

@@ -0,0 +1,52 @@
import { vi, describe, it, expect, 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.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
import { createRole } from '../../../../../../test/factories/role.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/app-auth-clients/:appAuthClientId', () => {
let currentUser, currentUserRole, currentAppAuthClient, token;
describe('with valid license key', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
currentAppAuthClient = await createAppAuthClient();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const response = await request(app)
.get(`/api/v1/admin/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAdminAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client UUID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/admin/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/admin/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});
});

View File

@@ -0,0 +1,6 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import permissionCatalog from '../../../../../helpers/permission-catalog.ee.js';
export default async (request, response) => {
renderObject(response, permissionCatalog);
};

View File

@@ -0,0 +1,32 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getPermissionsCatalogMock from '../../../../../../test/mocks/rest/api/v1/admin/permissions/get-permissions-catalog.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/permissions/catalog', () => {
let role, currentUser, token;
beforeEach(async () => {
role = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: role.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return roles', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get('/api/v1/admin/permissions/catalog')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getPermissionsCatalogMock();
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,16 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import Role from '../../../../../models/role.js';
export default async (request, response) => {
const role = await Role.query()
.leftJoinRelated({
permissions: true,
})
.withGraphFetched({
permissions: true,
})
.findById(request.params.roleId)
.throwIfNotFound();
renderObject(response, role);
};

View File

@@ -0,0 +1,59 @@
import { vi, describe, it, expect, 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.js';
import { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createPermission } from '../../../../../../test/factories/permission.js';
import getRoleMock from '../../../../../../test/mocks/rest/api/v1/admin/roles/get-role.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/roles/:roleId', () => {
let role, currentUser, token, permissionOne, permissionTwo;
beforeEach(async () => {
role = await createRole({ key: 'admin' });
permissionOne = await createPermission({ roleId: role.id });
permissionTwo = await createPermission({ roleId: role.id });
currentUser = await createUser({ roleId: role.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return role', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get(`/api/v1/admin/roles/${role.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getRoleMock(role, [
permissionOne,
permissionTwo,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing role UUID', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const notExistingRoleUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/admin/roles/${notExistingRoleUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
await request(app)
.get('/api/v1/admin/roles/invalidRoleUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,8 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import Role from '../../../../../models/role.js';
export default async (request, response) => {
const roles = await Role.query().orderBy('name');
renderObject(response, roles);
};

View File

@@ -0,0 +1,33 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import getRolesMock from '../../../../../../test/mocks/rest/api/v1/admin/roles/get-roles.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/roles', () => {
let roleOne, roleTwo, currentUser, token;
beforeEach(async () => {
roleOne = await createRole({ key: 'admin' });
roleTwo = await createRole({ key: 'user' });
currentUser = await createUser({ roleId: roleOne.id });
token = createAuthTokenByUserId(currentUser.id);
});
it('should return roles', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get('/api/v1/admin/roles')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getRolesMock([roleOne, roleTwo]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,10 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js';
export default async (request, response) => {
const samlAuthProvider = await SamlAuthProvider.query()
.findById(request.params.samlAuthProviderId)
.throwIfNotFound();
renderObject(response, samlAuthProvider);
};

View File

@@ -0,0 +1,57 @@
import { vi, describe, it, expect, 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.js';
import { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
import getSamlAuthProviderMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => {
let samlAuthProvider, currentUser, token;
beforeEach(async () => {
const role = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: role.id });
samlAuthProvider = await createSamlAuthProvider();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return saml auth provider with specified id', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get(`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing saml auth provider UUID', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const notExistingSamlAuthProviderUUID = Crypto.randomUUID();
await request(app)
.get(
`/api/v1/admin/saml-auth-providers/${notExistingSamlAuthProviderUUID}`
)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
await request(app)
.get('/api/v1/admin/saml-auth-providers/invalidSamlAuthProviderUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,11 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js';
export default async (request, response) => {
const samlAuthProviders = await SamlAuthProvider.query().orderBy(
'created_at',
'desc'
);
renderObject(response, samlAuthProviders);
};

View File

@@ -0,0 +1,39 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../../app.js';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
import { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
import getSamlAuthProvidersMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/saml-auth-providers', () => {
let samlAuthProviderOne, samlAuthProviderTwo, currentUser, token;
beforeEach(async () => {
const role = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: role.id });
samlAuthProviderOne = await createSamlAuthProvider();
samlAuthProviderTwo = await createSamlAuthProvider();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return saml auth providers', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get('/api/v1/admin/saml-auth-providers')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getSamlAuthProvidersMock([
samlAuthProviderTwo,
samlAuthProviderOne,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,13 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import User from '../../../../../models/user.js';
export default async (request, response) => {
const user = await User.query()
.withGraphFetched({
role: true,
})
.findById(request.params.userId)
.throwIfNotFound();
renderObject(response, user);
};

View File

@@ -0,0 +1,55 @@
import { vi, describe, it, expect, 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';
import getUserMock from '../../../../../../test/mocks/rest/api/v1/admin/users/get-user.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/users/:userId', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: currentUserRole.id });
anotherUser = await createUser();
anotherUserRole = await anotherUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified user info', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get(`/api/v1/admin/users/${anotherUser.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing user UUID', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const notExistingUserUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/admin/users/${notExistingUserUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
await request(app)
.get('/api/v1/admin/users/invalidUserUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,15 @@
import { renderObject } from '../../../../../helpers/renderer.js';
import User from '../../../../../models/user.js';
import paginateRest from '../../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const usersQuery = User.query()
.withGraphFetched({
role: true,
})
.orderBy('full_name', 'asc');
const users = await paginateRest(usersQuery, request.query.page);
renderObject(response, users);
};

View File

@@ -0,0 +1,49 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../../app';
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../../../../test/factories/role';
import { createUser } from '../../../../../../test/factories/user';
import getUsersMock from '../../../../../../test/mocks/rest/api/v1/admin/users/get-users.js';
import * as license from '../../../../../helpers/license.ee.js';
describe('GET /api/v1/admin/users', () => {
let currentUser, currentUserRole, anotherUser, anotherUserRole, token;
beforeEach(async () => {
currentUserRole = await createRole({ key: 'admin' });
currentUser = await createUser({
roleId: currentUserRole.id,
fullName: 'Current User',
});
anotherUserRole = await createRole({
key: 'anotherUser',
name: 'Another user role',
});
anotherUser = await createUser({
roleId: anotherUserRole.id,
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return users data', async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get('/api/v1/admin/users')
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUsersMock(
[anotherUser, currentUser],
[anotherUserRole, currentUserRole]
);
expect(response.body).toEqual(expectedResponsePayload);
});
});

View File

@@ -0,0 +1,11 @@
import { renderObject } from '../../../../helpers/renderer.js';
import AppAuthClient from '../../../../models/app-auth-client.js';
export default async (request, response) => {
const appAuthClient = await AppAuthClient.query()
.findById(request.params.appAuthClientId)
.where({ active: true })
.throwIfNotFound();
renderObject(response, appAuthClient);
};

View File

@@ -0,0 +1,48 @@
import { vi, describe, it, expect, 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.js';
import { createUser } from '../../../../../test/factories/user.js';
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/app-auth-clients/:id', () => {
let currentUser, currentAppAuthClient, token;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
currentUser = await createUser();
currentAppAuthClient = await createAppAuthClient();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {
const response = await request(app)
.get(`/api/v1/app-auth-clients/${currentAppAuthClient.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing app auth client ID', async () => {
const notExistingAppAuthClientUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/app-auth-clients/${notExistingAppAuthClientUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await request(app)
.get('/api/v1/app-auth-clients/invalidAppAuthClientUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,11 @@
import App from '../../../../models/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const substeps = await App.findActionSubsteps(
request.params.appKey,
request.params.actionKey
);
renderObject(response, substeps);
};

View File

@@ -0,0 +1,52 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import App from '../../../../models/app';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getActionSubstepsMock from '../../../../../test/mocks/rest/api/v1/apps/get-action-substeps.js';
describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
let currentUser, exampleApp, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
exampleApp = await App.findOneByKey('github');
});
it('should return the app auth info', async () => {
const actions = await App.findActionsByKey('github');
const exampleAction = actions.find(
(action) => action.key === 'createIssue'
);
const endpointUrl = `/api/v1/apps/${exampleApp.key}/actions/${exampleAction.key}/substeps`;
const response = await request(app)
.get(endpointUrl)
.set('Authorization', token)
.expect(200);
const expectedPayload = getActionSubstepsMock(exampleAction.substeps);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
await request(app)
.get('/api/v1/apps/invalid-app-key/actions/invalid-actions-key/substeps')
.set('Authorization', token)
.expect(404);
});
it('should return empty array for invalid action key', async () => {
const endpointUrl = `/api/v1/apps/${exampleApp.key}/actions/invalid-action-key/substeps`;
const response = await request(app)
.get(endpointUrl)
.set('Authorization', token)
.expect(200);
expect(response.body.data).toEqual([]);
});
});

View File

@@ -0,0 +1,8 @@
import App from '../../../../models/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const actions = await App.findActionsByKey(request.params.appKey);
renderObject(response, actions, { serializer: 'Action' });
};

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import App from '../../../../models/app';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getActionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-actions.js';
describe('GET /api/v1/apps/:appKey/actions', () => {
let currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the app actions', async () => {
const exampleApp = await App.findOneByKey('github');
const response = await request(app)
.get(`/api/v1/apps/${exampleApp.key}/actions`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getActionsMock(exampleApp.actions);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
await request(app)
.get('/api/v1/apps/invalid-app-key/actions')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -0,0 +1,8 @@
import App from '../../../../models/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const app = await App.findOneByKey(request.params.appKey);
renderObject(response, app, { serializer: 'App' });
};

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import App from '../../../../models/app';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getAppMock from '../../../../../test/mocks/rest/api/v1/apps/get-app.js';
describe('GET /api/v1/apps/:appKey', () => {
let currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the app info', async () => {
const exampleApp = await App.findOneByKey('github');
const response = await request(app)
.get(`/api/v1/apps/${exampleApp.key}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppMock(exampleApp);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
await request(app)
.get('/api/v1/apps/invalid-app-key')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -0,0 +1,16 @@
import App from '../../../../models/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
let apps = await App.findAll(request.query.name);
if (request.query.onlyWithTriggers) {
apps = apps.filter((app) => app.triggers?.length);
}
if (request.query.onlyWithActions) {
apps = apps.filter((app) => app.actions?.length);
}
renderObject(response, apps, { serializer: 'App' });
};

View File

@@ -0,0 +1,63 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import App from '../../../../models/app';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getAppsMock from '../../../../../test/mocks/rest/api/v1/apps/get-apps.js';
describe('GET /api/v1/apps', () => {
let currentUser, apps, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
apps = await App.findAll();
});
it('should return all apps', async () => {
const response = await request(app)
.get('/api/v1/apps')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock(apps);
expect(response.body).toEqual(expectedPayload);
});
it('should return all apps filtered by name', async () => {
const appsWithNameGit = apps.filter((app) => app.name.includes('Git'));
const response = await request(app)
.get('/api/v1/apps?name=Git')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock(appsWithNameGit);
expect(response.body).toEqual(expectedPayload);
});
it('should return only the apps with triggers', async () => {
const appsWithTriggers = apps.filter((app) => app.triggers?.length > 0);
const response = await request(app)
.get('/api/v1/apps?onlyWithTriggers=true')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock(appsWithTriggers);
expect(response.body).toEqual(expectedPayload);
});
it('should return only the apps with actions', async () => {
const appsWithActions = apps.filter((app) => app.actions?.length > 0);
const response = await request(app)
.get('/api/v1/apps?onlyWithActions=true')
.set('Authorization', token)
.expect(200);
const expectedPayload = getAppsMock(appsWithActions);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,8 @@
import App from '../../../../models/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const auth = await App.findAuthByKey(request.params.appKey);
renderObject(response, auth, { serializer: 'Auth' });
};

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import App from '../../../../models/app';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getAuthMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth.js';
describe('GET /api/v1/apps/:appKey/auth', () => {
let currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the app auth info', async () => {
const exampleApp = await App.findOneByKey('github');
const response = await request(app)
.get(`/api/v1/apps/${exampleApp.key}/auth`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getAuthMock(exampleApp.auth);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
await request(app)
.get('/api/v1/apps/invalid-app-key/auth')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -0,0 +1,11 @@
import App from '../../../../models/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const substeps = await App.findTriggerSubsteps(
request.params.appKey,
request.params.triggerKey
);
renderObject(response, substeps);
};

View File

@@ -0,0 +1,52 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import App from '../../../../models/app';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getTriggerSubstepsMock from '../../../../../test/mocks/rest/api/v1/apps/get-trigger-substeps.js';
describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
let currentUser, exampleApp, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
exampleApp = await App.findOneByKey('github');
});
it('should return the app auth info', async () => {
const triggers = await App.findTriggersByKey('github');
const exampleTrigger = triggers.find(
(trigger) => trigger.key === 'newIssues'
);
const endpointUrl = `/api/v1/apps/${exampleApp.key}/triggers/${exampleTrigger.key}/substeps`;
const response = await request(app)
.get(endpointUrl)
.set('Authorization', token)
.expect(200);
const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
await request(app)
.get('/api/v1/apps/invalid-app-key/triggers/invalid-trigger-key/substeps')
.set('Authorization', token)
.expect(404);
});
it('should return empty array for invalid trigger key', async () => {
const endpointUrl = `/api/v1/apps/${exampleApp.key}/triggers/invalid-trigger-key/substeps`;
const response = await request(app)
.get(endpointUrl)
.set('Authorization', token)
.expect(200);
expect(response.body.data).toEqual([]);
});
});

View File

@@ -0,0 +1,8 @@
import App from '../../../../models/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const triggers = await App.findTriggersByKey(request.params.appKey);
renderObject(response, triggers, { serializer: 'Trigger' });
};

View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import App from '../../../../models/app';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import getTriggersMock from '../../../../../test/mocks/rest/api/v1/apps/get-triggers.js';
describe('GET /api/v1/apps/:appKey/triggers', () => {
let currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the app triggers', async () => {
const exampleApp = await App.findOneByKey('github');
const response = await request(app)
.get(`/api/v1/apps/${exampleApp.key}/triggers`)
.set('Authorization', token)
.expect(200);
const expectedPayload = getTriggersMock(exampleApp.triggers);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for invalid app key', async () => {
await request(app)
.get('/api/v1/apps/invalid-app-key/triggers')
.set('Authorization', token)
.expect(404);
});
});

View File

@@ -0,0 +1,13 @@
import appConfig from '../../../../config/app.js';
import { hasValidLicense } from '../../../../helpers/license.ee.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const info = {
isCloud: appConfig.isCloud,
isMation: appConfig.isMation,
isEnterprise: await hasValidLicense(),
};
renderObject(response, info);
};

View File

@@ -0,0 +1,22 @@
import { vi, expect, describe, it } from 'vitest';
import request from 'supertest';
import appConfig from '../../../../config/app.js';
import app from '../../../../app.js';
import infoMock from '../../../../../test/mocks/rest/api/v1/automatisch/info.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/automatisch/info', () => {
it('should return Automatisch info', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false);
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
const response = await request(app)
.get('/api/v1/automatisch/info')
.expect(200);
const expectedPayload = infoMock();
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,15 @@
import { getLicense } from '../../../../helpers/license.ee.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const license = await getLicense();
const computedLicense = {
id: license ? license.id : null,
name: license ? license.name : null,
expireAt: license ? license.expireAt : null,
verified: license ? true : false,
};
renderObject(response, computedLicense);
};

View File

@@ -0,0 +1,23 @@
import { vi, expect, describe, it } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import licenseMock from '../../../../../test/mocks/rest/api/v1/automatisch/license.js';
import * as license from '../../../../helpers/license.ee.js';
describe('GET /api/v1/automatisch/license', () => {
it('should return Automatisch license info', async () => {
vi.spyOn(license, 'getLicense').mockResolvedValue({
id: '123',
name: 'license-name',
expireAt: '2025-12-31T23:59:59Z',
});
const response = await request(app)
.get('/api/v1/automatisch/license')
.expect(200);
const expectedPayload = licenseMock();
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,19 @@
import { renderObject } from '../../../../helpers/renderer.js';
import axios from '../../../../helpers/axios-with-proxy.js';
import logger from '../../../../helpers/logger.js';
const NOTIFICATIONS_URL =
'https://notifications.automatisch.io/notifications.json';
export default async (request, response) => {
let notifications = [];
try {
const response = await axios.get(NOTIFICATIONS_URL);
notifications = response.data;
} catch (error) {
logger.error('Error fetching notifications API endpoint!', error);
}
renderObject(response, notifications);
};

View File

@@ -0,0 +1,9 @@
import { describe, it } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
describe('GET /api/v1/automatisch/notifications', () => {
it('should return Automatisch notifications', async () => {
await request(app).get('/api/v1/automatisch/notifications').expect(200);
});
});

View File

@@ -0,0 +1,6 @@
import appConfig from '../../../../config/app.js';
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, { version: appConfig.version });
};

View File

@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
describe('GET /api/v1/automatisch/version', () => {
it('should return Automatisch version', async () => {
const response = await request(app)
.get('/api/v1/automatisch/version')
.expect(200);
const expectedPayload = {
data: {
version: '0.10.0',
},
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'Object',
},
};
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,23 @@
import { renderObject } from '../../../../helpers/renderer.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const execution = await request.currentUser.authorizedExecutions
.clone()
.withSoftDeleted()
.findById(request.params.executionId)
.throwIfNotFound();
const executionStepsQuery = execution
.$relatedQuery('executionSteps')
.withSoftDeleted()
.withGraphFetched('step')
.orderBy('created_at', 'asc');
const executionSteps = await paginateRest(
executionStepsQuery,
request.query.page
);
renderObject(response, executionSteps);
};

View File

@@ -0,0 +1,153 @@
import { describe, it, expect, 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 { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createExecution } from '../../../../../test/factories/execution.js';
import { createExecutionStep } from '../../../../../test/factories/execution-step.js';
import { createPermission } from '../../../../../test/factories/permission';
import getExecutionStepsMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution-steps';
describe('GET /api/v1/executions/:executionId/execution-steps', () => {
let currentUser, currentUserRole, anotherUser, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
anotherUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the execution steps of current user execution', async () => {
const currentUserFlow = await createFlow({
userId: currentUser.id,
});
const stepOne = await createStep({
flowId: currentUserFlow.id,
type: 'trigger',
});
const stepTwo = await createStep({
flowId: currentUserFlow.id,
type: 'action',
});
const currentUserExecution = await createExecution({
flowId: currentUserFlow.id,
});
const currentUserExecutionStepOne = await createExecutionStep({
executionId: currentUserExecution.id,
stepId: stepOne.id,
});
const currentUserExecutionStepTwo = await createExecutionStep({
executionId: currentUserExecution.id,
stepId: stepTwo.id,
});
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get(`/api/v1/executions/${currentUserExecution.id}/execution-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getExecutionStepsMock(
[currentUserExecutionStepOne, currentUserExecutionStepTwo],
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the execution steps of another user execution', async () => {
const anotherUserFlow = await createFlow({
userId: anotherUser.id,
});
const stepOne = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
});
const stepTwo = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const anotherUserExecution = await createExecution({
flowId: anotherUserFlow.id,
});
const anotherUserExecutionStepOne = await createExecutionStep({
executionId: anotherUserExecution.id,
stepId: stepOne.id,
});
const anotherUserExecutionStepTwo = await createExecutionStep({
executionId: anotherUserExecution.id,
stepId: stepTwo.id,
});
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get(`/api/v1/executions/${anotherUserExecution.id}/execution-steps`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getExecutionStepsMock(
[anotherUserExecutionStepOne, anotherUserExecutionStepTwo],
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing execution step UUID', async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingExcecutionUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/executions/${notExistingExcecutionUUID}/execution-steps`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.get('/api/v1/executions/invalidExecutionUUID/execution-steps')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,15 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const execution = await request.currentUser.authorizedExecutions
.withGraphFetched({
flow: {
steps: true,
},
})
.withSoftDeleted()
.findById(request.params.executionId)
.throwIfNotFound();
renderObject(response, execution);
};

View File

@@ -0,0 +1,134 @@
import { describe, it, expect, 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 { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createExecution } from '../../../../../test/factories/execution.js';
import { createPermission } from '../../../../../test/factories/permission';
import getExecutionMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution';
describe('GET /api/v1/executions/:executionId', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the execution data of current user', async () => {
const currentUserFlow = await createFlow({
userId: currentUser.id,
});
const stepOne = await createStep({
flowId: currentUserFlow.id,
type: 'trigger',
});
const stepTwo = await createStep({
flowId: currentUserFlow.id,
type: 'action',
});
const currentUserExecution = await createExecution({
flowId: currentUserFlow.id,
});
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get(`/api/v1/executions/${currentUserExecution.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getExecutionMock(
currentUserExecution,
currentUserFlow,
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the execution data of another user', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({
userId: anotherUser.id,
});
const stepOne = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
});
const stepTwo = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const anotherUserExecution = await createExecution({
flowId: anotherUserFlow.id,
});
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get(`/api/v1/executions/${anotherUserExecution.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getExecutionMock(
anotherUserExecution,
anotherUserFlow,
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing execution UUID', async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingExcecutionUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/executions/${notExistingExcecutionUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.get('/api/v1/executions/invalidExecutionUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,26 @@
import { renderObject } from '../../../../helpers/renderer.js';
import paginateRest from '../../../../helpers/pagination-rest.js';
export default async (request, response) => {
const executionsQuery = request.currentUser.authorizedExecutions
.withSoftDeleted()
.orderBy('created_at', 'desc')
.withGraphFetched({
flow: {
steps: true,
},
});
const executions = await paginateRest(executionsQuery, request.query.page);
for (const execution of executions.records) {
const executionSteps = await execution.$relatedQuery('executionSteps');
const status = executionSteps.some((step) => step.status === 'failure')
? 'failure'
: 'success';
execution.status = status;
}
renderObject(response, executions);
};

View File

@@ -0,0 +1,113 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import { createFlow } from '../../../../../test/factories/flow.js';
import { createStep } from '../../../../../test/factories/step.js';
import { createExecution } from '../../../../../test/factories/execution.js';
import { createPermission } from '../../../../../test/factories/permission';
import getExecutionsMock from '../../../../../test/mocks/rest/api/v1/executions/get-executions';
describe('GET /api/v1/executions', () => {
let currentUser, currentUserRole, anotherUser, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
anotherUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the executions of current user', async () => {
const currentUserFlow = await createFlow({
userId: currentUser.id,
});
const stepOne = await createStep({
flowId: currentUserFlow.id,
type: 'trigger',
});
const stepTwo = await createStep({
flowId: currentUserFlow.id,
type: 'action',
});
const currentUserExecutionOne = await createExecution({
flowId: currentUserFlow.id,
});
const currentUserExecutionTwo = await createExecution({
flowId: currentUserFlow.id,
deletedAt: new Date().toISOString(),
});
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get('/api/v1/executions')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getExecutionsMock(
[currentUserExecutionTwo, currentUserExecutionOne],
currentUserFlow,
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
it('should return the executions of another user', async () => {
const anotherUserFlow = await createFlow({
userId: anotherUser.id,
});
const stepOne = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
});
const stepTwo = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});
const anotherUserExecutionOne = await createExecution({
flowId: anotherUserFlow.id,
});
const anotherUserExecutionTwo = await createExecution({
flowId: anotherUserFlow.id,
deletedAt: new Date().toISOString(),
});
await createPermission({
action: 'read',
subject: 'Execution',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get('/api/v1/executions')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getExecutionsMock(
[anotherUserExecutionTwo, anotherUserExecutionOne],
anotherUserFlow,
[stepOne, stepTwo]
);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,11 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const flow = await request.currentUser.authorizedFlows
.withGraphJoined({ steps: true })
.orderBy('steps.position', 'asc')
.findOne({ 'flows.id': request.params.flowId })
.throwIfNotFound();
renderObject(response, flow);
};

View File

@@ -0,0 +1,102 @@
import { describe, it, expect, 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 { createFlow } from '../../../../../test/factories/flow';
import { createStep } from '../../../../../test/factories/step';
import { createPermission } from '../../../../../test/factories/permission';
import getFlowMock from '../../../../../test/mocks/rest/api/v1/flows/get-flow';
describe('GET /api/v1/flows/:flowId', () => {
let currentUser, currentUserRole, token;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
});
it('should return the flow data of current user', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });
const triggerStep = await createStep({ flowId: currentUserflow.id });
const actionStep = await createStep({ flowId: currentUserflow.id });
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
const response = await request(app)
.get(`/api/v1/flows/${currentUserflow.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowMock(currentUserflow, [
triggerStep,
actionStep,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return the flow data of another user', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
const triggerStep = await createStep({ flowId: anotherUserFlow.id });
const actionStep = await createStep({ flowId: anotherUserFlow.id });
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const response = await request(app)
.get(`/api/v1/flows/${anotherUserFlow.id}`)
.set('Authorization', token)
.expect(200);
const expectedPayload = await getFlowMock(anotherUserFlow, [
triggerStep,
actionStep,
]);
expect(response.body).toEqual(expectedPayload);
});
it('should return not found response for not existing flow UUID', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
const notExistingFlowUUID = Crypto.randomUUID();
await request(app)
.get(`/api/v1/flows/${notExistingFlowUUID}`)
.set('Authorization', token)
.expect(404);
});
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
await request(app)
.get('/api/v1/flows/invalidFlowUUID')
.set('Authorization', token)
.expect(400);
});
});

View File

@@ -0,0 +1,8 @@
import { renderObject } from '../../../../helpers/renderer.js';
import Billing from '../../../../helpers/billing/index.ee.js';
export default async (request, response) => {
const paddleInfo = Billing.paddleInfo;
renderObject(response, paddleInfo);
};

View File

@@ -0,0 +1,33 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getPaddleInfoMock from '../../../../../test/mocks/rest/api/v1/payment/get-paddle-info.js';
import appConfig from '../../../../config/app.js';
import billing from '../../../../helpers/billing/index.ee.js';
describe('GET /api/v1/payment/paddle-info', () => {
let user, token;
beforeEach(async () => {
user = await createUser();
token = createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
vi.spyOn(billing.paddleInfo, 'vendorId', 'get').mockReturnValue(
'sampleVendorId'
);
});
it('should return payment plans', async () => {
const response = await request(app)
.get('/api/v1/payment/paddle-info')
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getPaddleInfoMock();
expect(response.body).toEqual(expectedResponsePayload);
});
});

View File

@@ -0,0 +1,8 @@
import { renderObject } from '../../../../helpers/renderer.js';
import Billing from '../../../../helpers/billing/index.ee.js';
export default async (request, response) => {
const paymentPlans = Billing.paddlePlans;
renderObject(response, paymentPlans);
};

View File

@@ -0,0 +1,29 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getPaymentPlansMock from '../../../../../test/mocks/rest/api/v1/payment/get-plans.js';
import appConfig from '../../../../config/app.js';
describe('GET /api/v1/payment/plans', () => {
let user, token;
beforeEach(async () => {
user = await createUser();
token = createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});
it('should return payment plans', async () => {
const response = await request(app)
.get('/api/v1/payment/plans')
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getPaymentPlansMock();
expect(response.body).toEqual(expectedResponsePayload);
});
});

View File

@@ -0,0 +1,5 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
renderObject(response, request.currentUser);
};

View File

@@ -0,0 +1,44 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createPermission } from '../../../../../test/factories/permission';
import { createRole } from '../../../../../test/factories/role';
import { createUser } from '../../../../../test/factories/user';
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
describe('GET /api/v1/users/me', () => {
let role, permissionOne, permissionTwo, currentUser, token;
beforeEach(async () => {
role = await createRole();
permissionOne = await createPermission({
roleId: role.id,
});
permissionTwo = await createPermission({
roleId: role.id,
});
currentUser = await createUser({
roleId: role.id,
});
token = createAuthTokenByUserId(currentUser.id);
});
it('should return current user info', async () => {
const response = await request(app)
.get('/api/v1/users/me')
.set('Authorization', token)
.expect(200);
const expectedPayload = getCurrentUserMock(currentUser, role, [
permissionOne,
permissionTwo,
]);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,7 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const invoices = await request.currentUser.getInvoices();
renderObject(response, invoices);
};

View File

@@ -0,0 +1,34 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
import { createUser } from '../../../../../test/factories/user';
import User from '../../../../models/user';
import getInvoicesMock from '../../../../../test/mocks/rest/api/v1/users/get-invoices.ee';
describe('GET /api/v1/user/invoices', () => {
let currentUser, token;
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
});
it('should return current user invoices', async () => {
const invoices = [
{ id: 1, amount: 100, description: 'Invoice 1' },
{ id: 2, amount: 200, description: 'Invoice 2' },
];
vi.spyOn(User.prototype, 'getInvoices').mockResolvedValue(invoices);
const response = await request(app)
.get('/api/v1/users/invoices')
.set('Authorization', token)
.expect(200);
const expectedPayload = await getInvoicesMock(invoices);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -0,0 +1,12 @@
import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => {
const inTrial = await request.currentUser.inTrial();
const trialInfo = {
inTrial,
expireAt: request.currentUser.trialExpiryDate,
};
renderObject(response, trialInfo);
};

View File

@@ -0,0 +1,38 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../../../app.js';
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
import { createUser } from '../../../../../test/factories/user.js';
import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial.js';
import appConfig from '../../../../config/app.js';
import { DateTime } from 'luxon';
import User from '../../../../models/user.js';
describe('GET /api/v1/users/:userId/trial', () => {
let user, token;
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
token = createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});
describe('should return in trial, active subscription and expire at info', () => {
beforeEach(async () => {
vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false);
vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(true);
});
it('should return null', async () => {
const response = await request(app)
.get(`/api/v1/users/${user.id}/trial`)
.set('Authorization', token)
.expect(200);
const expectedResponsePayload = await getUserTrialMock(user);
expect(response.body).toEqual(expectedResponsePayload);
});
});
});

View File

@@ -0,0 +1,3 @@
export default async (request, response) => {
response.status(200).end();
};

View File

@@ -0,0 +1,9 @@
import { describe, it } from 'vitest';
import request from 'supertest';
import app from '../../app.js';
describe('GET /healthcheck', () => {
it('should return 200 response with version data', async () => {
await request(app).get('/healthcheck').expect(200);
});
});

View File

@@ -0,0 +1,16 @@
export async function up(knex) {
return knex.schema.createTable('datastore', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('key').notNullable();
table.string('value');
table.string('scope').notNullable();
table.uuid('scope_id').notNullable();
table.index(['key', 'scope', 'scope_id']);
table.timestamps(true, true);
});
}
export async function down(knex) {
return knex.schema.dropTable('datastore');
}

View File

@@ -1,8 +1,13 @@
const deleteStep = async (_parent, params, context) => {
context.currentUser.can('update', 'Flow');
import Step from '../../models/flow.js';
const step = await context.currentUser
.$relatedQuery('steps')
const deleteStep = async (_parent, params, context) => {
const conditions = context.currentUser.can('update', 'Flow');
const isCreator = conditions.isCreator;
const allSteps = Step.query();
const userSteps = context.currentUser.$relatedQuery('steps');
const baseQuery = isCreator ? userSteps : allSteps;
const step = await baseQuery
.withGraphFetched('flow')
.findOne({
'steps.id': params.input.id,

View File

@@ -1,7 +1,10 @@
import appConfig from '../../config/app.js';
import User from '../../models/user.js';
import Role from '../../models/role.js';
const registerUser = async (_parent, params) => {
if (!appConfig.isCloud) return;
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({

View File

@@ -1,21 +0,0 @@
import appConfig from '../../config/app.js';
import { getLicense } from '../../helpers/license.ee.js';
const getAutomatischInfo = async () => {
const license = await getLicense();
const computedLicense = {
id: license ? license.id : null,
name: license ? license.name : null,
expireAt: license ? license.expireAt : null,
verified: license ? true : false,
};
return {
isCloud: appConfig.isCloud,
isMation: appConfig.isMation,
license: computedLicense,
};
};
export default getAutomatischInfo;

View File

@@ -1,190 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import * as license from '../../helpers/license.ee';
import appConfig from '../../config/app';
describe('graphQL getAutomatischInfo query', () => {
const query = `
query {
getAutomatischInfo {
isCloud
isMation
license {
id
name
expireAt
verified
}
}
}
`;
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'getLicense').mockResolvedValue(false);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false);
});
it('should return empty license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: false,
license: {
id: null,
name: null,
expireAt: null,
verified: false,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with valid license', () => {
beforeEach(async () => {
const mockedLicense = {
id: '123123',
name: 'Test License',
expireAt: '2025-08-09T10:56:54.144Z',
verified: true,
};
vi.spyOn(license, 'getLicense').mockResolvedValue(mockedLicense);
});
describe('and with cloud flag enabled', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: true,
isMation: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with cloud flag disabled', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with mation flag enabled', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(true);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: true,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with mation flag disabled', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isMation: false,
isCloud: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -1,9 +1,17 @@
import appConfig from '../../config/app.js';
import { hasValidLicense } from '../../helpers/license.ee.js';
import Config from '../../models/config.js';
const getConfig = async (_parent, params) => {
if (!(await hasValidLicense())) return {};
const defaultConfig = {
disableNotificationsPage: appConfig.disableNotificationsPage,
disableFavicon: appConfig.disableFavicon,
additionalDrawerLink: appConfig.additionalDrawerLink,
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
};
const configQuery = Config.query();
if (Array.isArray(params.keys)) {
@@ -18,7 +26,7 @@ const getConfig = async (_parent, params) => {
computedConfig[key] = value?.data;
return computedConfig;
}, {});
}, defaultConfig);
};
export default getConfig;

View File

@@ -2,6 +2,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import { createConfig } from '../../../test/factories/config';
import appConfig from '../../config/app';
import * as license from '../../helpers/license.ee';
describe('graphQL getConfig query', () => {
@@ -56,6 +57,10 @@ describe('graphQL getConfig query', () => {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
@@ -82,6 +87,48 @@ describe('graphQL getConfig query', () => {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
disableNotificationsPage: false,
disableFavicon: false,
additionalDrawerLink: undefined,
additionalDrawerLinkText: undefined,
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with different defaults', () => {
beforeEach(async () => {
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
true
);
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
'https://automatisch.io'
);
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
'Automatisch'
);
});
it('should return custom config', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getConfig: {
[configOne.key]: configOne.value.data,
[configTwo.key]: configTwo.value.data,
[configThree.key]: configThree.value.data,
disableNotificationsPage: true,
disableFavicon: true,
additionalDrawerLink: 'https://automatisch.io',
additionalDrawerLinkText: 'Automatisch',
},
},
};

View File

@@ -3,7 +3,6 @@ import getAppAuthClient from './queries/get-app-auth-client.ee.js';
import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
import getAppConfig from './queries/get-app-config.ee.js';
import getApps from './queries/get-apps.js';
import getAutomatischInfo from './queries/get-automatisch-info.js';
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
import getConfig from './queries/get-config.ee.js';
import getConnectedApps from './queries/get-connected-apps.js';
@@ -39,7 +38,6 @@ const queryResolvers = {
getAppAuthClients,
getAppConfig,
getApps,
getAutomatischInfo,
getBillingAndUsage,
getConfig,
getConnectedApps,

View File

@@ -40,7 +40,6 @@ type Query {
key: String!
parameters: JSONObject
): [SubstepArgument]
getAutomatischInfo: GetAutomatischInfo
getBillingAndUsage: GetBillingAndUsage
getCurrentUser: User
getConfig(keys: [String]): JSONObject
@@ -644,12 +643,6 @@ type AppHealth {
version: String
}
type GetAutomatischInfo {
isCloud: Boolean
isMation: Boolean
license: License
}
type License {
id: String
name: String

View File

@@ -20,7 +20,8 @@ export const isAuthenticated = async (_parent, _args, req) => {
.withGraphFetched({
role: true,
permissions: true,
});
})
.throwIfNotFound();
return true;
} catch (error) {
@@ -28,12 +29,19 @@ export const isAuthenticated = async (_parent, _args, req) => {
}
};
export const authenticateUser = async (request, response, next) => {
if (await isAuthenticated(null, null, request)) {
next();
} else {
return response.status(401).end();
}
};
const isAuthenticatedRule = rule()(isAuthenticated);
export const authenticationRules = {
Query: {
'*': isAuthenticatedRule,
getAutomatischInfo: allow,
getConfig: allow,
getNotifications: allow,
healthcheck: allow,

View File

@@ -1,11 +1,8 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { allow } from 'graphql-shield';
import jwt from 'jsonwebtoken';
import User from '../models/user.js';
import { isAuthenticated, authenticationRules } from './authentication.js';
vi.mock('jsonwebtoken');
vi.mock('../models/user.js');
import { createUser } from '../../test/factories/user.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
describe('isAuthenticated', () => {
it('should return false if no token is provided', async () => {
@@ -14,29 +11,26 @@ describe('isAuthenticated', () => {
});
it('should return false if token is invalid', async () => {
jwt.verify.mockImplementation(() => {
throw new Error('invalid token');
});
const req = { headers: { authorization: 'invalidToken' } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
it('should return true if token is valid', async () => {
jwt.verify.mockReturnValue({ userId: '123' });
it('should return true if token is valid and there is a user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
User.query.mockReturnValue({
findById: vi.fn().mockReturnValue({
leftJoinRelated: vi.fn().mockReturnThis(),
withGraphFetched: vi
.fn()
.mockResolvedValue({ id: '123', role: {}, permissions: {} }),
}),
});
const req = { headers: { authorization: 'validToken' } };
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(true);
});
it('should return false if token is valid and but there is no user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
await user.$query().delete();
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(false);
});
});
describe('authentication rules', () => {

View File

@@ -0,0 +1,49 @@
const authorizationList = {
'GET /api/v1/users/:userId': {
action: 'read',
subject: 'User',
},
'GET /api/v1/users/': {
action: 'read',
subject: 'User',
},
'GET /api/v1/flows/:flowId': {
action: 'read',
subject: 'Flow',
},
'GET /api/v1/executions/:executionId': {
action: 'read',
subject: 'Execution',
},
'GET /api/v1/executions/': {
action: 'read',
subject: 'Execution',
},
'GET /api/v1/executions/:executionId/execution-steps': {
action: 'read',
subject: 'Execution',
},
};
export const authorizeUser = async (request, response, next) => {
const currentRoute =
request.method + ' ' + request.baseUrl + request.route.path;
const currentRouteRule = authorizationList[currentRoute];
try {
request.currentUser.can(currentRouteRule.action, currentRouteRule.subject);
next();
} catch (error) {
return response.status(403).end();
}
};
export const authorizeAdmin = async (request, response, next) => {
const role = await request.currentUser.$relatedQuery('role');
if (role?.isAdmin) {
next();
} else {
return response.status(403).end();
}
};

View File

@@ -0,0 +1,11 @@
import appConfig from '../config/app.js';
export const checkIsCloud = async (request, response, next) => {
if (appConfig.isCloud) {
next();
} else {
return response.status(404).end();
}
};
export default checkIsCloud;

View File

@@ -0,0 +1,9 @@
import { hasValidLicense } from './license.ee.js';
export const checkIsEnterprise = async (request, response, next) => {
if (await hasValidLicense()) {
next();
} else {
return response.status(404).end();
}
};

View File

@@ -1,6 +1,9 @@
import * as path from 'path';
import * as fs from 'fs';
import * as handlebars from 'handlebars';
import path from 'path';
import fs from 'fs';
import handlebars from 'handlebars';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compileEmail = (emailPath, replacements = {}) => {
const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`);

View File

@@ -1,14 +1,45 @@
import logger from './logger.js';
import objection from 'objection';
import * as Sentry from './sentry.ee.js';
const { NotFoundError, DataError } = objection;
// Do not remove `next` argument as the function signature will not fit for an error handler middleware
// eslint-disable-next-line no-unused-vars
const errorHandler = (err, req, res, next) => {
if (err.message === 'Not Found') {
res.status(404).end();
} else {
logger.error(err.message + '\n' + err.stack);
res.status(err.statusCode || 500).send(err.message);
const errorHandler = (error, request, response, next) => {
if (error.message === 'Not Found' || error instanceof NotFoundError) {
response.status(404).end();
}
if (notFoundAppError(error)) {
response.status(404).end();
}
if (error instanceof DataError) {
response.status(400).end();
}
const statusCode = error.statusCode || 500;
logger.error(request.method + ' ' + request.url + ' ' + statusCode);
logger.error(error.stack);
Sentry.captureException(error, {
tags: { rest: true },
extra: {
url: request?.url,
method: request?.method,
params: request?.params,
},
});
response.status(statusCode).end();
};
const notFoundAppError = (error) => {
return (
error.message.includes('An application with the') ||
error.message.includes("key couldn't be found.")
);
};
export default errorHandler;

View File

@@ -1,6 +1,7 @@
import createHttpClient from './http-client/index.js';
import EarlyExitError from '../errors/early-exit.js';
import AlreadyProcessedError from '../errors/already-processed.js';
import Datastore from '../models/datastore.js';
const globalVariable = async (options) => {
const {
@@ -88,6 +89,43 @@ const globalVariable = async (options) => {
setActionItem: (actionItem) => {
$.actionOutput.data = actionItem;
},
datastore: {
get: async ({ key }) => {
const datastore = await Datastore.query().findOne({
key,
scope: 'flow',
scope_id: $.flow.id,
});
return {
key: datastore.key,
value: datastore.value,
[datastore.key]: datastore.value,
};
},
set: async ({ key, value }) => {
let datastore = await Datastore.query()
.where({ key, scope: 'flow', scope_id: $.flow.id })
.first();
if (datastore) {
await datastore.$query().patchAndFetch({ value: value });
} else {
datastore = await Datastore.query().insert({
key,
value,
scope: 'flow',
scopeId: $.flow.id,
});
}
return {
key: datastore.key,
value: datastore.value,
[datastore.key]: datastore.value,
};
},
},
};
if (request) {

View File

@@ -4,8 +4,8 @@ import appConfig from '../config/app.js';
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
http: 2,
info: 3,
debug: 4,
};

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