Compare commits

...

785 Commits
2.0.3 ... 2.1.4

Author SHA1 Message Date
Alejandro Alonso
689aab32c9 📎 Update changelog 2024-09-03 13:04:04 +02:00
Alejandro Alonso
c642f4afa2 📎 Update version.txt file 2024-09-03 12:52:36 +02:00
Alejandro
a62a083294 Merge pull request #5038 from penpot/palba-add-export-event
🎉 Add export event for telemetry
2024-09-03 12:51:06 +02:00
Pablo Alba
2a13c2ec00 🎉 Add export event for telemetry 2024-09-03 12:03:36 +02:00
Andrey Antukh
bf60bf1848 Merge pull request #5033 from penpot/superalex-revert-test-default-theme
Revert "🎉 Test A/B for starting with light theme"
2024-08-29 11:42:04 +02:00
Andrey Antukh
c581395df2 Merge pull request #5034 from penpot/superalex-track-copy-shared-link-event
 Track copy shared link event
2024-08-29 10:26:34 +02:00
Alejandro Alonso
8a44fb689a 🐛 Fix create share link name 2024-08-29 10:25:08 +02:00
Alejandro Alonso
9fd36526ef Track copy shared link event 2024-08-29 10:25:08 +02:00
Alejandro Alonso
78f4d9cc5d 🎉 Revert test A/B for starting with light theme
This reverts commit b0af94415f.
2024-08-28 13:00:43 +02:00
Alejandro
bd2a3e197a Merge pull request #5032 from penpot/niwinz-backports-1
🐛 Backport several bugfixes from develop
2024-08-28 12:49:16 +02:00
Andrey Antukh
cc98ac5853 🐛 Fix json encoding on zip encoding decoding 2024-08-28 10:43:47 +02:00
Andrey Antukh
05750c3b38 🐛 Add schema validation for color changes 2024-08-28 10:43:47 +02:00
Andrey Antukh
3ddecef5a7 Ensure plain map on path params in several functions 2024-08-28 10:31:22 +02:00
Andrey Antukh
7fd96a0533 🎉 Backport app.common.json namespace from develop 2024-08-28 10:30:11 +02:00
Andrey Antukh
f73e5446ab Merge pull request #5016 from penpot/superalex-bug-render-texts-without-position-data
🐛 Fix render of some texts without position data
2024-08-22 15:43:50 +02:00
Alejandro Alonso
df255b5a6f 🐛 Fix render of some texts without position data 2024-08-22 15:24:13 +02:00
Alejandro
de05521b57 Merge pull request #5001 from penpot/superalex-fix-deleted-fonts
🐛 Fix deleted fonts on file load
2024-08-20 13:13:48 +02:00
Alejandro Alonso
c86afca1d0 🐛 Fix deleted fonts on file load 2024-08-20 13:00:18 +02:00
Andrey Antukh
5617ca24b8 Merge pull request #5008 from penpot/superalex-improve-disabled-registry-flows
 Improve disabled registry flows
2024-08-20 10:11:20 +02:00
Alejandro Alonso
cd51f2f652 Improve disabled registry flows 2024-08-20 08:20:46 +02:00
Pablo Alba
00bb988ecc Merge pull request #5007 from penpot/superalex-a-b-remove-testing-signup-01
 Add a/b remove testing for signup image
2024-08-19 10:49:32 +02:00
Alejandro Alonso
5efc56eb5a Revert " Add a/b testing for signup image"
This reverts commit 5ac6f04857.
2024-08-19 10:22:01 +02:00
Alejandro
0ccae600bc Merge pull request #5000 from penpot/palba-default-light
🎉 Test A/B for starting with light theme
2024-08-19 08:57:10 +02:00
Pablo Alba
b0af94415f 🎉 Test A/B for starting with light theme 2024-08-19 08:20:31 +02:00
Andrey Antukh
380ead2ad6 Merge pull request #4994 from penpot/superalex-make-explicit-test-openldap-devenv-docker-ulimits
 Make explicit test-openldap devenv docker ulimits
2024-08-14 12:57:09 +02:00
Alejandro Alonso
3df45d697d Make explicit test-openldap devenv docker ulimits 2024-08-14 11:07:00 +02:00
Alejandro
efb70f0b97 Merge pull request #4985 from penpot/niwinz-hotfix-2
🐛 Disable ipv6 from docker nginx resolver.
2024-08-14 10:50:26 +02:00
Alejandro
924c1d60f9 Merge pull request #4990 from penpot/niwinz-hotfix-5
🐛 Update storage specs
2024-08-13 12:47:24 +02:00
Andrey Antukh
6b80f19e5f 🐛 Update storage specs 2024-08-13 12:21:16 +02:00
Alejandro
000d2c3935 Merge pull request #4989 from penpot/niwinz-hotfix-3
🐛 Backport  storage changes from develop
2024-08-13 11:26:49 +02:00
Andrey Antukh
91435bf372 🐛 Backport storage backend naming changes from develop
for properly handle backward comaptibility when two
versions are running over a single database
2024-08-13 11:14:38 +02:00
Alejandro
ea5c22c244 Merge pull request #4983 from penpot/niwinz-backports-1
🐛 Backport bugfixes from develop
2024-08-13 08:25:10 +02:00
Andrey Antukh
e07c1bba7a 🐛 Disable ipv6 from docker nginx resolver 2024-08-12 16:22:19 +02:00
Andrey Antukh
ec56a4149b 🐛 Fix unhandled exception on try to reuse registration token 2024-08-12 12:59:18 +02:00
Andrey Antukh
314742a563 Add :params prop to :not-found exception 2024-08-12 12:59:18 +02:00
Andrey Antukh
38c9e3e7cc 🐛 Fix error handling issue on login with oidc
happens when no oidc backend is configured on backend
2024-08-12 12:59:18 +02:00
Andrey Antukh
2533d0ebc0 Add naming consistency changes for file_data_fragment table 2024-08-08 07:42:17 +02:00
Andrey Antukh
a1c78683f5 📎 Update version.txt file 2024-08-07 10:57:51 +02:00
Alejandro
4fe77ca386 Merge pull request #4963 from penpot/niwinz-oidc-fixes-2
🐛 Fix OIDC issues and regressions
2024-08-06 12:02:02 +02:00
Andrey Antukh
ea7ad2aaa0 Add flag oidc-registration for switch on/off registration with oidc 2024-08-06 11:51:26 +02:00
Julian Schacher
0162451205 Revert "🐛 Set proper default tenant on exporter"
This reverts commit 86b2ce4dab.
2024-08-06 10:15:21 +02:00
Andrey Antukh
82ad240053 Merge pull request #4960 from penpot/superalex-fix-custom-smtp-port-with-ssl-enabled
🐛 Fix custom smtp port with ssl enabled
2024-08-05 13:01:17 +02:00
Alejandro Alonso
aa21430a5c 🐛 Fix custom smtp port with ssl enabled 2024-08-05 12:45:05 +02:00
Andrey Antukh
aa4368f97f Merge pull request #4959 from penpot/superalex-fix-user-language-validator
🐛 Fix user language validator
2024-08-05 12:35:32 +02:00
Alejandro Alonso
8eddcd64f1 🐛 Fix user language validator 2024-08-05 11:05:56 +02:00
Pablo Alba
fcf9444b1d Merge pull request #4948 from penpot/superalex-a-b-testing-signup-01
 Add a/b testing for signup image
2024-08-01 10:28:43 +02:00
Alejandro Alonso
5ac6f04857 Add a/b testing for signup image 2024-07-31 13:04:12 +02:00
Alejandro
b4d91b5a48 Merge pull request #4937 from penpot/niwinz-fix-email-complains-handling
 Add improvements to internal sns handler
2024-07-31 12:18:01 +02:00
Andrey Antukh
52425a993a 🐛 Check complaints reports in the same way as bounces are checked 2024-07-31 12:02:42 +02:00
Alejandro
e72e812166 Merge pull request #4943 from penpot/niwinz-temporal-log
 Add temporal log entry for profile insert conflict
2024-07-31 10:45:48 +02:00
Pablo Alba
65a00aa13f Merge pull request #4931 from penpot/hiru-fix-touched-detach
🐛 Fix touched groups when detaching with nested copies
2024-07-31 09:46:16 +02:00
Andrey Antukh
acc0623219 Add temporal log entry for profile insert conflict 2024-07-30 16:46:38 +02:00
Andrés Moya
990a948bcc 🐛 Fix touched groups when detaching with nested copies 2024-07-30 14:28:37 +02:00
Andrey Antukh
e0f2c4e0aa Add the ability to pass body to a log entry 2024-07-29 16:16:39 +02:00
Alejandro
4b6d3546e0 Merge pull request #4926 from penpot/niwinz-fix-error-report
🐛 Fix regression on error reporting
2024-07-26 08:27:37 +02:00
Alejandro
0bd3d80816 Merge pull request #4925 from penpot/niwinz-resolve-thumbnail-on-frontend
 Resolve file thumbnail on frontend instead of backend
2024-07-26 08:24:29 +02:00
Andrey Antukh
a261a57868 Prevent double error asignation on persistence error 2024-07-25 17:17:49 +02:00
Andrey Antukh
af389fe63a 🐛 Fix error reporting regression 2024-07-25 17:17:49 +02:00
Andrey Antukh
defcef3e59 Resolve file thumbnail on frontend instead of backend 2024-07-25 15:17:41 +02:00
Andrey Antukh
5ed49995f0 📎 Update changelog 2024-07-25 10:48:46 +02:00
Andrey Antukh
482901f315 Merge pull request #4922 from penpot/niwinz-staging-inet
 Ip Addr parsing and audit log context forwarding fixes
2024-07-24 23:16:27 +02:00
Andrey Antukh
cb26f341d5 Merge pull request #4921 from penpot/eva-fix-search-label
🐛  Fix search label in translations
2024-07-24 21:27:58 +02:00
Andrey Antukh
69b432eb0e 📎 Fix audit event type naming on oidc 2024-07-24 21:25:55 +02:00
Andrey Antukh
7df9ac5e4f 🐛 Fix audit context forwarding on explicit events 2024-07-24 21:25:55 +02:00
Andrey Antukh
343f3feed3 Improve ip-addr parsing 2024-07-24 21:07:11 +02:00
Andrey Antukh
08c8c47006 Merge pull request #4917 from penpot/superalex-fix-flag-email-verification
🐛 Fix flag email verification
2024-07-24 21:00:29 +02:00
Alejandro Alonso
a6d738f0db 🐛 Fix flag email verification 2024-07-24 20:46:49 +02:00
Eva Marco
1f80827d94 🐛 Fix search label in translations 2024-07-24 17:16:13 +02:00
Alejandro
51611fbc09 Merge pull request #4871 from penpot/palba-fix-collapse-groups
🐛 Fix can't collapse colors and typograhies groups when searching assets
2024-07-24 10:20:42 +02:00
Alejandro
c80b35e3ad Merge pull request #4916 from penpot/palba-consolidate-templates-order
 Consolidate templates new order and naming
2024-07-24 09:32:44 +02:00
Pablo Alba
166b8c806c 🐛 Fix can't collapse colors and typograhies groups when searching assets 2024-07-24 09:25:26 +02:00
Pablo Alba
81bd30a11b Consolidate templates new order and naming 2024-07-24 09:18:16 +02:00
Andrey Antukh
a457f8baf5 Merge pull request #4913 from penpot/superalex-fix-files-with-invalid-library-colors
🐛 Fix file with invalid library colors
2024-07-24 08:37:51 +02:00
Alejandro
3832377e04 Merge pull request #4902 from penpot/niwinz-notifications-improvements
 Improvements to notifications
2024-07-23 16:57:51 +02:00
Andrey Antukh
975efd80cb Add maintenance predefined notification 2024-07-23 16:56:36 +02:00
Alejandro Alonso
ecb0dc073d 🐛 Fix file with invalid library colors 2024-07-23 16:47:23 +02:00
Alejandro
3553b02c55 Merge pull request #4911 from penpot/main-tmp
 Add timestamp to release build hash
2024-07-23 14:49:10 +02:00
Andrey Antukh
434209af7d Add timestamp to release build hash 2024-07-23 14:07:46 +02:00
Alejandro
16ae057b4f Merge pull request #4880 from penpot/niwinz-oidc-fixes
 Several improvements to OIDC and other related code
2024-07-23 11:25:46 +02:00
Alejandro
2431cb40bf Merge pull request #4905 from penpot/niwinz-srepl-improvements
 Add proper deletion/restore helpers to srepl/main
2024-07-23 07:35:55 +02:00
Andrey Antukh
34293326b8 Add proper deletion/restore helpers to srepl/main 2024-07-22 15:17:11 +02:00
Andrey Antukh
57c60716f0 Add minor improvements to notifications-hub component 2024-07-22 13:14:58 +02:00
Andrey Antukh
7e50ab52b9 📎 Add minor improvement on notification message validation 2024-07-22 13:14:58 +02:00
Andrey Antukh
9e0fb44b3f 🐛 Remove unnecesary delay on hide notification banner 2024-07-22 13:14:58 +02:00
Andrey Antukh
142ae32256 Add better call signature for srepl/notify! fn helper 2024-07-22 13:14:58 +02:00
Andrey Antukh
085b933796 Update default buffers and resolver on devenv nginx config 2024-07-12 14:26:21 +02:00
Andrey Antukh
8dfc97d875 Add jwks loading on gitlab oidc provider 2024-07-12 14:03:48 +02:00
Andrey Antukh
3b48be808c 💄 Add minor naming change on calling logging on oidc ns 2024-07-12 13:39:32 +02:00
Andrey Antukh
a54160965d 🐛 Fix ip-addr parsing issue when it comes with port 2024-07-12 13:38:46 +02:00
Andrey Antukh
f4b59cc5a0 Normalize external-session-id parsing from request 2024-07-12 13:38:46 +02:00
Andrey Antukh
d52f2b18a5 Add context to OIDC errors 2024-07-12 13:38:46 +02:00
Eva Marco
e916c97491 Merge pull request #4867 from penpot/ladybenko-8348-fix-pencil-loader
🐛 Fix pencil loader in dashboard
2024-07-11 08:56:04 +02:00
Belén Albeza
cdabf0d6b9 🐛 Fix pencil loader in dashboard 2024-07-11 08:46:16 +02:00
Alejandro
ff43d43020 Merge pull request #4861 from penpot/niwinz-auth-oidc-reject-bugfix
🐛 Fix unexpected error when user explictly reject oidc auth
2024-07-09 14:11:59 +02:00
Andrey Antukh
0ae8cb4979 🐛 Do not report explicit user reject as error on oidc auth process 2024-07-09 13:57:48 +02:00
Andrey Antukh
fc1495fdd1 🐛 Fix unexpected error when user explictly reject oidc auth 2024-07-09 13:51:07 +02:00
Alejandro
74622919f6 Merge pull request #4860 from penpot/palba-testab-template-order
🎉 Test A/B for templates order and names
2024-07-09 12:48:37 +02:00
Pablo Alba
4b4b160ea8 🎉 Test A/B for templates order and names 2024-07-09 12:35:48 +02:00
Alejandro
2baab838e4 Merge pull request #4859 from penpot/hiru-fix-export-include-libs
Fix embed assets in unpublish and export
2024-07-09 12:13:29 +02:00
Andrés Moya
29d0499725 🐛 Fix embed assets in unpublish and export 2024-07-09 11:51:52 +02:00
Andrey Antukh
d99f4f62ea Merge pull request #4858 from penpot/superalex-update-release-name
 Update release 2.1.0 name
2024-07-09 10:22:33 +02:00
Alejandro Alonso
90f545ae6d Update release 2.1.0 name 2024-07-09 10:18:54 +02:00
Andrey Antukh
b295b79565 Merge pull request #4838 from penpot/superalex-fix-size-presets-dont-work-well
🐛 Size presets landscape doesn't work well
2024-07-09 08:26:07 +02:00
Belén Albeza
ffee6c63eb Merge pull request #4841 from penpot/superalex-release-notes-2-1
 Release notes for 2.1
2024-07-04 11:55:25 +02:00
Alejandro Alonso
0ec1bb7a22 Release notes for 2.1 2024-07-04 11:16:20 +02:00
Alejandro Alonso
2944860696 🐛 Size presets landscape doesn't work well 2024-07-03 13:35:14 +02:00
Andrey Antukh
8d6791105a Merge pull request #4833 from penpot/superalex-add-extra-info-to-create-team-response
 Add extra info to create team response
2024-07-02 15:37:42 +02:00
Alejandro Alonso
f051137098 Add extra info to create team response 2024-07-02 13:36:12 +02:00
Andrey Antukh
675a31796c Merge pull request #4823 from penpot/superalex-improve-audit-log-external-origin-events
 improve audit log external origin events
2024-07-02 08:29:33 +02:00
Alejandro Alonso
8dcd538bd2 Improve external origin events for audit_log 2024-07-02 08:14:22 +02:00
Andrey Antukh
384ad2e6fa Merge pull request #4813 from penpot/superalex-tracking-teams-and-invitations-in-posthog
 Add extra events info to track teams and invitations
2024-06-28 11:08:30 +02:00
Andrey Antukh
c090a11e5b Normalize audit event origin tracking on frontend and backend 2024-06-28 10:30:26 +02:00
Alejandro Alonso
f6b367cdca Add extra events info to track teams and invitations 2024-06-28 10:30:26 +02:00
Andrey Antukh
5b9d2663c0 Merge remote-tracking branch 'origin/main' into staging 2024-06-28 08:18:30 +02:00
Alejandro
5e5c105d92 Merge pull request #4817 from penpot/niwinz-hotfix-1
🐛 More fixes on account deletion process
2024-06-28 07:43:07 +02:00
Alejandro
9c2c2fec6a Merge pull request #4815 from penpot/niwinz-bugfix-2
🐛 Backport from main account deletion bugfixes
2024-06-28 07:35:53 +02:00
Andrey Antukh
56160cf64d 🐛 Fix error handling on verify-token page 2024-06-27 16:11:43 +02:00
Andrey Antukh
c45a105186 🐛 Set correct order of execution for logged-out event 2024-06-27 16:11:37 +02:00
Andrey Antukh
f364666d48 🐛 Fix error handling on verify-token page 2024-06-27 16:10:26 +02:00
Andrey Antukh
7facd69039 🐛 Set correct order of execution for logged-out event 2024-06-27 16:10:04 +02:00
Alejandro
b0fea30770 Merge pull request #4814 from penpot/niwinz-hotfix-1
🐛 Add missing logged-out event after account deletion
2024-06-27 14:04:16 +02:00
Andrey Antukh
17015c5353 🐛 Add missing logged-out event after account deletion 2024-06-27 14:00:52 +02:00
Andrey Antukh
ba721def26 Add srepl helpers for profile deletion handling 2024-06-27 14:00:52 +02:00
Andrey Antukh
f9af7f0f09 🐛 Make profile deletion follow the delete-object flow
This removes the need of the specific task for cleaning
orphan teams.
2024-06-27 14:00:52 +02:00
Andrey Antukh
56476acc19 🐛 Fix error handling on account deletion process 2024-06-27 13:56:39 +02:00
Andrey Antukh
67489c0bb9 🐛 Fix profile deletion issue with 1 participant 2024-06-27 13:56:39 +02:00
Andrey Antukh
272edec3c6 🐛 Add missing logged-out event after account deletion 2024-06-27 13:53:30 +02:00
Alejandro
78addf00b4 Merge pull request #4812 from penpot/niwinz-hotfix-1
🐛 Fix account deletion process issues
2024-06-27 13:02:55 +02:00
Andrey Antukh
2cddbc8a3d 🐛 Fix error handling on account deletion process 2024-06-27 12:16:51 +02:00
Andrey Antukh
7e44ae62a2 🐛 Fix profile deletion issue with 1 participant 2024-06-27 12:16:51 +02:00
Alejandro Alonso
40b43c6c5b 🐛 Fix lost image colors in library when export/import using zip format 2024-06-27 09:54:02 +02:00
Alejandro Alonso
034170afac 🐛 Export/import of images doesn't work for .zip format 2024-06-27 09:54:02 +02:00
Alejandro
46a9535f35 Merge pull request #4810 from penpot/niwinz-bugfix-11
🐛 Add migration for fix path shapes with invalid curve segment params
2024-06-27 08:02:00 +02:00
Alejandro
ff4d66ec75 Merge pull request #4806 from penpot/niwinz-bugfix-10
🐛 Properly report erorrs on importation
2024-06-27 07:37:58 +02:00
Alejandro
e3f4258252 Merge pull request #4796 from penpot/palba-bugfixing-007
🐛 Bugfixing
2024-06-27 07:35:42 +02:00
Pablo Alba
950d6195f6 🐛 Fix import file message does not detect 0 as error 2024-06-27 07:35:30 +02:00
Pablo Alba
a56e7e383f 🐛 Fix 'Detach instance' shortcut is not working 2024-06-27 07:35:29 +02:00
Pablo Alba
f786aff3fc 🐛 Fix can't collapse groups when searching in the assets tab 2024-06-27 07:35:28 +02:00
Andrey Antukh
f393ce9273 🐛 Add migration for fix path shapes with invalid curve segment params 2024-06-26 19:46:29 +02:00
Andrey Antukh
16a0e1f578 Merge pull request #4805 from penpot/superalex-fix-selected-colors-do-not-appear-in-color-palette
🐛 Selected colors do not appear in the color palette
2024-06-26 15:04:40 +02:00
Andrey Antukh
1c1929ed32 📎 Add helpers for process deletion cascade of old objects 2024-06-26 15:00:47 +02:00
Andrey Antukh
37d0cec265 🐛 Properly report importation errors 2024-06-26 14:59:38 +02:00
Alejandro Alonso
9ca1535a65 🐛 Selected colors do not appear in the color palette 2024-06-26 12:57:53 +02:00
Andrey Antukh
8a63089773 Merge pull request #4801 from penpot/superalex-fix-color-library-loses-association
🐛 Color library loses association with shapes when exporting/impo…
2024-06-26 11:28:45 +02:00
Alejandro Alonso
58466e6488 🐛 Color library loses association with shapes when exporting/importing the document 2024-06-26 10:40:30 +02:00
Alejandro
3bb2573dec Merge pull request #4795 from penpot/niwinz-bugfix-8
🐛 Bugfixes
2024-06-26 07:41:22 +02:00
Andrey Antukh
3a30e6b29e Merge pull request #4798 from penpot/superalex-fix-alt-duplicate-shapes
🐛 Fix duplicate shapes with alt key
2024-06-25 16:32:24 +02:00
Alejandro Alonso
686f59b0de 🐛 Fix duplicate shapes with alt key 2024-06-25 15:53:17 +02:00
Andrey Antukh
fc30e81072 🐛 Make component changes watcher look on local commits only 2024-06-25 15:47:24 +02:00
Andrés Moya
798970a923 🐛 Rework svg import of deleted components 2024-06-25 15:23:52 +02:00
Andrés Moya
6aeb87a122 🐛 Rework svg export of deleted components 2024-06-25 15:23:52 +02:00
Andrey Antukh
ec4260830c ♻️ Add API consistency fixes for task calling
Also adds a helper for calling tasks synchronously
2024-06-25 13:24:17 +02:00
Andrey Antukh
aa1cf3e03a Add some redundancy to delete_object task 2024-06-25 13:06:44 +02:00
Alejandro
3b5b81b08f Merge pull request #4758 from penpot/superalex-refactor-data-test-attribute
 Refactor data-test attribute
2024-06-25 10:49:03 +02:00
Aitor Moreno
f3333336f0 Merge pull request #4787 from penpot/niwinz-bugfix-8
🐛 Fix thumbnails related issues
2024-06-21 13:23:00 +02:00
Andrey Antukh
b25a9f8626 🐛 Return back to use blob uris for transient thumbnails 2024-06-21 13:12:44 +02:00
Andrey Antukh
7b8d127583 🐛 Fix incorrect frame change detection on thumbnails generation 2024-06-21 13:12:44 +02:00
Alejandro
0b6c84f551 Merge pull request #4786 from penpot/niwinz-bugfix-8
 Changes on metrics
2024-06-21 09:42:30 +02:00
Andrey Antukh
31818ec365 Replace other summary metrics with histogram 2024-06-21 09:32:46 +02:00
Andrey Antukh
f249945dff Include climit into the RPC handler metrics 2024-06-21 09:28:28 +02:00
Andrey Antukh
56556a3f3b Make the penpot_rpc_climit_timing metric as histogram 2024-06-21 09:27:11 +02:00
Alejandro Alonso
a59f31ebe5 Refactor data-test attribute 2024-06-21 09:14:09 +02:00
Alejandro
4d0bfb1003 Merge pull request #4785 from penpot/niwinz-bugfix-4
🐛 Remove climit setup for delete-file-object-thumbnail RPC method
2024-06-20 18:47:27 +02:00
Andrey Antukh
afb23bcebe Merge pull request #4784 from penpot/azazeln28-refactor-component-thumbnails-as-configurable
♻️ Refactor component thumbnails as configurable
2024-06-20 17:58:45 +02:00
AzazelN28
7b1bda3a49 ♻️ Refactor component thumbnails as configurable 2024-06-20 17:40:41 +02:00
Andrey Antukh
8e9e967f82 🐛 Remove climit setup for delete-file-object-thumbnail RPC method 2024-06-20 16:49:19 +02:00
Andrey Antukh
d1d384acaf Merge pull request #4782 from penpot/eva-fix-translation-error
🐛 Fix minor onboarding errors
2024-06-20 16:10:34 +02:00
Eva Marco
1845e759d3 🐛 Fix a small error on translations and on step number for event 2024-06-20 15:06:19 +02:00
Andrey Antukh
955dadc301 Merge pull request #4780 from penpot/superalex-removing-underline-and-strikethrough-affects-the-previous-text-object
🐛 Removing Underline and Strikethrough Affects the Previous Text …
2024-06-20 12:08:30 +02:00
Alejandro
446edee7c6 Merge pull request #4779 from penpot/ladybenko-7937-login-visual-test
Add temporary visual regression tests for the login page
2024-06-20 11:58:45 +02:00
Alejandro Alonso
c35a818d4c 🐛 Removing Underline and Strikethrough Affects the Previous Text Object 2024-06-20 11:49:10 +02:00
Andrey Antukh
e5cc262644 Merge pull request #4778 from penpot/superalex-fix-crash-when-new-colorpicker-while-image-upload
🐛 Penpot crashes when a new colorpicker is created while uploadin…
2024-06-20 10:36:44 +02:00
Belén Albeza
84350ea71c 📎 Remove dashboard visual tests that depend on specific config flags that are not on by default 2024-06-20 09:49:41 +02:00
Belén Albeza
d4eea973b6 📎 Add temporary visual regression test for login form 2024-06-20 09:49:41 +02:00
Belén Albeza
6d82f41e43 ♻️ Refactor login POM logged out user intercept 2024-06-20 09:49:37 +02:00
Alejandro Alonso
4884a11102 🐛 Penpot crashes when a new colorpicker is created while uploading an image to another instance 2024-06-20 09:22:39 +02:00
Alejandro
3e6b34c563 Merge pull request #4772 from penpot/niwinz-audit-enhancements-2
 Minor enhancements to audit events related to clone-template
2024-06-20 07:05:37 +02:00
Andrey Antukh
6a253871b0 Improve internal handling of external-session-id 2024-06-19 16:15:48 +02:00
Andrey Antukh
8da153f604 Emit create-file action events on clone-template 2024-06-19 14:58:04 +02:00
Andrey Antukh
d1e9ea372a Assign external session ID to params on RPC main handler 2024-06-19 14:57:17 +02:00
Alejandro Alonso
b668fed1c8 🐛 Export shapes that are rotated act a bit strange when reimported 2024-06-19 13:04:15 +02:00
Andrey Antukh
d527184dfc ♻️ Try to fix recursive update-file ops on text edition
on concurrent multi-user sessions
2024-06-19 11:48:08 +02:00
Andrey Antukh
a2cc7764fb Improve how indexes are updated on concurrent sessions 2024-06-19 11:48:08 +02:00
Alejandro
d977b4e27c Merge pull request #4768 from penpot/eva-visual-testing-viewer
  Add visual testing to viewer
2024-06-19 10:43:46 +02:00
Eva Marco
f3193a1984 Add visual testing to viewer 2024-06-19 10:01:31 +02:00
Alejandro
5e7180b054 Merge pull request #4767 from penpot/ladybenko-ds-visual-tests-workspace-stg
📎 Add (temporary) visual regression tests for the Workspace
2024-06-19 09:55:18 +02:00
Belén Albeza
7f4f54e3fd 📎 Add (temporary) visual regression tests for the Workspace 2024-06-19 09:44:03 +02:00
Alejandro
8a0aa9cd7f Merge pull request #4760 from penpot/niwinz-fix-json-truncation
🐛 Fix json encoding output truncation
2024-06-19 08:10:48 +02:00
Alejandro Alonso
bc8435dc5b Merge remote-tracking branch 'origin/main' into staging 2024-06-19 08:01:08 +02:00
Andrey Antukh
3363793d64 🐛 Fix json encoding truncation issue 2024-06-19 07:59:28 +02:00
Alejandro
bb63375933 Merge pull request #4764 from penpot/niwinz-audit-enhancements
 Forward external session id to backend
2024-06-19 07:56:11 +02:00
Andrey Antukh
06bab212b5 🐛 Set correct order for http middlewares 2024-06-19 07:48:12 +02:00
Andrey Antukh
504f833a53 🐛 Fix global error handler incorrect body encoding 2024-06-19 07:48:12 +02:00
Andrey Antukh
00b4013385 Forward external session id to backend 2024-06-19 07:45:27 +02:00
Andrés Moya
d039df6b73 Add tests for detach with swapped copies 2024-06-18 23:39:19 +02:00
Alejandro Alonso
3e657874d7 🐛 Create resources/public/css/ on run-devenv 2024-06-18 14:22:48 +02:00
Alejandro
52a49a7359 Merge pull request #4733 from penpot/eva-visual-testing
 Add visual testing to dashboard
2024-06-18 13:16:07 +02:00
Eva Marco
4e770fd326 Add visual testing to dashboard 2024-06-18 10:31:39 +02:00
Aitor Moreno
6023ab1c07 Merge pull request #4746 from penpot/superalex-remove-a-b-testing-for-onboarding-questions
 Remove a/b testing code for onboarding questions
2024-06-18 09:59:18 +02:00
Alejandro
47fcac1c00 Merge pull request #4757 from penpot/niwinz-enhancements-3
 Fix a performance regression with file validation with some features
2024-06-18 09:32:36 +02:00
Andrey Antukh
293b460cab Fix a performance regression with file validation with some features
The feature fdata/pointer-map tracking mechanism interacts pretty bad
with possible local mutations on the validation subsystem. The fix
consist on disabling the tracking mechanism on the validation.
2024-06-17 15:56:06 +02:00
Pablo Alba
4e6c1857dd 🐛 Add validator and repair for duplicated slots on deleted components 2024-06-17 15:15:40 +02:00
Pablo Alba
4546e98dc6 Revert "Revert "🐛 Add validator and repair for duplicated slots""
This reverts commit e0906be6e7.
2024-06-17 15:15:40 +02:00
Andrés Moya
e0906be6e7 Revert "🐛 Add validator and repair for duplicated slots"
This reverts commit db2ba42b14.
2024-06-17 09:56:27 +02:00
Pablo Alba
db2ba42b14 🐛 Add validator and repair for duplicated slots 2024-06-17 09:23:50 +02:00
Alejandro
1d28be07d0 Merge pull request #4747 from penpot/niwinz-bugfix-2
🔥 Fix many inconsistencies on the onboarding translation strings
2024-06-17 06:58:23 +02:00
Pablo Alba
cc1b51cb2c Add fix to locate files with duplicated slots 2024-06-14 13:57:30 +02:00
Aitor Moreno
22ede6b08e Merge pull request #4731 from penpot/niwinz-bugfix-6
🐛 Fix many race conditions on thumbnail generation process
2024-06-14 13:15:10 +02:00
Andrey Antukh
a3ac22f781 🔥 Fix many inconsistencies on the onboarding translation strings
Mainly related to the quiestions modal
2024-06-14 11:55:57 +02:00
Pablo Alba
2d9c5d1ac4 Merge pull request #4742 from penpot/hiru-fix-detach-swapped-nested
Fix detach swapped nested
2024-06-14 11:04:41 +02:00
Andrés Moya
1ac6b556b0 🐛 Fix detach when a nested copy has been swapped 2024-06-14 10:45:42 +02:00
Andrés Moya
b5477c4e30 🔧 Improve debug utility 2024-06-14 10:45:20 +02:00
Alejandro
7a05580df3 Merge pull request #4745 from penpot/niwinz-bugfix-9
🐛 Clear persistence status on server error
2024-06-14 08:10:11 +02:00
Alejandro Alonso
2c506fc721 🐛 Fix SVG attrs are not handled correctly when exporting/importing .zip 2024-06-14 07:58:18 +02:00
Alejandro Alonso
043769c255 Remove a/b testing code for onboarding questions 2024-06-14 07:45:59 +02:00
Alejandro
68741bb56f Merge pull request #4744 from penpot/niwinz-bugfix-8
 Fix translation strings inconsistencies on the onboarding questions modals
2024-06-14 07:00:56 +02:00
Alejandro Alonso
0d23f4ab5d 🐛 Workspace-palette items stay hidden when opening with keyboard-shortcut 2024-06-14 00:06:08 +02:00
Belén Albeza
599bc8dbe7 📎 Remove unused playwright config file in root 2024-06-14 00:05:13 +02:00
Andrey Antukh
f463a1989f 🐛 Clear persistence status on server error
This avoids the problem of permanently keeping on the persistence
queue a problematic change.
2024-06-14 00:01:59 +02:00
Andrey Antukh
a9f5b1559f Make translations consistent on the onboarding questions modal 2024-06-13 23:51:23 +02:00
Andrey Antukh
b4c9528603 Add missing frontend translations to devenv watcher 2024-06-13 18:01:46 +02:00
Eva Marco
a84c2e1138 Merge pull request #4741 from penpot/superalex-fix-persistence-status-widget-color
🐛 Fix persistence status widget lost saving status color
2024-06-13 16:11:53 +02:00
Alejandro Alonso
1ad2171933 🐛 Fix persistence status widget lost saving status color 2024-06-13 14:47:42 +02:00
Eva Marco
195305e4c8 Merge pull request #4737 from penpot/niwinz-bugfix-7
🐛 Fix onboarding step1 values
2024-06-13 13:05:05 +02:00
Andrey Antukh
535246f1c6 🐛 Fix onboarding step1 values 2024-06-13 12:45:36 +02:00
Alejandro
1a7cdfbf56 Merge pull request #4734 from penpot/superalex-cherry-picking
 Cherry-pick
2024-06-13 10:58:38 +02:00
alonso.torres
d48e486668 🐛 Fix problem moving layout to frame 2024-06-13 10:47:22 +02:00
Alejandro Alonso
c15c3b14ee Add e2e tests for fix color palette default library 2024-06-13 10:47:22 +02:00
Alejandro
5c1e7adf7e Merge pull request #4724 from penpot/palba-fix-open-overlay-self
🐛 Fix open overlay relative to a frame
2024-06-13 06:34:14 +02:00
Pablo Alba
67e1081f11 🐛 Fix open overlay relative to a frame 2024-06-13 06:33:48 +02:00
Alejandro
dd69f8f29b Merge pull request #4727 from penpot/eva-fix-scroll-select
🐛 Fix move scrollbar create a selection rectangle
2024-06-13 06:28:40 +02:00
Eva Marco
232cfea709 🐛 Fix move scrollbar create a selection rectangle 2024-06-13 06:28:26 +02:00
Alejandro
09a671cffa Merge pull request #4730 from penpot/azazeln28-fix-toolbar-keeps-hiding-clicking-outside
🐛 Fix toolbar keeps hiding clicking outside
2024-06-12 17:07:56 +02:00
AzazelN28
b4004af255 🐛 Fix toolbar keeps hiding when clicking outside 2024-06-12 17:07:35 +02:00
Aitor Moreno
a150e1c2e5 Merge pull request #4728 from penpot/superalex-toolbar-keeps-toggling-on-and-off
🐛 Toolbar keeps toggling on and off on spacebar press
2024-06-12 17:05:30 +02:00
Alejandro Alonso
740a872231 🐛 Toolbar keeps toggling on and off on spacebar press 2024-06-12 16:41:06 +02:00
Andrey Antukh
ec7aa64c62 Disable http cache from get-file-object-thumbnails RPC method 2024-06-12 16:13:34 +02:00
Andrey Antukh
c1463ebd12 🐛 Fix many race conditions on thumbnail generation process 2024-06-12 16:04:08 +02:00
Alejandro Alonso
bbd9207191 Improve email verify threshold 2024-06-12 13:56:19 +02:00
Alejandro
82a5754923 Merge pull request #4729 from penpot/hiru-fix-detach-slots
🐛 Fix swap slots when detaching a copy with subcopies
2024-06-12 13:54:31 +02:00
Andrés Moya
03aa0817f7 🐛 Fix swap slots when detaching a copy with subcopies 2024-06-12 13:17:17 +02:00
Alejandro Alonso
058a72b817 🐛 Fix internal error when I set up a stroke for some objects without and with stroke 2024-06-12 13:00:39 +02:00
Alejandro Alonso
9f7a002a78 🐛 Fix misaligned input on comments 2024-06-12 10:20:18 +02:00
Alejandro Alonso
a861691ffd 🐛 Fix change color on imported svg also changes the stroke alignment 2024-06-12 10:08:30 +02:00
Alejandro Alonso
a5ba9d113f Improve create group tests 2024-06-12 10:07:34 +02:00
Alejandro
132908c224 Merge pull request #4723 from penpot/ladybenko-7466-layers-sidebar
Fix layers tree not expanding to the bottom edge
2024-06-12 09:36:47 +02:00
Belén Albeza
f417445f31 Refactor WorkspacePage so it has more semantic locators 2024-06-12 09:23:30 +02:00
Belén Albeza
7719cd8d0b ♻️ Move libraries test to sidebar spec file 2024-06-12 09:23:30 +02:00
Belén Albeza
04f341ce1d 🐛 Fix layers tree not expanding towards the bottom edge 2024-06-12 09:23:30 +02:00
Alejandro
00f7ea2b56 Merge pull request #4720 from penpot/niwinz-bugfix-5
 Improve how email is passed on post-signup page
2024-06-12 09:00:29 +02:00
Alejandro
f2cc363caa Merge pull request #4698 from penpot/eva-new-onboarding-integration-tests
 Add integration tests to new onboarding process
2024-06-12 08:57:17 +02:00
Eva Marco
a90baa91c7 Add integration test to the onboarding process 2024-06-11 16:35:05 +02:00
Andrey Antukh
4057084981 Improve how email is passed on post-signup page 2024-06-11 14:12:07 +02:00
Alejandro
8f42be1096 Merge pull request #4710 from penpot/eva-fix-guides
🐛 Fix move guides with board
2024-06-11 14:06:34 +02:00
Alejandro
27d8d8649e Merge pull request #4718 from penpot/niwinz-bugfix-4
 Add minor improvements on how we handle logout
2024-06-11 14:02:19 +02:00
Alejandro
136b115006 Merge pull request #4717 from penpot/niwinz-bugfix-3
🐛 Fix issue with annotation menu rerendering
2024-06-11 13:55:28 +02:00
Eva Marco
0e0ceaa9bf 🐛 Fix move guides with board 2024-06-11 12:54:43 +02:00
Andrey Antukh
eba6f51953 📎 Update THANKYOU.md file 2024-06-11 11:03:54 +02:00
Andrey Antukh
c14f6aecf3 📎 Update THANKYOU.md file 2024-06-11 11:03:54 +02:00
Andrey Antukh
6896a4e9f0 Add minor improvements on how logout is handled
The profile-id is now required on the body of logout request
2024-06-11 11:03:54 +02:00
Pablo Alba
8825e9f80b 🐛 Fix show in view mode and interactions workflow 2024-06-11 11:01:24 +02:00
Alejandro Alonso
694e71c3fc Improve CHANGES.md format 2024-06-11 10:54:11 +02:00
Alejandro Alonso
46764a1e6e 🐛 Fix export boards loses opacity 2024-06-11 10:54:11 +02:00
Alejandro Alonso
3d7f399a50 🐛 Fix missing scroll in comments 2024-06-11 10:38:41 +02:00
Alejandro Alonso
c5bf2a775e 🐛 Fix group automatically selected after creation 2024-06-11 10:38:03 +02:00
Andrey Antukh
2d527b2caf 🐛 Fix issue with annotation menu rerendering 2024-06-11 10:29:26 +02:00
Alejandro
38fa5be862 Merge pull request #4653 from penpot/eva-new-onboarding-modals
 Update onboarding modals
2024-06-11 10:19:26 +02:00
Andrey Antukh
93ac80f217 Improve onboarding initialization 2024-06-11 09:47:03 +02:00
Alejandro
88e2e11634 Merge pull request #4713 from penpot/hiru-bugs-export-zip
Fix bugs in the export to zip file
2024-06-11 07:33:01 +02:00
Alejandro
4db189f90d Merge pull request #4714 from penpot/niwinz-persistence-bugfix-1
🐛 Fix race condition between shape modifiation and persistence
2024-06-11 07:26:02 +02:00
Andrey Antukh
d35569dc55 Simplify transducer definition for proces redo changes 2024-06-10 18:59:39 +02:00
Andrey Antukh
e4e56828f6 💄 Fix internal naming for make code more self-explanatory 2024-06-10 18:56:59 +02:00
Andrey Antukh
3c1086dfcc 🐛 Fix race condition between shape modifiation and persistence 2024-06-10 18:51:48 +02:00
Andrey Antukh
b635427f91 🐛 Fix incorrect order of update-index operations 2024-06-10 17:50:46 +02:00
Andrés Moya
9a4c45c8a3 🐛 Fix export touched attributes 2024-06-10 17:09:46 +02:00
Andrey Antukh
273a5f7a0a Improve state management on onboarding team modal 2024-06-10 14:36:25 +02:00
Andrey Antukh
0dda893d73 Improve state management on onboarding newsletter modal 2024-06-10 14:36:25 +02:00
Andrey Antukh
2629fa0662 Improve state management on onboarding questions modal 2024-06-10 14:36:24 +02:00
Eva Marco
fda6deaa4f Update onboarding modals 2024-06-10 14:36:24 +02:00
Andrey Antukh
55ce9bef49 🐛 Fix incorrect error merging mechanism on form validation 2024-06-10 14:36:24 +02:00
Andrey Antukh
e7a8c25883 🐛 Fix touched handling on multi-input form component 2024-06-10 14:36:24 +02:00
Andrey Antukh
88b65cd864 🐛 Fix incorrect compilation of css-case macro in a small corne case 2024-06-10 14:36:24 +02:00
Andrés Moya
257dab4775 🐛 Fix export hidden shapes 2024-06-10 11:09:14 +02:00
Alejandro Alonso
edfc47d3de Add e2e tests for fix color palette default library 2024-06-10 09:29:58 +02:00
Alejandro
d8184fb756 Merge pull request #4703 from penpot/palba-bugfixing-009
Palba bugfixing 009
2024-06-10 06:45:47 +02:00
Pablo Alba
96993a6ebd 🐛 Fix "Share prototypes" modal remains open 2024-06-10 06:45:29 +02:00
Pablo Alba
1f7b5a0f7f 🐛 Fix black line is displaying after show main 2024-06-10 06:45:27 +02:00
Alejandro
a553de3c98 Merge pull request #4699 from penpot/ladybenko-7805-viewer-zoom
Fix zoom setting not updating the URL in viewer
2024-06-10 06:40:02 +02:00
Pablo Alba
d9618c6213 Merge pull request #4700 from penpot/alotor-bugfix
Alotor bugfix
2024-06-07 16:33:41 +02:00
alonso.torres
3ad91d1c9d 🐛 Fix problem with flex layout fit to content not positioning correctly children 2024-06-07 15:25:57 +02:00
alonso.torres
2c21a049e1 🐛 Fix problem with moving+selection not working properly 2024-06-07 14:04:03 +02:00
Belén Albeza
724bc24063 Add test for #7805 2024-06-07 13:44:54 +02:00
Belén Albeza
96b7fb7f12 🐛 Fix viewer querystring not being updated with zoom type 2024-06-07 13:15:20 +02:00
Alejandro
d29215a282 Merge pull request #4675 from penpot/niwinz-enhancements-1
 Add backward compatibility fixes for email whitelisting
2024-06-07 13:04:34 +02:00
Andrey Antukh
ae90d59b43 Remove spec usage o teams rpc ns 2024-06-07 10:30:14 +02:00
Andrey Antukh
046ef7eb6e 🔥 Replace clojure.spec with simple assert on tokens ns 2024-06-07 10:30:14 +02:00
Andrey Antukh
25265cec70 Remove claims from token validation error report 2024-06-07 10:30:14 +02:00
Andrey Antukh
40f39681ad Add backward compatibility fixes for email whitelisting 2024-06-07 10:30:14 +02:00
Alejandro
70c9314f7f Merge pull request #4685 from penpot/palba-fix-validation-error-coping-main-frame
🐛 Fix verification error on coping main component with frame over a copy
2024-06-07 09:59:39 +02:00
Alejandro
7bf4305269 Merge pull request #4682 from penpot/ladybenko-7686-mixed-constraints
Fix constraints dropdown not showing "Mixed"
2024-06-07 07:44:04 +02:00
Alejandro
49879caf2c Merge pull request #4691 from penpot/alotor-fix-components
Alotor fix components
2024-06-07 07:40:26 +02:00
alonso.torres
33bf8892c0 🐛 Fix problem when creating multiple components 2024-06-06 17:17:01 +02:00
Belén Albeza
4efab3e8c8 Add an integration test for #7686 (constraints dropdown) 2024-06-06 15:30:21 +02:00
Belén Albeza
c8ff8fcbf7 🐛 Fix multiple constraints dropdown not showing 'mixed' 2024-06-06 15:29:28 +02:00
Belén Albeza
da40d662be Merge pull request #4684 from penpot/superalex-speed-up-e2e-testing
 Speed up e2e testing
2024-06-06 15:13:07 +02:00
Alejandro
1a312b08b7 Merge pull request #4676 from penpot/ladybenko-7988-fix-viewer-logo
🐛 Fix link in penpot logo spanning way below it (in viewer)
2024-06-06 13:06:47 +02:00
Alejandro Alonso
08c3901134 Speed up e2e testing 2024-06-06 13:04:27 +02:00
Andrés Moya
bc3aac1597 📖 Update change log 2024-06-06 10:29:11 +02:00
Andrés Moya
41024728ae 🐛 Fix importing a component from a zip file in v2 2024-06-06 10:29:11 +02:00
Pablo Alba
91fd8c4f81 🐛 Fix verror on coping main component with frame over a copy 2024-06-06 10:24:39 +02:00
Belén Albeza
67ababf1ed 🐛 Fix link in penpot logo spanning way below it (in viewer) 2024-06-06 09:30:52 +02:00
Belén Albeza
2aa1b97769 Merge pull request #4678 from penpot/superalex-bugfixing
🐛 Bugfixing
2024-06-05 14:11:36 +02:00
Anonymous
4e0c6f847f 🌐 Add translations for: Yoruba.
Currently translated at 87.7% (1220 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-06-05 12:42:53 +02:00
Anonymous
ee2f4c11c0 🌐 Add translations for: Igbo.
Currently translated at 38.9% (542 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2024-06-05 12:42:53 +02:00
Anonymous
5814559880 🌐 Add translations for: Hausa.
Currently translated at 92.5% (1287 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2024-06-05 12:42:53 +02:00
Anonymous
2569282d91 🌐 Add translations for: Korean.
Currently translated at 16.0% (223 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2024-06-05 12:42:53 +02:00
Anonymous
8f5a35f5f9 🌐 Add translations for: German.
Currently translated at 99.1% (1378 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-06-05 12:42:53 +02:00
Anonymous
7971bcf7d9 🌐 Add translations for: Chinese (Simplified).
Currently translated at 98.5% (1370 of 1390 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2024-06-05 12:42:52 +02:00
Andrey Antukh
64a5c68cc9 Merge branch 'translations' into develop 2024-06-05 12:41:12 +02:00
Andrey Antukh
028812dc10 Merge remote-tracking branch 'weblate/develop' into translations 2024-06-05 12:40:07 +02:00
Alejandro Alonso
fc7294c10d Make CHANGES format uniform 2024-06-05 09:37:53 +02:00
Alejandro Alonso
697b6776ba 🐛 Fix color palette default library 2024-06-05 06:38:22 +02:00
alonso.torres
3a40c7f59c ⬆️ Update plugin runtime 2024-06-04 16:44:24 +02:00
alonso.torres
3e2ccbc85f Add infor for users 2024-06-04 16:44:24 +02:00
alonso.torres
98c550b20e Update content of path shapes in plugins 2024-06-04 16:44:24 +02:00
alonso.torres
bf66e12075 Allow masks, booleans, and some path read only 2024-06-04 16:44:24 +02:00
alonso.torres
55c27f140a Create paths, booleans and ellipses through plugins api 2024-06-04 16:44:24 +02:00
alonso.torres
e4e537b960 Add support for components in plugins 2024-06-04 16:44:24 +02:00
alonso.torres
3209511557 Add support for colors and typographies 2024-06-04 16:44:24 +02:00
Alejandro
d4a0541926 Merge pull request #4670 from penpot/niwinz-email-shortening
 Abbreviate profile name on emails
2024-06-04 13:01:35 +02:00
Pablo Alba
5872bf024c Add test for remove swap slot on detach parent 2024-06-04 12:24:31 +02:00
Pablo Alba
5b88589157 Merge remote-tracking branch 'origin/staging' into develop 2024-06-04 11:57:59 +02:00
Andrey Antukh
b6b6822c31 Abbreviate profile name on emails 2024-06-04 11:32:28 +02:00
Alejandro
622d1faffc Merge pull request #4664 from penpot/niwinz-email-blacklist
 Add email blacklist mechanism
2024-06-04 11:18:28 +02:00
Andrey Antukh
5aa62ef1dd Add email blacklist mechanism 2024-06-04 10:45:55 +02:00
Alejandro
83090826f7 Merge pull request #4463 from penpot/niwinz-workspace-persistence-3
♻️ Refactor frontend persistence layer
2024-06-04 10:38:37 +02:00
Andrey Antukh
6436ef334b ♻️ Refactor persistence layer 2024-06-04 10:15:32 +02:00
Alejandro
d679001955 Merge pull request #4663 from penpot/eva-bugfixing-2.1
🐛 Bugfixing
2024-06-04 09:54:29 +02:00
Eva Marco
b74c5fc9b3 🐛 Fix demo account visible on login 2024-06-04 09:39:18 +02:00
Eva Marco
f9692fde35 Allow library colors as recent colors 2024-06-04 09:39:15 +02:00
Eva Marco
53526b9957 🐛 Fix expand libraries when search results are present 2024-06-04 09:38:36 +02:00
Alejandro
38353f3728 Merge pull request #4660 from penpot/niwinz-update-deps
⬆️ Update dependencies
2024-06-04 09:37:00 +02:00
Eva Marco
ee2ee326f3 Merge pull request #4662 from penpot/superalex-locking-degrees-on-path-edition
 Add locking degrees increment (hold shift) on path edition
2024-06-04 09:34:54 +02:00
Alejandro
fabe2d3d1d Merge pull request #4666 from penpot/ladybenko-7559-fix-palette
Fixes for the color palette
2024-06-04 09:27:59 +02:00
Belén Albeza
f8ca4c4343 🐛 Fix color name in circle bullets spanning over more than 2 lines 2024-06-04 09:08:59 +02:00
Belén Albeza
258aaf81d5 🐛 Fix color palette inner scroll width 2024-06-04 09:05:34 +02:00
Belén Albeza
8f0fd0a6e2 🐛 Fix wrong css selector applied globally and affecting the palette 2024-06-04 09:05:34 +02:00
Belén Albeza
de7880b4a2 🐛 Fix color names in palette being clipped and not breaking at dot characters 2024-06-04 09:05:34 +02:00
Pablo Alba
06221c37a3 Update THANKYOU.md 2024-06-04 09:05:34 +02:00
Alejandro Alonso
e200ed616c 🎉 Add a/b test for onboarding questions 2024-06-04 09:05:34 +02:00
Pablo Alba
928fec0903 Merge pull request #4661 from penpot/hiru-improve-slot-validation
🐛 Add validate and repair for :misplaced-slot
2024-06-03 16:08:54 +02:00
Pablo Alba
53513a523e Update THANKYOU.md 2024-06-03 13:04:49 +02:00
Alejandro Alonso
b5c419512f Add locking degrees increment (hold shift) on path edition 2024-06-03 13:01:45 +02:00
Andrés Moya
21052c661c 🐛 Add migration to remove all misplaced slots 2024-06-03 11:37:13 +02:00
Andrés Moya
b700a926c2 🐛 Add validate and repair for :misplaced-slot 2024-06-03 10:55:29 +02:00
Andrey Antukh
3bdcaa12e7 ⬆️ Update AWS S3 SDK dependency on backend 2024-05-31 13:31:56 +02:00
Andrey Antukh
87e3dc1c7c ⬆️ Update dependencies across all modules 2024-05-31 13:28:32 +02:00
Andrey Antukh
76ca1d9be8 ⬆️ Update frontend npm dependencies 2024-05-31 12:59:11 +02:00
Andrey Antukh
e2dd6a3791 ⬆️ Update playwright 2024-05-31 12:34:07 +02:00
Andrey Antukh
1d7c7f4a72 ⬆️ Update react to 18.3.1 2024-05-31 11:57:38 +02:00
Andrey Antukh
3c3ef57da2 Merge remote-tracking branch 'origin/staging' into develop 2024-05-31 10:08:21 +02:00
alonso.torres
98f3ef2755 ⬆️ Update plugins runtime 2024-05-30 10:11:15 +02:00
alonso.torres
ae774b10be Removed mandatory manifest property 2024-05-30 10:11:15 +02:00
Andrey Antukh
4bd585739a Merge branch 'mbiesiad-develop' into develop 2024-05-30 00:10:54 +02:00
Michal
9646f13a22 📚 Fix markdown bugs on changelog
Fix Markdown :bug
2024-05-30 00:10:37 +02:00
Andrés Moya
83327ef278 🔧 Change backend test runner to kaocha 2024-05-29 19:05:04 +02:00
Andrés Moya
0a3a6e19f1 Add test for swap slots bug 2024-05-29 15:12:19 +02:00
Andrés Moya
81ea392da6 Merge branch 'staging' into develop 2024-05-29 15:10:44 +02:00
alonso.torres
007ab3d909 Add library edition mode for plugins 2024-05-29 12:54:49 +02:00
Belén Albeza
b95cb3d4c5 Merge pull request #4638 from penpot/ladybenko-7936-setup-temp-visual-testing
 Setup temporary visual regression testing for the design system
2024-05-29 09:28:53 +02:00
Belén Albeza
804f4bb176 🐛 Fix color picker position 2024-05-28 20:54:50 +02:00
alonso.torres
d073f51790 🐛 Fix problem with plugin edition text 2024-05-28 20:54:11 +02:00
Belén Albeza
d4dc87a740 Setup temporary visual regression testing for the design system 2024-05-28 14:54:26 +02:00
alonso.torres
a2df74be38 Improved styles for plugins dialog 2024-05-28 13:44:39 +02:00
alonso.torres
032e551dc1 🐛 Fix problem with shadows 2024-05-28 13:44:39 +02:00
Alejandro Alonso
dad91421b1 📚 Update changelog 2024-05-28 11:27:18 +02:00
Alejandro Alonso
4ee9272177 Merge remote-tracking branch 'origin/staging' into develop 2024-05-28 10:43:00 +02:00
Alejandro Alonso
6d8fe193fb Merge remote-tracking branch 'origin/staging' into develop 2024-05-28 08:14:20 +02:00
Alejandro
c918e06859 Merge pull request #4636 from penpot/niwinz-bugfix-2
 Minor changes
2024-05-28 08:07:46 +02:00
alonso.torres
313e501a2a Expose text properties for shapes 2024-05-28 08:04:06 +02:00
alonso.torres
9498006fb8 🐛 Fix problem with border radius 2024-05-28 08:04:06 +02:00
alonso.torres
0576884a8b 🐛 Fix problem when setting shadows/blurs 2024-05-28 08:04:06 +02:00
alonso.torres
f0427e454e Add default icon for plugins 2024-05-28 08:04:06 +02:00
Alejandro
0b8604f9ea Merge pull request #4633 from penpot/niwinz-objects-gc-locking
 Improve object deletion flow
2024-05-27 16:20:50 +02:00
Andrey Antukh
121bff4eac Send only necessary data on profile update 2024-05-27 16:06:27 +02:00
Andrey Antukh
408ca338e7 📎 Make public default profile audit props 2024-05-27 16:06:27 +02:00
Andrey Antukh
574c8d1789 Move library-absorb operation to async task
And make it not mandatory in case of failure
2024-05-27 11:19:15 +02:00
Andrey Antukh
39119ac040 Reuse team deletion logic on orphan teams gc task 2024-05-27 11:17:00 +02:00
Andrey Antukh
761bbb7334 Add srepl helpers for delete/restore teams, projects, and files 2024-05-27 11:17:00 +02:00
Chan Young Park
6c34706160 🌐 Add translations for: Korean.
Currently translated at 16.2% (224 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2024-05-25 20:09:22 +02:00
Alejandro
c3c6879a2f Merge pull request #4616 from penpot/hiru-prepare-builder-for-figma-exporter
Prepare builder for figma exporter
2024-05-24 14:57:15 +02:00
Alejandro Alonso
b073c23ced Merge remote-tracking branch 'origin/staging' into develop 2024-05-24 13:16:53 +02:00
Andrés Moya
7bcb2b928d 🔧 Remove trace 2024-05-24 13:09:27 +02:00
Andrés Moya
3bf76e8d0f 🐛 Better handling of components v2 in file builder 2024-05-24 13:09:27 +02:00
Pablo Alba
260c0e0678 Add copying and duplicating component frontend tests 2024-05-24 12:58:18 +02:00
alonso.torres
519b2d7f04 🐛 Fix problem when initializing plugin 2024-05-24 12:22:09 +02:00
Belén Albeza
4b05ee35b8 Merge pull request #4607 from penpot/ladybenko-7845-migrate-storybook-compile
 Remove storybook dependency on Gulp
2024-05-24 09:36:38 +02:00
Alejandro
5ab4ed9a05 Merge pull request #4621 from penpot/niwinz-objects-gc-locking
 Reduce locking on objects-gc task
2024-05-24 09:04:11 +02:00
Chan Young Park
a217d2085a 🌐 Add translations for: Korean.
Currently translated at 15.7% (217 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2024-05-23 22:09:23 +02:00
Andrey Antukh
72facff282 🔥 Remove unnecessary code from test helpers 2024-05-23 16:45:48 +02:00
Andrey Antukh
f3346786ea 🔥 Remove unused object-update task 2024-05-23 16:36:43 +02:00
Andrey Antukh
d241f45253 🔥 Remove unnecessary async touch operation 2024-05-23 16:36:35 +02:00
Andrey Antukh
584a0fdba1 Reduce locking on objects-gc task
The main issue was the long running gc operation that
affects storage objects with deduplication. The long running
transacion ends locking some storage object rows which collaterally
made operations like import-binfile become blocked indefinitelly
because of the same rows (because of deduplication).

The solution used in this commit is split operations on small
chunks so we no longer use long running transactions that holds
too many locks. With this approach we will make a window to work
concurrently all operarate the distinct operations that requires
locks on the same rows.
2024-05-23 16:35:54 +02:00
Alejandro
12c34c6d42 Merge pull request #4620 from penpot/niwinz-bugfix-1
🐛 Bugfixes
2024-05-23 15:48:51 +02:00
Belén Albeza
dd3c92c1f5 Remove storybook dependency on Gulp 2024-05-23 15:12:29 +02:00
Andrey Antukh
86b2ce4dab 🐛 Set proper default tenant on exporter 2024-05-23 14:42:52 +02:00
Andrey Antukh
29ef9f752a 🐛 Fix incorrect password encoding on create-profile manage command 2024-05-23 14:42:28 +02:00
alonso.torres
4c7a30a029 ⬆️ Update plugins runtime 2024-05-23 14:24:12 +02:00
alonso.torres
3e6d3a2306 Add finish event for plugins 2024-05-23 14:24:12 +02:00
alonso.torres
5af77af6da 🐛 Fix problem running plugins from menu 2024-05-23 14:24:12 +02:00
Alejandro
fbd81e091d Merge pull request #4612 from penpot/niwinz-fix-devenv
 Improve yarn setup on devenv
2024-05-23 14:00:12 +02:00
Andrey Antukh
d9e4ee67c3 📎 Enable frontend integration tests 2024-05-23 13:25:03 +02:00
Andrey Antukh
7e5068f38e ⬆️ Update draft-js dependency commit reference
Fixes yarn.lock file
2024-05-23 13:19:22 +02:00
Belén Albeza
f74330dffe Add more timeout for playwright expects on CI 2024-05-23 13:18:51 +02:00
Alejandro Alonso
1d3d3f9b74 Add playwright dependencies to devenv docker 2024-05-23 13:18:49 +02:00
Alejandro
b28432f7fd Merge pull request #4618 from penpot/niwinz-ci-enhancements
 Improve yarn setup on devenv
2024-05-23 13:00:50 +02:00
Andrey Antukh
7158c4fd43 Improve yarn setup on devenv
And update yarn version to 4.2.2
2024-05-23 12:21:07 +02:00
Alejandro
20a7d668f5 Merge pull request #4617 from penpot/niwinz-fix-ci
🐳 Fix CI
2024-05-23 11:50:07 +02:00
Andrey Antukh
2c30dde198 📎 Disable frontend integration tests 2024-05-23 11:38:16 +02:00
Andrey Antukh
7f8c600837 📎 Fix cljs linter issue 2024-05-23 11:37:46 +02:00
Andrey Antukh
96844f5bea 📎 Fix cljs fmt linter issue 2024-05-23 11:37:38 +02:00
Andrey Antukh
4cb0e97db4 Revert "⬆️ Update JVM and NODE deps on devenv Dockerfile"
This reverts commit 0a1b255da7.
2024-05-22 18:08:49 +02:00
alonso.torres
4f4ce174ae Changes to the manifest loading 2024-05-22 17:30:19 +02:00
alonso.torres
85ae3ff6f8 🐛 Fix problem with hot reload 2024-05-22 17:30:19 +02:00
alonso.torres
9fcb4216b6 Improved performance to update layout 2024-05-22 17:30:19 +02:00
Alejandro Alonso
47d7d24910 Add copying and duplicating component tests 2024-05-22 14:26:03 +02:00
Alejandro Alonso
eb168a6f9f Merge remote-tracking branch 'origin/staging' into develop 2024-05-22 13:02:44 +02:00
alonso.torres
4ad0cc2680 ⬆️ Update plugins runtime 2024-05-22 12:25:28 +02:00
Aitor Moreno
8a74f24977 Merge pull request #4600 from penpot/eva-bugfixing
Eva bugfixing
2024-05-22 11:38:03 +02:00
alonso.torres
dcb6315ff6 🐛 Fix problem with minification 2024-05-21 19:53:09 +02:00
alonso.torres
3ca5b13e27 New apis for plugins 2024-05-21 15:24:28 +02:00
alonso.torres
d6de1fdbdf ♻️ Refactor plugins proxies internal data 2024-05-21 15:24:28 +02:00
Alejandro
b59dae57ca Merge pull request #4601 from penpot/niwinz-fire-package-lock
🔥 Remove package-lock.json file from repo root
2024-05-21 13:15:59 +02:00
Andrey Antukh
0a1b255da7 ⬆️ Update JVM and NODE deps on devenv Dockerfile 2024-05-20 12:10:13 +02:00
Andrey Antukh
8d7b2008f5 🔥 Remove package-lock.json file from repo root 2024-05-20 12:07:20 +02:00
Eva Marco
c56c3f9588 🐛 Fix project name ellipsis 2024-05-20 11:28:52 +02:00
Eva Marco
bd9ef6d221 🐛 Change login notification to error 2024-05-20 09:40:25 +02:00
Eva Marco
76fc2b04f4 🐛 Add fallback color to code generation 2024-05-20 09:38:16 +02:00
Eva Marco
b48d568905 Merge pull request #4596 from penpot/ladybenko-fix-debug-css-template
Fix including Debug CSS in the index template
2024-05-20 09:34:23 +02:00
Amine Gdoura
50d1d19d25 🌐 Add translations for: Arabic.
Currently translated at 82.0% (1131 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2024-05-19 13:01:53 +02:00
Belén Albeza
e74ab949ba 🐛 Include debug css in local dev only 2024-05-17 16:48:03 +02:00
Belén Albeza
d30eca016e 💄 Reformat JS file 2024-05-17 16:47:15 +02:00
Eva Marco
4a3a5f701f 🐛 Fix demo account text color 2024-05-17 12:15:18 +02:00
Eva Marco
7461126d1a Merge pull request #4592 from penpot/ladybenko-set-e2e-workers-1
 Set default workers for front-end integration tests to 1
2024-05-17 11:05:10 +02:00
Belén Albeza
06ac5ae520 Set default workers for front-end integration tests to 1 2024-05-17 09:29:31 +02:00
Aitor Moreno
a3d4d62269 Merge pull request #4585 from penpot/alotor-plugins-api
Updates to Plugin API and menu
2024-05-16 15:26:28 +02:00
Alejandro Alonso
614af9edc4 Add swap as override component tests 2024-05-16 13:27:06 +02:00
Pablo Alba
d221241451 Components refactor: Add tests for remove swap slot on copy-paste 2024-05-16 13:25:25 +02:00
Andrés Moya
9c8a5484e1 🔥 Remove traces 2024-05-16 13:24:17 +02:00
Pablo Alba
385e8d837a Components refactor: Add tests for remove or keep swap slot 2024-05-16 13:23:02 +02:00
Alejandro Alonso
f27cdee5ca 🐛 Remove TODO on multiple nesting levels component tests 2024-05-16 13:22:42 +02:00
Alejandro Alonso
4b8322dc82 Refactor common tests 2024-05-16 13:22:42 +02:00
alonso.torres
0ddcfa05cf Update plugins runtime 2024-05-16 12:26:19 +02:00
alonso.torres
4c71a4367f Add plugins management dialog 2024-05-16 12:26:19 +02:00
alonso.torres
236ff06763 Expand api for plugins 2024-05-16 11:05:01 +02:00
Eva Marco
6e409cbd47 Merge pull request #4583 from penpot/ladybenko-7730-ws-flaky
Fix flakiness of workspace tests
2024-05-14 16:46:18 +02:00
Belén Albeza
d43458ee89 🐛 Fix mocking websockets when running multiple tests 2024-05-14 16:36:54 +02:00
Alejandro Alonso
39c8b2ea3c Add multiple nesting levels component tests 2024-05-14 13:48:34 +02:00
Alejandro Alonso
fcf14b5cab Add duplicate page component tests 2024-05-14 13:48:34 +02:00
Alejandro Alonso
3e4aaa7935 Add new composition functions for common tests 2024-05-14 13:48:34 +02:00
Belén Albeza
575873eba7 🐛 Fix workspace rect drawing test 2024-05-13 17:41:00 +02:00
Andrés Moya
677b28218e 🔧 Update dependencies for fmt:clj 2024-05-13 13:31:38 +02:00
Pablo Alba
cddc50036f Merge pull request #4580 from penpot/hiru-refactor-frontend-tests
Refactor frontend test helpers
2024-05-13 13:24:55 +02:00
Andrés Moya
da939cc0a6 ♻️ Refactor front end test to make use of common file helpers 2024-05-13 13:10:01 +02:00
Andrés Moya
c16ef39abf ♻️ Make test helpers globally usable 2024-05-13 13:10:01 +02:00
AzazelN28
d8c60aa770 Skip failing test 2024-05-13 12:19:12 +02:00
Andy Li
ee0492120a 🌐 Add translations for: Chinese (Traditional).
Currently translated at 47.2% (651 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2024-05-12 11:01:11 +02:00
Aitor Moreno
0419b2c405 Merge pull request #4565 from penpot/eva-dashboard-pom-basic
  Add basic dashboard POM and test
2024-05-10 14:35:43 +02:00
Eva Marco
d341cef406 Add basic dashboard pom and test 2024-05-10 12:53:26 +02:00
Alejandro
ea20f693cb Merge pull request #4562 from penpot/hiru-move-frontend-tests
Move more frontend tests
2024-05-10 11:04:31 +02:00
Alejandro
db99e994c6 Merge pull request #4570 from Cenadros/hotfix/fix-media-import
🐛 Fix medias not being imported using lib-penpot
2024-05-10 10:33:21 +02:00
Andrés Moya
cb73ddc353 💄 Small refactor 2024-05-10 10:28:23 +02:00
Alejandro Alonso
a3e750ed0a Tests for chained components changes propagation 2024-05-10 10:25:08 +02:00
Andrés Moya
cf4ef426d7 Add tests for component sync 2024-05-10 10:17:09 +02:00
Belén Albeza
702e451530 Merge pull request #4572 from penpot/superalex-integration-tests
 Frontend integration tests
2024-05-10 09:30:48 +02:00
Alejandro Alonso
6b76213128 Frontend integration tests 2024-05-10 06:50:33 +02:00
Eva Marco
99371234dc Merge pull request #4563 from penpot/ladybenko-workspace-draw-test
Refactor POMS + extra workspace tests
2024-05-09 16:19:46 +02:00
Andrés Moya
b6e633865e Add tests for component creation and modification 2024-05-09 14:49:10 +02:00
Belén Albeza
00430d63eb Add test for drawing a shape in the workspace 2024-05-09 13:45:06 +02:00
Belén Albeza
e28d56e670 Add WorkspacePage POM for playwright testing 2024-05-09 13:45:02 +02:00
Andrés Moya
8b4e52a2be 💄 Improve copy child selection and test robustness 2024-05-09 13:33:52 +02:00
Andrés Moya
caefaf6016 Add tests for reset components 2024-05-09 13:33:52 +02:00
Alejandro
dd62653d4b Merge pull request #4571 from penpot/palba-fix-tests
🐛 Fix tests for remove swap slots
2024-05-09 13:20:24 +02:00
Pablo Alba
8deb799c3d 🐛 Fix tests for remove swap slots 2024-05-09 13:11:50 +02:00
Pablo Alba
6f93db034f 🎉 Components refactor: Add tests for remove swap slot on duplicate 2024-05-09 11:20:19 +02:00
Pablo Alba
bfe9caba15 ♻️ Components refactor: move generators for duplicate 2024-05-09 11:20:19 +02:00
Alex Sánchez
40bc1fac73 🐛 Fix medias not being imported using lib-penpot 2024-05-09 10:36:49 +02:00
Belén Albeza
9fd9e0178e ♻️ Refactor LoginPage POM 2024-05-08 12:17:53 +02:00
Pablo Alba
d92faaa6c6 Merge pull request #4558 from penpot/hiru-enhance-tests-debug
🔧 Enhance debug trace helpers for common tests
2024-05-07 22:12:05 +02:00
Andrés Moya
ea6a9c87ec 🔧 Enhance debug trace helpers for common tests 2024-05-07 18:45:10 +02:00
Belén Albeza
127c47a35a Merge pull request #4557 from penpot/azazeln28-test-add-websocket-mock
 Add basic test with websocket mock
2024-05-07 16:25:51 +02:00
AzazelN28
0091ac0f5f ♻️ Refactor tests and pages 2024-05-07 16:18:42 +02:00
Andrés Moya
b4ea749388 🐛 Fix linter error 2024-05-07 16:00:32 +02:00
Andrés Moya
86e524638c 🔥 Do a small cleanup 2024-05-07 15:51:56 +02:00
Pablo Alba
e7b065ac6c Merge pull request #4552 from penpot/hiru-move-touched-test
 Move more touched tests to common
2024-05-07 13:00:25 +02:00
Andrés Moya
c937d49ce9 💄 Split helpers in separated files 2024-05-07 12:51:07 +02:00
Andrés Moya
97e34d6e28 💄 Unify comments 2024-05-07 12:51:07 +02:00
Andrés Moya
b68bc9a08c Move more touched tests to common 2024-05-07 12:51:07 +02:00
Alejandro Alonso
1de138466f Merge remote-tracking branch 'origin/staging' into develop 2024-05-07 12:47:40 +02:00
AzazelN28
572c6f02e2 ♻️ Refactor MockWebSocket 2024-05-07 11:59:33 +02:00
Belén Albeza
3bae6e4661 Adapt mock and add workspace test with websocket mock 2024-05-07 11:59:33 +02:00
AzazelN28
30321e54f0 📎 Add WebSocket mock 2024-05-07 11:59:33 +02:00
Aitor Moreno
38e35fb5ae Merge pull request #4555 from penpot/eva-testing-login-pom
 Add login page as Page Object Model
2024-05-07 10:58:08 +02:00
Eva Marco
832c1db63b Add login page as Page Object Model 2024-05-07 10:28:17 +02:00
Pablo Alba
da437a0902 Merge pull request #4554 from penpot/hiru-create-logic-module
♻️ Move generate-xx methods to logic module
2024-05-06 18:48:44 +02:00
Andrés Moya
0b4fbc184d ♻️ Move generate-xx methods to logic module 2024-05-06 16:48:18 +02:00
Pablo Alba
7280dfd3f7 Tests for remove swap slot on move shapes to frame 2024-05-06 16:18:43 +02:00
Belén Albeza
e666127b57 Merge pull request #4500 from penpot/eva-login-tests-additions
 Add more login integration tests
2024-05-06 15:03:43 +02:00
Aitor Moreno
5436633104 Merge pull request #4536 from penpot/alotor-plugins-api
New plugins APIs
2024-05-06 12:17:49 +02:00
alonso.torres
ca7f17efd1 Add items to grid cells 2024-05-06 12:02:29 +02:00
Andy Li
f575650379 🌐 Add translations for: Chinese (Traditional).
Currently translated at 46.7% (644 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2024-05-05 07:07:10 +02:00
Stas Haas
5f560426fa 🌐 Add translations for: Russian.
Currently translated at 57.5% (794 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2024-05-03 15:07:10 +02:00
Louis Chance
402b6d4f34 🌐 Add translations for: French.
Currently translated at 100.0% (1379 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-05-03 15:07:08 +02:00
alonso.torres
fde0bcfd3e Add grid layout options to context 2024-05-03 13:44:31 +02:00
alonso.torres
9243ba937d Add to plugins clone and remove 2024-05-03 11:36:59 +02:00
alonso.torres
e30c21a71f Add relative positioning 2024-05-03 11:36:59 +02:00
alonso.torres
67d48435e7 Plugins create svg shapes 2024-05-03 11:36:59 +02:00
alonso.torres
21d38a058b Add to plugin api: upload media, group and ungroup 2024-05-03 11:36:59 +02:00
alonso.torres
75d8965365 Add method to append children 2024-05-03 11:36:59 +02:00
alonso.torres
4a74862bf5 Add viewport information to the plugin 2024-05-03 11:36:59 +02:00
TheScientistPT
43dd4ce457 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 98.5% (1359 of 1379 strings)

Co-authored-by: TheScientistPT <joao.ed.reis.gomes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
Translation: Penpot/frontend
2024-05-01 10:07:43 +02:00
Yaron Shahrabani
cd03794a09 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1379 of 1379 strings)

Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
Translation: Penpot/frontend
2024-05-01 10:07:42 +02:00
Stas Haas
a749519d8e 🌐 Add translations for: German.
Currently translated at 100.0% (1379 of 1379 strings)

Co-authored-by: Stas Haas <stas@girafic.de>
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
Translation: Penpot/frontend
2024-05-01 10:07:41 +02:00
Linerly
71687593fd 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1379 of 1379 strings)

Co-authored-by: Linerly <linerly@proton.me>
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
Translation: Penpot/frontend
2024-05-01 10:07:39 +02:00
Geek Squirrel
a7eb70a8e0 🌐 Add translations for: Chinese (Simplified).
Currently translated at 99.4% (1371 of 1379 strings)

🌐 Add translations for: Chinese (Simplified).

Currently translated at 95.5% (1317 of 1379 strings)

Co-authored-by: Geek Squirrel <geeksquirrel@qq.com>
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
Translation: Penpot/frontend
2024-05-01 10:07:38 +02:00
Pablo Alba
5e396010b3 Merge pull request #4535 from penpot/hiru-move-front-tests
Move frontend tests to common
2024-04-30 19:31:50 +02:00
Pablo Alba
de6d8ccbf9 Small fix on components touched test 2024-04-30 19:23:58 +02:00
Alejandro Alonso
bca8180aeb 🐛 Fix duplicate component 2024-04-30 16:46:36 +02:00
Andrés Moya
77d4901db1 Add more tests for touched 2024-04-30 14:18:55 +02:00
Andrés Moya
a40afd5b63 Add test for touched shapes 2024-04-30 13:47:40 +02:00
Andrés Moya
5611fcfc2c 🔧 Add generator function for update-shapes 2024-04-30 13:47:40 +02:00
Pablo Alba
f354942487 ♻️ Components refactor: generator for relocate shapes (and tests) 2024-04-30 13:37:32 +02:00
Pablo Alba
78d0611632 Merge pull request #4538 from penpot/superalex-swap-and-reset-generate-tests
 Swap and reset generate changes tests
2024-04-30 13:17:15 +02:00
Alejandro Alonso
f84cd933a8 Swap and reset generate changes tests 2024-04-30 12:49:43 +02:00
Yamila Moreno
d956f7c72c Update README.md
update penpot fest information
2024-04-30 12:21:28 +02:00
Aitor Moreno
b2e40155b0 Merge pull request #4519 from penpot/ladybenko-use-custom-server
 Swap http-server for a custom server with express (front-end integration tests)
2024-04-29 18:12:00 +02:00
Alejandro
013dbf1f76 Merge pull request #4499 from penpot/hiru-test-helpers
 Add helpers to build files for testing
2024-04-26 13:29:52 +02:00
Alejandro Alonso
cf62b083fe Merge remote-tracking branch 'origin/staging' into develop 2024-04-26 12:50:43 +02:00
Andrés Moya
40e43fd501 ♻️ Reorganize tests by level 2024-04-26 12:44:49 +02:00
Andrés Moya
ff4e27a1d5 Add composition helpers 2024-04-26 12:44:49 +02:00
Andrés Moya
dde89e60dd Add new helper functions for common tests 2024-04-26 11:45:37 +02:00
Andrés Moya
1026f5b972 🔧 Change common tests runner to kaocha 2024-04-26 11:42:41 +02:00
Alejandro
d1a8427563 Merge pull request #4528 from penpot/translations-2024-04-26-12
:sparkes: Update translations
2024-04-26 09:07:27 +02:00
Hosted Weblate
ee6245d2d1 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2024-04-26 09:06:15 +02:00
Alejandro
e7247817fb Merge pull request #4527 from penpot/translations-2024-04-26-11
Translations 2024 04 26 11
2024-04-26 08:58:29 +02:00
Alejandro Alonso
43fedb5fd4 Merge remote-tracking branch 'weblate/develop' into translations-2024-04-26-11 2024-04-26 08:57:20 +02:00
Alejandro
708c44d3f1 Merge pull request #4526 from penpot/translations-2024-04-26-9
 Update translations
2024-04-26 08:53:40 +02:00
Alejandro Alonso
1361917569 🌐 Add translations for: Spanish.
Currently translated at 99.8% (1377 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2024-04-26 08:51:54 +02:00
Stephan Paternotte
5394fce632 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1379 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-04-26 08:51:54 +02:00
Edgars Andersons
4b05551ecc 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1379 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-26 08:51:54 +02:00
Yaron Shahrabani
3cb6c501d3 🌐 Add translations for: Hebrew.
Currently translated at 98.9% (1364 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-26 08:51:54 +02:00
Stas Haas
c754e606ac 🌐 Add translations for: German.
Currently translated at 99.7% (1376 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-26 08:51:54 +02:00
Oğuz Ersen
258ead34f0 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1379 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-26 08:51:54 +02:00
Anonymous
871a08aa75 🌐 Add translations for: Yoruba.
Currently translated at 88.5% (1221 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-04-26 08:51:53 +02:00
Anonymous
476099c06a 🌐 Add translations for: Chinese (Traditional).
Currently translated at 44.5% (615 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2024-04-26 08:51:53 +02:00
Yaron Shahrabani
b74ae49f38 🌐 Add translations for: Hebrew.
Currently translated at 97.7% (1348 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-26 08:51:53 +02:00
Anonymous
51eabd2a23 🌐 Add translations for: Chinese (Simplified).
Currently translated at 95.4% (1316 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2024-04-26 08:51:53 +02:00
Oğuz Ersen
522909c66d 🌐 Add translations for: Turkish.
Currently translated at 97.8% (1350 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-26 08:51:53 +02:00
Anonymous
d2204ad48c 🌐 Add translations for: Turkish.
Currently translated at 97.8% (1350 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-26 08:51:53 +02:00
Anonymous
158753073c 🌐 Add translations for: Igbo.
Currently translated at 40.0% (546 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2024-04-26 08:51:53 +02:00
Anonymous
46705d9ebc 🌐 Add translations for: Malay.
Currently translated at 52.0% (709 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2024-04-26 08:51:53 +02:00
Anonymous
10c64330eb 🌐 Add translations for: Hausa.
Currently translated at 96.1% (1309 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2024-04-26 08:51:53 +02:00
Anonymous
16a5218592 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-04-26 08:51:53 +02:00
Anonymous
6cf0605745 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-26 08:51:53 +02:00
Anonymous
155346fa09 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-04-26 08:51:53 +02:00
Anonymous
3cf385e209 🌐 Add translations for: Czech.
Currently translated at 98.6% (1344 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2024-04-26 08:51:53 +02:00
Anonymous
16873891cb 🌐 Add translations for: Basque.
Currently translated at 87.5% (1192 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2024-04-26 08:51:53 +02:00
Anonymous
861348f97b 🌐 Add translations for: Polish.
Currently translated at 85.3% (1162 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2024-04-26 08:51:53 +02:00
Anonymous
f60c405167 🌐 Add translations for: Persian.
Currently translated at 50.0% (681 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2024-04-26 08:51:53 +02:00
Anonymous
6a2bdeb3cb 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-26 08:51:53 +02:00
Anonymous
5434ed146e 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2024-04-26 08:51:53 +02:00
Anonymous
0ab9300107 🌐 Add translations for: Arabic.
Currently translated at 83.1% (1132 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2024-04-26 08:51:53 +02:00
Anonymous
c40e3815df 🌐 Add translations for: Romanian.
Currently translated at 96.1% (1310 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2024-04-26 08:51:53 +02:00
Anonymous
a3cec26994 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-26 08:51:53 +02:00
Anonymous
5523914605 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 85.2% (1161 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2024-04-26 08:51:53 +02:00
Anonymous
7a849dd5c4 🌐 Add translations for: French.
Currently translated at 96.4% (1313 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-04-26 08:51:53 +02:00
Anonymous
6d3596ba14 🌐 Add translations for: Spanish.
Currently translated at 99.3% (1353 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2024-04-26 08:51:53 +02:00
Hosted Weblate
1f4266ffe4 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2024-04-26 08:51:53 +02:00
TheScientistPT
0fd31c253a 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-04-26 08:51:52 +02:00
Edgars Andersons
aae02bfedb 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-26 08:51:52 +02:00
Linerly
f9514f62a7 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2024-04-26 08:51:52 +02:00
Stas Haas
04359701a3 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-26 08:51:52 +02:00
Edgars Andersons
704103618b 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-26 08:51:52 +02:00
Louis Chance
f3a0f818aa 🌐 Add translations for: French.
Currently translated at 96.4% (1313 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-04-26 08:51:52 +02:00
Yaron Shahrabani
17b01b97cf 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-26 08:51:52 +02:00
Stas Haas
bb0427d613 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-26 08:51:52 +02:00
Alejandro Alonso
a0aec8023a 🌐 Add translations for: Yoruba.
Currently translated at 90.6% (1234 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-04-26 08:51:52 +02:00
Alejandro Alonso
2204799429 🌐 Add translations for: Igbo.
Currently translated at 40.0% (546 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2024-04-26 08:51:52 +02:00
Alejandro Alonso
f970397b36 🌐 Added translation for: Yoruba. 2024-04-26 08:51:52 +02:00
Alejandro Alonso
e15667bc39 🌐 Added translation for: Igbo. 2024-04-26 08:51:52 +02:00
Alejandro Alonso
9e9771fa1e 🌐 Deleted translation: Yoruba. 2024-04-26 08:51:52 +02:00
Alejandro Alonso
b3d46a59e3 🌐 Add translations for: Yoruba.
Currently translated at 1.5% (21 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-04-26 08:51:52 +02:00
Alejandro Alonso
d2a85dcd37 🌐 Added translation for: Yoruba. 2024-04-26 08:51:52 +02:00
Stephan Paternotte
316242ac67 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-04-26 08:51:52 +02:00
TheScientistPT
8adcb82c45 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-04-26 08:51:52 +02:00
Stas Haas
062c086eca 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-26 08:51:52 +02:00
Revenant
dc53c60db6 🌐 Add translations for: Malay.
Currently translated at 52.0% (709 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2024-04-26 08:51:52 +02:00
Oğuz Ersen
2166435a52 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-26 08:51:52 +02:00
Alejandro
4df2d7a49b Merge pull request #4525 from penpot/translations-2024-04-26-7
 Update translations
2024-04-26 08:47:11 +02:00
Oğuz Ersen
15debe65fd Update translations 2024-04-26 08:46:41 +02:00
Alejandro Alonso
007bd86f09 🌐 Add translations for: Spanish.
Currently translated at 99.8% (1377 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2024-04-26 06:53:02 +02:00
Alejandro
625bfa7166 Merge pull request #4520 from penpot/alotor-fix-exporter-problem
🐛 Fix problem with exporter texts
2024-04-25 16:30:32 +02:00
alonso.torres
a8363f0c02 🐛 Fix problem with exporter texts 2024-04-25 15:35:53 +02:00
Stephan Paternotte
8abc2261a6 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1379 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-04-25 15:07:21 +02:00
Edgars Andersons
e4178a66d6 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1379 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-25 15:07:20 +02:00
Yaron Shahrabani
ca7bd20c33 🌐 Add translations for: Hebrew.
Currently translated at 98.9% (1364 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-25 15:07:19 +02:00
Stas Haas
77f61191d1 🌐 Add translations for: German.
Currently translated at 99.7% (1376 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-25 15:07:19 +02:00
Oğuz Ersen
4896d39261 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1379 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-25 15:07:18 +02:00
Pablo Alba
a7bfa7c7c8 ♻️ Components refactor: generator for sync-file 2024-04-25 14:22:37 +02:00
Alejandro Alonso
c73eb77125 ♻️ Components refactor: generator for reset component 2024-04-25 14:22:37 +02:00
Alejandro Alonso
f3220fa985 ♻️ Components refactor: generator for sync head 2024-04-25 14:22:37 +02:00
Pablo Alba
37e68249aa ♻️ Components refactor: remove it usage on libraries_helpers 2024-04-25 14:22:37 +02:00
Alejandro Alonso
05f4459fb7 ♻️ Components refactor: generator for generate component for swap 2024-04-25 14:22:37 +02:00
Alejandro Alonso
c001710676 ♻️ Components refactor: generator for delete shapes 2024-04-25 14:22:37 +02:00
Pablo Alba
0ed582ebc5 ♻️ Components refactor: generator for detach-component 2024-04-25 14:22:37 +02:00
Pablo Alba
7d44eef4ab ♻️ Components refactor: generator for restore-component 2024-04-25 14:22:37 +02:00
Pablo Alba
f91d60eeb6 ♻️ Components refactor: generator for rename-component 2024-04-25 14:22:37 +02:00
Pablo Alba
02c455dcba ♻️ Components refactor: generator for duplicate component 2024-04-25 14:22:37 +02:00
Belén Albeza
306a8edbec Swap http-server for a custom server with express (front-end integration tests) 2024-04-25 13:40:14 +02:00
AzazelN28
47804429c0 Merge branch 'staging' into develop 2024-04-25 11:32:28 +02:00
Eva Marco
106fe05657 Add more login integration tests 2024-04-25 09:56:10 +02:00
Anonymous
e2f6b02075 🌐 Add translations for: Yoruba.
Currently translated at 88.5% (1221 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-04-24 15:04:10 +02:00
Anonymous
9a54785291 🌐 Add translations for: Chinese (Traditional).
Currently translated at 44.5% (615 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2024-04-24 15:04:10 +02:00
Yaron Shahrabani
9ba7bb7e17 🌐 Add translations for: Hebrew.
Currently translated at 97.7% (1348 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-24 15:04:09 +02:00
Anonymous
7d0bae6619 🌐 Add translations for: Chinese (Simplified).
Currently translated at 95.4% (1316 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2024-04-24 15:04:09 +02:00
Oğuz Ersen
ec9e32241d 🌐 Add translations for: Turkish.
Currently translated at 97.8% (1350 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-24 15:04:09 +02:00
Anonymous
a55bf34155 🌐 Add translations for: Turkish.
Currently translated at 97.8% (1350 of 1379 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-24 15:04:09 +02:00
Alejandro
e808818f02 Merge pull request #4507 from penpot/translations-8
 Update translations
2024-04-24 15:00:19 +02:00
Oğuz Ersen
d3f8abb9aa Update translations 2024-04-24 14:57:12 +02:00
Anonymous
aa56c293ca 🌐 Add translations for: Igbo.
Currently translated at 40.0% (546 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2024-04-24 14:55:21 +02:00
Anonymous
8cfc669d9d 🌐 Add translations for: Malay.
Currently translated at 52.0% (709 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2024-04-24 14:55:20 +02:00
Anonymous
3068721fc3 🌐 Add translations for: Hausa.
Currently translated at 96.1% (1309 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2024-04-24 14:55:19 +02:00
Anonymous
cd06bb13ba 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-04-24 14:55:18 +02:00
Anonymous
11cca08ec1 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-24 14:55:17 +02:00
Anonymous
8f9ba827d8 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-04-24 14:55:16 +02:00
Anonymous
ddd0e10c84 🌐 Add translations for: Czech.
Currently translated at 98.6% (1344 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2024-04-24 14:55:15 +02:00
Anonymous
e97162bdae 🌐 Add translations for: Basque.
Currently translated at 87.5% (1192 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2024-04-24 14:55:15 +02:00
Anonymous
b87b1120ca 🌐 Add translations for: Polish.
Currently translated at 85.3% (1162 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2024-04-24 14:55:14 +02:00
Anonymous
4b5d75bef0 🌐 Add translations for: Persian.
Currently translated at 50.0% (681 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2024-04-24 14:55:13 +02:00
Anonymous
330dddbc31 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-24 14:55:13 +02:00
Anonymous
609ffae580 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2024-04-24 14:55:12 +02:00
Anonymous
eef05c2352 🌐 Add translations for: Arabic.
Currently translated at 83.1% (1132 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2024-04-24 14:55:11 +02:00
Anonymous
19639fed2d 🌐 Add translations for: Romanian.
Currently translated at 96.1% (1310 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2024-04-24 14:55:10 +02:00
Anonymous
a7785f3a47 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-24 14:55:09 +02:00
Anonymous
b1e645abe6 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 85.2% (1161 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2024-04-24 14:55:09 +02:00
Anonymous
bb5f5eaba2 🌐 Add translations for: French.
Currently translated at 96.4% (1313 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-04-24 14:55:08 +02:00
Anonymous
990714c1da 🌐 Add translations for: Spanish.
Currently translated at 99.3% (1353 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2024-04-24 14:55:07 +02:00
Hosted Weblate
68d59051fc Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2024-04-24 14:52:04 +02:00
TheScientistPT
30b0a2eddb 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-04-24 14:52:02 +02:00
Edgars Andersons
084378556b 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-24 14:52:02 +02:00
Linerly
84b0905096 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2024-04-24 14:52:02 +02:00
Stas Haas
6e03267697 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-24 14:52:02 +02:00
Edgars Andersons
11bfc45c0b 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2024-04-24 14:52:02 +02:00
Louis Chance
8b17da4235 🌐 Add translations for: French.
Currently translated at 96.4% (1313 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2024-04-24 14:52:02 +02:00
Yaron Shahrabani
f410783283 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2024-04-24 14:52:02 +02:00
Stas Haas
b4e46347d4 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-24 14:52:02 +02:00
Alejandro Alonso
a678eb7295 🌐 Add translations for: Yoruba.
Currently translated at 90.6% (1234 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-04-24 14:52:02 +02:00
Alejandro Alonso
f5d7dc7c5c 🌐 Add translations for: Igbo.
Currently translated at 40.0% (546 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2024-04-24 14:52:02 +02:00
Alejandro Alonso
5d899df456 🌐 Added translation for: Yoruba. 2024-04-24 14:52:02 +02:00
Alejandro Alonso
4e6dd09753 🌐 Added translation for: Igbo. 2024-04-24 14:52:02 +02:00
Alejandro Alonso
0eb35f352a 🌐 Deleted translation: Yoruba. 2024-04-24 14:52:02 +02:00
Alejandro Alonso
80b635af67 🌐 Add translations for: Yoruba.
Currently translated at 1.5% (21 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2024-04-24 14:52:02 +02:00
Alejandro Alonso
d5827562a3 🌐 Added translation for: Yoruba. 2024-04-24 14:52:02 +02:00
Stephan Paternotte
99417bd124 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2024-04-24 14:52:02 +02:00
TheScientistPT
f3b68725d3 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2024-04-24 14:52:02 +02:00
Stas Haas
9369cf2d94 🌐 Add translations for: German.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2024-04-24 14:52:02 +02:00
Revenant
b4853fca7a 🌐 Add translations for: Malay.
Currently translated at 52.0% (709 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2024-04-24 14:52:01 +02:00
Oğuz Ersen
727836af76 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1362 of 1362 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2024-04-24 14:52:01 +02:00
Alejandro Alonso
0cc92eccfa Revert " Update translations"
This reverts commit 268a26b6a4.
2024-04-24 14:50:17 +02:00
Alejandro
2c6201f42a Merge pull request #4505 from penpot/superlex-update-translations-4
  Update translations
2024-04-24 14:13:26 +02:00
Oğuz Ersen
268a26b6a4 Update translations 2024-04-24 14:06:29 +02:00
Alejandro Alonso
0be97e1c44 🐛 Fix validate translations script 2024-04-24 14:06:22 +02:00
alonso.torres
6fbdc4ff07 🐛 Fix problem activating feature 2024-04-24 10:25:47 +02:00
Aitor Moreno
3eabab7fc7 Merge pull request #4489 from penpot/alotor-plugins-create-api
 Add write apis to the plugins
2024-04-23 13:38:28 +02:00
alonso.torres
00de89197e Add write apis to the plugins 2024-04-22 17:08:18 +02:00
Alejandro
c85f76300a Merge pull request #4477 from penpot/comp-refactor-lib-helpers
♻️ Move and merge libraries_helpers
2024-04-22 12:58:11 +02:00
Eva Marco
2fd55e3289 Merge pull request #4471 from penpot/ladybenko-7534-intercepting-login-seq
Integration test example: User login
2024-04-22 09:07:37 +02:00
Belén Albeza
c33d4ff3e2 Add user login front-end integration test 2024-04-19 17:55:19 +02:00
Pablo Alba
a8738b44a1 ♻️ Merge libraries_common_helpers into libraries_helpers 2024-04-19 16:00:38 +02:00
Pablo Alba
2e18ce9323 ♻️ Move libraries_helpers to common 2024-04-19 15:37:29 +02:00
Pablo Alba
e1cd6d04d9 ♻️ Move common libraries_helpers to libraries_common_helpers 2024-04-19 14:02:27 +02:00
alonso.torres
362d4ea47f Command to activate feature plugin 2024-04-19 12:09:20 +02:00
Andrey Antukh
716211524b Merge remote-tracking branch 'origin/staging' into develop 2024-04-19 09:37:37 +02:00
alonso.torres
d530815860 Upload plugins-runtime binary 2024-04-19 01:29:13 +02:00
alonso.torres
0ffd82299f Refactor to the context api 2024-04-19 01:29:13 +02:00
alonso.torres
7b508f2803 Create feature for plugins 2024-04-19 01:29:13 +02:00
alonso.torres
d7324b2e98 Support development and production plugin runtime 2024-04-19 01:29:13 +02:00
alonso.torres
97c3abfd60 Add nginx configuration for mjs files 2024-04-19 01:29:13 +02:00
alonso.torres
432e894344 Experiment for data definition 2024-04-19 01:29:13 +02:00
alonso.torres
127b481c38 Refactor of the plugins context 2024-04-19 01:29:13 +02:00
Juanfran
5a34c25926 🐛 Fix events inside webcomponent 2024-04-19 01:29:13 +02:00
Andrey Antukh
3f473ca765 Plugins proof of concept 2024-04-19 01:29:13 +02:00
Andrey Antukh
fd92437f7d Show error notificaton on oidc error redirect 2024-04-18 09:52:24 +02:00
Andrey Antukh
81b52d7170 Disable oidc registration when registration flag is disabled 2024-04-18 09:52:24 +02:00
Andrey Antukh
a969550aa4 Trust oidc providers which provides email_verified claim 2024-04-18 09:52:24 +02:00
Andrey Antukh
7e20cf10c5 ⬆️ Update dependencies 2024-04-18 09:52:24 +02:00
Andrey Antukh
90022041e6 Improve error handling on ldap frontend code 2024-04-18 09:52:24 +02:00
Andrey Antukh
ce790d83fd Improve internal registration flow 2024-04-18 09:52:24 +02:00
Andrey Antukh
606aeeb38f Allow attach context to audit entry from metadata 2024-04-18 09:52:24 +02:00
Andrey Antukh
ef632bcae7 Trigger email verification on OIDC register process
This will happen only if:
- the oidc provider properly reports that user has a non-verified email
- the oidc does not provides any way to know the email verification
  status
2024-04-18 09:52:24 +02:00
Andrey Antukh
b9ed0e1146 Merge remote-tracking branch 'origin/staging' into develop 2024-04-17 19:32:39 +02:00
Andrey Antukh
6477a48a50 Merge remote-tracking branch 'origin/staging' into develop 2024-04-16 19:35:24 +02:00
Ikko Eltociear Ashimine
64ff6d05c0 🐛 Fix alt typo
Communnity -> Community
2024-04-16 13:01:11 +02:00
Alejandro Alonso
ea42669ff6 Merge remote-tracking branch 'origin/staging' into develop 2024-04-16 12:42:20 +02:00
Alejandro Alonso
b14b8f794a Merge remote-tracking branch 'origin/staging' into develop 2024-04-15 20:58:22 +02:00
Alejandro Alonso
87927a3476 Merge remote-tracking branch 'origin/staging' into develop 2024-04-15 16:47:06 +02:00
Andrey Antukh
83f8218bf3 🐛 Fix incorrect queryparams handling on default nginx entrypoint 2024-04-12 10:50:13 +02:00
Andrey Antukh
c9fbb21924 Don't stop start-tmux if playwright is not found on frontend 2024-04-12 10:08:15 +02:00
Andrey Antukh
bc279977d5 📎 Comment playwright installation on frontend directory 2024-04-12 09:58:40 +02:00
Andrey Antukh
ca640964d1 ⬆️ Update exporter dependencies 2024-04-12 09:58:07 +02:00
Belén Albeza
4d29bff9b5 Add playwright to the frontend 2024-04-11 14:05:16 +02:00
Alejandro Alonso
ff04877d24 Merge remote-tracking branch 'origin/staging' into develop 2024-04-11 13:33:37 +02:00
Alejandro Alonso
34d75957f0 Merge remote-tracking branch 'origin/staging' into develop 2024-04-11 12:36:45 +02:00
Alejandro Alonso
5fccc59ad5 Merge remote-tracking branch 'origin/staging' into develop 2024-04-11 10:21:04 +02:00
Alejandro Alonso
c1c0ec6f2f Merge remote-tracking branch 'origin/staging' into develop 2024-04-10 16:32:19 +02:00
Alejandro Alonso
f47991fa9c Merge remote-tracking branch 'origin/staging' into develop 2024-04-10 09:31:44 +02:00
Kelp
656e910896 📎 Remove unnecessary html tags 2024-04-09 17:55:51 +02:00
Kelp
39c35d51d3 📎 Add missing <p> 2024-04-09 17:55:51 +02:00
Kelp
5b4fcd2c80 📎 Fix small content error 2024-04-09 17:55:51 +02:00
Kelp
91ade7fe58 🐛 Fix social media links 2024-04-09 17:55:51 +02:00
Kelp
19878484a6 🐛 Fix wrong website link 2024-04-09 17:55:51 +02:00
Kelp
c27f884755 Add light and dark version of the header 2024-04-09 17:55:51 +02:00
Alejandro Alonso
19fc5fa820 Merge remote-tracking branch 'origin/staging' into develop 2024-04-09 12:56:31 +02:00
Andrey Antukh
171320d1c0 Merge remote-tracking branch 'origin/staging' into develop 2024-04-09 10:05:51 +02:00
Alejandro Alonso
3fa1d3b5fd Merge remote-tracking branch 'origin/staging' into develop 2024-04-08 16:53:28 +02:00
Andrey Antukh
fb24918fd9 Merge remote-tracking branch 'origin/staging' into develop 2024-04-08 14:07:43 +02:00
Kelp
984dacba2b Update README.md small fix 2024-04-08 11:50:55 +02:00
Kelp
93debeb272 Update README.md 2.0 2024-04-08 11:49:56 +02:00
Andrey Antukh
e813fcb9b7 Merge remote-tracking branch 'origin/staging' into develop 2024-04-04 14:19:45 +02:00
Andrey Antukh
ad0aae375b Merge remote-tracking branch 'origin/staging' into develop 2024-04-04 12:45:38 +02:00
Andrey Antukh
d20a92ce50 Merge remote-tracking branch 'origin/staging' into develop 2024-04-02 11:35:30 +02:00
Andrey Antukh
4351c221ac Merge remote-tracking branch 'origin/staging' into develop 2024-03-27 11:10:45 +01:00
Alejandro Alonso
1f5658ad1b Merge remote-tracking branch 'origin/staging' into develop 2024-03-26 09:44:10 +01:00
Alejandro
0df97d4d7d Merge pull request #4324 from penpot/alotor-bugfix-34
Bugfixes
2024-03-26 07:40:07 +01:00
alonso.torres
e10c96fa8b 🐛 Fix problem with grid edition 2024-03-25 16:10:25 +01:00
alonso.torres
4b846b17f0 🐛 Fix problem when exporting html texts 2024-03-25 15:35:04 +01:00
alonso.torres
6f2f2291c2 🐛 Fix problem when importing SVG 2024-03-25 13:12:47 +01:00
alonso.torres
33c12117cc 🐛 Fix problem with gradients and borders 2024-03-25 09:49:38 +01:00
alonso.torres
94f9551b92 🐛 Fix problem with gradient fill text 2024-03-25 09:20:12 +01:00
Andrey Antukh
259eae70c6 Merge remote-tracking branch 'origin/staging' into develop 2024-03-25 08:48:18 +01:00
Andrey Antukh
c0fa766b64 📎 Update version.txt file 2024-03-22 08:40:31 +01:00
Andrey Antukh
a79e4d7af3 Merge remote-tracking branch 'origin/staging' into develop 2024-03-22 08:40:09 +01:00
Alejandro
589fb144db Merge pull request #4272 from peterstnsz/patch-1
Update README.md
2024-03-18 10:18:41 +01:00
alonso.torres
fd4470afde Merge remote-tracking branch 'origin/staging' into develop 2024-03-18 09:59:06 +01:00
Peter
e1e05e6de8 Update README.md
Link in the footer https://kaleidos.net/products returns 404
2024-03-14 14:52:20 +00:00
Andrey Antukh
d5dbe0b594 Merge remote-tracking branch 'origin/staging' into develop 2024-03-06 12:06:52 +01:00
Andrey Antukh
0606ef1c84 Merge remote-tracking branch 'origin/staging' into develop 2024-03-05 18:53:36 +01:00
Andrey Antukh
481058b8d4 Merge remote-tracking branch 'origin/staging' into develop 2024-03-04 10:29:11 +01:00
Andrey Antukh
a109673654 Merge remote-tracking branch 'origin/staging' into develop 2024-02-29 12:53:08 +01:00
Pablo Alba
b17371d440 🐛 Fix problems on sync with components chain with reset override 2024-02-28 11:35:19 +01:00
Andrey Antukh
617edbebec Merge remote-tracking branch 'origin/staging' into develop 2024-02-28 11:25:36 +01:00
Andrey Antukh
e3f9bafb33 Merge remote-tracking branch 'origin/staging' into develop 2024-02-27 12:53:20 +01:00
Andrey Antukh
2a6589ab01 Merge remote-tracking branch 'origin/staging' into develop 2024-02-27 12:45:59 +01:00
Andrey Antukh
9e2e9f5b64 Merge remote-tracking branch 'origin/staging' into develop 2024-02-22 16:24:26 +01:00
Andrey Antukh
82dca0439a Merge remote-tracking branch 'origin/staging' into develop 2024-02-21 14:56:28 +01:00
Andrey Antukh
66f8ffb408 Merge remote-tracking branch 'origin/staging' into develop 2024-02-20 16:07:51 +01:00
Andrey Antukh
fd641e87c9 Merge remote-tracking branch 'origin/staging' into develop 2024-02-19 16:02:59 +01:00
Andrey Antukh
bec59ab3c2 Merge remote-tracking branch 'origin/staging' into develop 2024-02-16 14:30:20 +01:00
Andrey Antukh
b426db133d Merge remote-tracking branch 'origin/staging' into develop 2024-02-14 13:01:58 +01:00
Andrey Antukh
e232beeb59 Merge remote-tracking branch 'origin/staging' into develop 2024-02-12 16:13:55 +01:00
Andrey Antukh
dae277adb2 Merge remote-tracking branch 'origin/staging' into develop 2024-02-12 10:07:32 +01:00
Andrey Antukh
cdf8c5836a Merge remote-tracking branch 'origin/staging' into develop 2024-02-09 15:01:42 +01:00
Andrey Antukh
1743da7ebf Merge remote-tracking branch 'origin/staging' into develop 2024-02-09 14:57:48 +01:00
Andrey Antukh
cff44e34c3 Merge remote-tracking branch 'origin/staging' into develop 2024-02-09 09:53:13 +01:00
Andrey Antukh
740db82b6c Merge remote-tracking branch 'origin/staging' into develop 2024-02-08 09:10:06 +01:00
Andrey Antukh
4d7a572daa Merge remote-tracking branch 'origin/staging' into develop 2024-02-06 20:10:55 +01:00
Andrey Antukh
cb6db21e63 Merge remote-tracking branch 'origin/staging' into develop 2024-02-06 09:38:25 +01:00
Andrey Antukh
06033ea955 Merge remote-tracking branch 'origin/staging' into develop 2024-02-02 10:15:40 +01:00
Andrey Antukh
f15caf54dd Merge remote-tracking branch 'origin/staging' into develop 2024-01-31 11:05:20 +01:00
Andrey Antukh
94ee83a120 Merge remote-tracking branch 'origin/staging' into develop 2024-01-30 19:32:44 +01:00
Andrey Antukh
cb6e3a2e3c Merge remote-tracking branch 'origin/staging' into develop 2024-01-30 15:34:50 +01:00
Andrey Antukh
8abab982e7 Merge remote-tracking branch 'origin/staging' into develop 2024-01-25 23:26:55 +01:00
Andrey Antukh
6a412c75ce Merge remote-tracking branch 'origin/staging' into develop 2024-01-24 09:00:42 +01:00
Andrey Antukh
098e33bd98 Merge remote-tracking branch 'origin/staging' into develop 2024-01-19 14:44:14 +01:00
Andrey Antukh
a5d056f254 Merge remote-tracking branch 'origin/staging' into develop 2024-01-17 09:51:47 +01:00
Andrey Antukh
aa33bb1ebf Merge remote-tracking branch 'origin/staging' into develop 2024-01-16 00:12:55 +01:00
Andrey Antukh
8a81bc11e0 Merge remote-tracking branch 'origin/staging' into develop 2024-01-15 10:10:35 +01:00
Andrey Antukh
f0ea613d47 Merge remote-tracking branch 'origin/staging' into develop 2024-01-11 16:18:17 +01:00
Andrey Antukh
725501faf9 Merge remote-tracking branch 'origin/staging' into develop 2024-01-11 10:17:52 +01:00
Andrey Antukh
470cf08134 Merge remote-tracking branch 'origin/staging' into develop 2024-01-10 11:43:01 +01:00
Andrey Antukh
e48bfb5d94 Merge remote-tracking branch 'origin/staging' into develop 2024-01-09 09:56:55 +01:00
Andrey Antukh
326f18cb66 Merge remote-tracking branch 'origin/staging' into develop 2024-01-09 08:53:46 +01:00
Andrey Antukh
5983b9cd54 Merge remote-tracking branch 'origin/staging' into develop 2024-01-08 15:27:27 +01:00
Andrey Antukh
864881721f 🐛 Fix validation of validation error on file validate ns 2024-01-05 17:59:17 +01:00
Andrey Antukh
9aff12f3c6 Merge remote-tracking branch 'origin/staging' into develop 2024-01-05 09:46:53 +01:00
560 changed files with 60640 additions and 26995 deletions

View File

@@ -32,42 +32,42 @@ jobs:
- run: clj-kondo --version
- run:
name: "fmt check backend [clj]"
name: "backend fmt check"
working_directory: "./backend"
command: |
yarn install
yarn run fmt:clj:check
- run:
name: "fmt check exporter [clj]"
name: "exporter fmt check"
working_directory: "./exporter"
command: |
yarn install
yarn run fmt:clj:check
- run:
name: "fmt check common [clj]"
name: "common fmt check"
working_directory: "./common"
command: |
yarn install
yarn run fmt:clj:check
- run:
name: "fmt check frontend [clj]"
name: "frontend fmt check"
working_directory: "./frontend"
command: |
yarn install
yarn run fmt:clj:check
- run:
name: common lint
name: "common linter check"
working_directory: "./common"
command: |
yarn install
yarn run lint:clj
- run:
name: frontend lint
name: "frontend linter check"
working_directory: "./frontend"
command: |
yarn install
@@ -75,14 +75,14 @@ jobs:
yarn run lint:clj
- run:
name: backend lint
name: "backend linter check"
working_directory: "./backend"
command: |
yarn install
yarn run lint:clj
- run:
name: exporter lint
name: "exporter linter check"
working_directory: "./exporter"
command: |
yarn install
@@ -93,7 +93,7 @@ jobs:
working_directory: "./common"
command: |
yarn test
clojure -X:dev:test :patterns '["common-tests.*-test"]'
clojure -M:dev:test
- run:
name: "frontend tests"
@@ -102,11 +102,21 @@ jobs:
yarn install
yarn test
- run:
name: "frontend integration tests"
working_directory: "./frontend"
command: |
yarn install
yarn run compile
clojure -M:dev:shadow-cljs release main
yarn playwright install --with-deps chromium
yarn e2e:test
- run:
name: "backend tests"
working_directory: "./backend"
command: |
clojure -X:dev:test :patterns '["backend-tests.*-test"]'
clojure -M:dev:test
environment:
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"

View File

@@ -12,6 +12,7 @@
(def registry (atom {}))
(defn potok-reify
[{:keys [:node :filename] :as params}]
(let [[rnode rtype & other] (:children node)

5
.gitignore vendored
View File

@@ -57,6 +57,7 @@
/frontend/package-lock.json
/frontend/resources/fonts/experiments
/frontend/resources/public/*
/frontend/storybook-static/
/frontend/target/
/other/
/scripts/
@@ -68,3 +69,7 @@
clj-profiler/
node_modules
frontend/.storybook/preview-body.html
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -1,13 +1,103 @@
# CHANGELOG
## 2.1.4
### :bug: Bugs fixed
- Fix json encoding on zip encoding decoding.
- Add schema validation for color changes.
- Fix render of some texts without position data.
## 2.1.3
### :bug: Bugs fixed
- Don't allow registry with email and password, if password login is disabled (invitation workflow) [Github #4975](https://github.com/penpot/penpot/issues/4975)
## 2.1.2
### :bug: Bugs fixed
- User switch language to "zh_hant" will get 400 [Github #4884](https://github.com/penpot/penpot/issues/4884)
- Smtp config ignoring port if ssl is set [Github #4872](https://github.com/penpot/penpot/issues/4872)
- Ability to let users to authenticate with a private oidc provider only [Github #4963](https://github.com/penpot/penpot/issues/4963)
## 2.1.1
### :sparkles: New features
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
### :bug: Bugs fixed
- Fix the “search” label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
- Fix several issues on the OIDC.
- Fix regression on the `email-verification` flag [Taiga #8398](https://tree.taiga.io/project/penpot/issue/8398)
## 2.1.0 - Things can only get better!
### :rocket: Epics and highlights
### :boom: Breaking changes & Deprecations
### :heart: Community contributions (Thank you!)
### :sparkles: New features
- Improve auth process [Taiga #7094](https://tree.taiga.io/project/penpot/us/7094)
- Add locking degrees increment (hold shift) on path edition [Taiga #7761](https://tree.taiga.io/project/penpot/issue/7761)
- Persistence & Concurrent Edition Enhancements [Taiga #5657](https://tree.taiga.io/project/penpot/us/5657)
- Allow library colors as recent colors [Taiga #7640](https://tree.taiga.io/project/penpot/issue/7640)
- Missing scroll in viewmode comments [Taiga #7427](https://tree.taiga.io/project/penpot/issue/7427)
- Comments in View mode should mimic the positioning behavior of the Workspace [Taiga #7346](https://tree.taiga.io/project/penpot/issue/7346)
- Misaligned input on comments [Taiga #7461](https://tree.taiga.io/project/penpot/issue/7461)
### :bug: Bugs fixed
- Fix selection rectangle appears on scroll [Taiga #7525](https://tree.taiga.io/project/penpot/issue/7525)
- Fix layer tree not expanding to the bottom edge [Taiga #7466](https://tree.taiga.io/project/penpot/issue/7466)
- Fix guides move when board is moved by inputs [Taiga #8010](https://tree.taiga.io/project/penpot/issue/8010)
- Fix clickable area of Penptot logo in the viewer [Taiga #7988](https://tree.taiga.io/project/penpot/issue/7988)
- Fix constraints dropdown when selecting multiple shapes [Taiga #7686](https://tree.taiga.io/project/penpot/issue/7686)
- Layout and scrollign fixes for the bottom palette [Taiga #7559](https://tree.taiga.io/project/penpot/issue/7559)
- Fix expand libraries when search results are present [Taiga #7876](https://tree.taiga.io/project/penpot/issue/7876)
- Fix color palette default library [Taiga #8029](https://tree.taiga.io/project/penpot/issue/8029)
- Component Library is lost after exporting/importing in .zip format [Github #4672](https://github.com/penpot/penpot/issues/4672)
- Fix problem with moving+selection not working properly [Taiga #7943](https://tree.taiga.io/project/penpot/issue/7943)
- Fix problem with flex layout fit to content not positioning correctly children [Taiga #7537](https://tree.taiga.io/project/penpot/issue/7537)
- Fix black line is displaying after show main [Taiga #7653](https://tree.taiga.io/project/penpot/issue/7653)
- Fix "Share prototypes" modal remains open [Taiga #7442](https://tree.taiga.io/project/penpot/issue/7442)
- Fix "Components visibility and opacity" [#4694](https://github.com/penpot/penpot/issues/4694)
- Fix "Attribute overrides in copies are not exported in zip file" [Taiga #8072](https://tree.taiga.io/project/penpot/issue/8072)
- Fix group not automatically selected in the Layers panel after creation [Taiga #8078](https://tree.taiga.io/project/penpot/issue/8078)
- Fix export boards loses opacity [Taiga #7592](https://tree.taiga.io/project/penpot/issue/7592)
- Fix change color on imported svg also changes the stroke alignment[Taiga #7673](https://github.com/penpot/penpot/pull/7673)
- Fix show in view mode and interactions workflow [Taiga #4711](https://github.com/penpot/penpot/pull/4711)
- Fix internal error when I set up a stroke for some objects without and with stroke [Taiga #7558](https://tree.taiga.io/project/penpot/issue/7558)
- Toolbar keeps toggling on and off on spacebar press [Taiga #7654](https://github.com/penpot/penpot/pull/7654)
- Fix toolbar keeps hiding when click outside workspace [Taiga #7776](https://tree.taiga.io/project/penpot/issue/7776)
- Fix open overlay relative to a frame [Taiga #7563](https://tree.taiga.io/project/penpot/issue/7563)
- Workspace-palette items stay hidden when opening with keyboard-shortcut [Taiga #7489](https://tree.taiga.io/project/penpot/issue/7489)
- Fix SVG attrs are not handled correctly when exporting/importing in .zip [Taiga #7920](https://tree.taiga.io/project/penpot/issue/7920)
- Fix validation error when detaching with two nested copies and a swap [Taiga #8095](https://tree.taiga.io/project/penpot/issue/8095)
- Export shapes that are rotated act a bit strange when reimported [Taiga #7585](https://tree.taiga.io/project/penpot/issue/7585)
- Penpot crashes when a new colorpicker is created while uploading an image to another instance [Taiga #8119](https://tree.taiga.io/project/penpot/issue/8119)
- Removing Underline and Strikethrough Affects the Previous Text Object [Taiga #8103](https://tree.taiga.io/project/penpot/issue/8103)
- Color library loses association with shapes when exporting/importing the document [Taiga #8132](https://tree.taiga.io/project/penpot/issue/8132)
- Fix can't collapse groups when searching in the assets tab [Taiga #8125](https://tree.taiga.io/project/penpot/issue/8125)
- Fix 'Detach instance' shortcut is not working [Taiga #8102](https://tree.taiga.io/project/penpot/issue/8102)
- Fix import file message does not detect 0 as error [Taiga #6824](https://tree.taiga.io/project/penpot/issue/6824)
- Image Color Library is not persisted when exporting/importing in .zip [Taiga #8131](https://tree.taiga.io/project/penpot/issue/8131)
- Fix export files including libraries [Taiga #8266](https://tree.taiga.io/project/penpot/issue/8266)
## 2.0.3
### :bug: Bugs fixed
- Fix chrome scrollbar styling [Taiga Issue #7852](https://tree.taiga.io/project/penpot/issue/7852)
- Fix chrome scrollbar styling [Taiga #7852](https://tree.taiga.io/project/penpot/issue/7852)
- Fix incorrect password encoding on create-profile manage scritp [Github #3651](https://github.com/penpot/penpot/issues/3651)
## 2.0.2
### :sparkles: Enhancements
@@ -17,7 +107,7 @@
### :bug: Bugs fixed
- Fix color palette sorting [Taiga Issue #7458](https://tree.taiga.io/project/penpot/issue/7458)
- Fix color palette sorting [Taiga #7458](https://tree.taiga.io/project/penpot/issue/7458)
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
@@ -121,7 +211,7 @@
- [REDESIGN] Panels visual separations [Taiga #6692](https://tree.taiga.io/project/penpot/us/6692)
- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678)
### :bug Bugs fixed
### :bug: Bugs fixed
- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661)
- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941)
- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998)
@@ -162,7 +252,7 @@
- Fix problem when changing typography assets [Github #3683](https://github.com/penpot/penpot/issues/3683)
- Internal error when you copy and paste some main components between files [Taiga #7397](https://tree.taiga.io/project/penpot/issue/7397)
- Fix toolbar disappearing [Taiga #7411](https://tree.taiga.io/project/penpot/issue/7411)
- Fix long text on tab breaks UI [Taiga Issue #7421](https://tree.taiga.io/project/penpot/issue/7421)
- Fix long text on tab breaks UI [Taiga #7421](https://tree.taiga.io/project/penpot/issue/7421)
## 1.19.5

123
README.md
View File

@@ -2,10 +2,11 @@
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
<h1 align="center">
<br>
<img src="https://penpot.app/images/readme/git-readme-header.png" alt="PENPOT">
</h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://penpot.app/images/readme/github-dark-mode.png">
<source media="(prefers-color-scheme: light)" srcset="https://penpot.app/images/readme/github-light-mode.png">
<img alt="penpot header image" src="https://penpot.app/images/readme/github-light-mode.png">
</picture>
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
<a href="https://gitter.im/penpot/community" rel="nofollow"><img src="https://camo.githubusercontent.com/5b0aecb33434f82a7b158eab7247544235ada0cf7eeb9ce8e52562dd67f614b7/68747470733a2f2f6261646765732e6769747465722e696d2f736572656e6f2d78797a2f636f6d6d756e6974792e737667" alt="Gitter" data-canonical-src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
@@ -13,22 +14,36 @@
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
<p align="center">
<a href="https://penpot.app/"><b>Website</b></a>
<a href="https://help.penpot.app/technical-guide/getting-started/"><b>Getting Started</b></a>
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a>
<a href="https://help.penpot.app/user-guide/introduction/info/"><b>Tutorials & Info</b></a>
<a href="https://community.penpot.app/"><b>Community</b></a>
<a href="https://twitter.com/penpotapp"><b>Twitter</b></a> •
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a>
<a href="https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g"><b>Youtube</b></a>
<a href="https://penpot.app/"><b>Website</b></a>
<a href="https://help.penpot.app/technical-guide/getting-started/"><b>Getting Started</b></a>
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a>
<a href="https://help.penpot.app/user-guide/introduction/info/"><b>Tutorials & Info</b></a>
<a href="https://community.penpot.app/"><b>Community</b></a>
</p>
<p align="center">
<a href="https://www.youtube.com/@Penpot"><b>Youtube</b></a>
<a href="https://peertube.kaleidos.net/a/penpot_app/video-channels"><b>Peertube</b></a>
<a href="https://www.linkedin.com/company/penpot/"><b>Linkedin</b></a> •
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
<a href="https://twitter.com/penpotapp"><b>X</b></a>
</p>
![feature-readme](https://user-images.githubusercontent.com/1045247/189871786-0b44f7cf-3a0a-4445-a87b-9919ec398bf7.gif)
<br />
🎇 **Penpot Fest exceeded all expectations - it was a complete success!** 🎇 Penpot Fest is our first Design event that brought designers and developers from the Open Source communities and beyond. Watch the replay of the talks on our [Youtube channel](https://www.youtube.com/playlist?list=PLgcCPfOv5v56-fghJo2dHNBqL9zlDTslh) or [Peertube channel](https://peertube.kaleidos.net/w/p/1tWgyJTt8sKbWwCEcBimZW)
[Penpot video](https://github.com/penpot/penpot/assets/5446186/b8ad0764-585e-4ddc-b098-9b4090d337cc)
Penpot is the first **Open Source** design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return.
<br />
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
Penpot is available on browser and [self host](https://penpot.app/self-host). Its web-based and works with open standards (SVG, CSS and HTML). And last but not least, its free!
Penpots latest [huge release 2.0](https://penpot.app/dev-diaries), takes the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. Plus, it's faster and more accessible.
🎇 **Penpot Fest** is our design, code & Open Source event. Check out the highlights from [Penpot Fest 2023 edition](https://www.youtube.com/watch?v=sOpLZaK5mDc)!
## Table of contents ##
@@ -40,48 +55,47 @@ Penpot is the first **Open Source** design and prototyping platform meant for cr
## Why Penpot ##
Penpot makes design and prototyping accessible to every team in the world.
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
### For cross-domain teams ###
We have a clear focus on design and code teams and our capabilities reflect exactly that. The less hand-off mindset, the more fun for everyone.
### Designed for developers ###
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
### Multiplatform ###
Being web based, Penpot is not dependent on operating systems or local installations, you will only need to run a modern browser.
### Inspect mode ###
Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code.
### Open Standards ###
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
### Self host your own instance ###
Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server.
### Integrations ###
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
### Whats great for design ###
With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions.
<br />
<p align="center">
<img src="https://penpot.app/images/readme/git-open.png" alt="Open Source" style="width: 65%;">
<img src="https://img.plasmic.app/img-optimizer/v1/img?src=https%3A%2F%2Fimg.plasmic.app%2Fimg-optimizer%2Fv1%2Fimg%2F9dd677c36afb477e9666ccd1d3f009ad.png" alt="Open Source" style="width: 65%;">
</p>
<br />
## Getting started ##
### Install with Elestio ###
[Elestio](https://elest.io/) offers a fully managed service for on-premise instances of a selection of open-source software! This means you can deploy a dedicated instance of Penpot in just 3 minutes with no technical knowledge needed.
Penpot is the only design & prototype platform that is deployment agnostic. You can use it or deploy it anywhere.
You dont need to worry about DNS configuration, SMTP, backups, SSL certificates, OS & Penpot upgrades, and much more.
[Get started with Elestio.](https://help.penpot.app/technical-guide/getting-started/#install-with-elestio)
### Install with Docker ###
You can also get started with Penpot locally or self-host it with **docker** and **docker-compose**.
Heres a step-by-step guide on [getting started with Docker.](https://help.penpot.app/technical-guide/getting-started/#install-with-docker)
### Penpot cloud app ###
If you prefer not to install Penpot in a local environment, [login or register on our Penpot cloud app](https://design.penpot.app). Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** on your own.
Learn how to install it with Elestio and Docker, or other options on [our website](https://penpot.app/self-host).
<br />
<p align="center">
<img src="https://penpot.app/images/readme/git-self-host.png" alt="Getting started" style="width: 65%;">
<img src="https://site-assets.plasmic.app/2168cf524dd543caeff32384eb9ea0a1.svg" alt="Open Source" style="width: 65%;">
</p>
<br />
## Community ##
We love the open source software community. Contributing is our passion and if its yours too, [participate](https://community.penpot.app/) and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your ideas and code are welcome!
We love the Open Source software community. Contributing is our passion and if its yours too, participate and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your designs, code and ideas are welcome!
If you need help or have any questions; if youd like to share your experience using Penpot or get inspired; if youd rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)!
@@ -93,30 +107,41 @@ You will find the following categories:
- [Events and Announcements](https://community.penpot.app/c/announcements/5)
- [Inside Penpot](https://community.penpot.app/c/inside-penpot/21)
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22)
<br />
<p align="center">
<img src="https://penpot.app/images/readme/git-collaborate.png" alt="Communnity" style="width: 65%;">
<img src="https://github.com/penpot/penpot/assets/5446186/6ac62220-a16c-46c9-ab21-d24ae357ed03" alt="Community" style="width: 65%;">
</p>
<br />
## Contributing ##
Every sort of contribution will be very helpful to enhance Penpot. How youll participate? All your ideas, designs and code are welcome:
Any contribution will make a difference to improve Penpot. How can you get involved?
Choose your way:
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
- Invite your [team to join](https://design.penpot.app/#/auth/register)
- Star this repo and follow us on Social Media: [Twitter](https://twitter.com/penpotapp), [Instagram](https://instagram.com/penpot.app), [Youtube](https://www.youtube.com/c/Penpot) or [Mastodon](https://fosstodon.org/@penpot/).
- Participate in the [Community](https://community.penpot.app/) asking and answering questions, reacting to others articles or opening your own conversations.
- Star this repo and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app) and [X](https://twitter.com/penpotapp).
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others articles; opening your own conversations and following along on decisions affecting the project.
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues)
- Create and [share Libraries & templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
- Become a [translator](https://help.penpot.app/contributing-guide/translations)
- Give feedback: [Mail us](mailto:support@penpot.app)
- Give feedback: [Email us](mailto:support@penpot.app)
- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpots repository and make changes in both front and back end
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/).
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/).
<br />
<p align="center">
<img src="https://penpot.app/images/readme/git-community.png" alt="Contributing" style="width: 65%;">
<img src="https://github.com/penpot/penpot/assets/5446186/fea18923-dc06-49be-86ad-c3496a7956e6" alt="Libraries and templates" style="width: 65%;">
</p>
<br />
## Resources ##
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
@@ -141,4 +166,4 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS INC
```
Penpot is a Kaleidos [open source project](https://kaleidos.net/products)
Penpot is a Kaleidos [open source project](https://kaleidos.net/)

View File

@@ -2,12 +2,19 @@
We want to thank to the amazing people that help us! Thank you! You're the best!
Feel free you make a PR updating this file if you miss you in the
list.
## Security
* Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD)
* [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/)
* Vaibhav Shukla
* Hassan Ahmed (Alias Xen Lee)
* Michal Biesiada (@mbiesiad)
## Internationalization
* [00ff88](https://hosted.weblate.org/user/00ff88)
* [AhmadHB](https://hosted.weblate.org/user/AhmadHB)
* [Aimee](https://hosted.weblate.org/user/Aimee)
@@ -89,6 +96,7 @@ We want to thank to the amazing people that help us! Thank you! You're the best!
* [zcraber](https://hosted.weblate.org/user/zcraber)
## Libraries & templates
* systxema
* plumilla
* victor crespo

View File

@@ -3,10 +3,10 @@
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.12.0-alpha5"}
org.clojure/tools.namespace {:mvn/version "1.4.4"}
org.clojure/clojure {:mvn/version "1.12.0-alpha12"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.5-11"}
com.github.luben/zstd-jni {:mvn/version "1.5.6-3"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.3.0.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.3.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti
@@ -26,13 +26,13 @@
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.909"}
metosin/reitit-core {:mvn/version "0.6.0"}
nrepl/nrepl {:mvn/version "1.1.0"}
cider/cider-nrepl {:mvn/version "0.44.0"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}
metosin/reitit-core {:mvn/version "0.7.0"}
nrepl/nrepl {:mvn/version "1.1.2"}
cider/cider-nrepl {:mvn/version "0.48.0"}
org.postgresql/postgresql {:mvn/version "42.7.1"}
org.xerial/sqlite-jdbc {:mvn/version "3.44.1.0"}
org.postgresql/postgresql {:mvn/version "42.7.3"}
org.xerial/sqlite-jdbc {:mvn/version "3.46.0.0"}
com.zaxxer/HikariCP {:mvn/version "5.1.0"}
@@ -54,11 +54,11 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.7"}
markdown-clj/markdown-clj {:mvn/version "1.12.1"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.22.12"}
software.amazon.awssdk/s3 {:mvn/version "2.25.63"}
}
:paths ["src" "resources" "target/classes"]
@@ -74,16 +74,13 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
{io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}}
:ns-default build}
:test
{:extra-paths ["test"]
:extra-deps
{io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}
{:main-opts ["-m" "kaocha.runner"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:outdated
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}

View File

@@ -4,19 +4,19 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.0.2",
"packageManager": "yarn@4.2.2",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
},
"dependencies": {
"luxon": "^3.4.2",
"sax": "^1.2.4"
"luxon": "^3.4.4",
"sax": "^1.4.1"
},
"devDependencies": {
"nodemon": "^3.0.1",
"nodemon": "^3.1.2",
"source-map-support": "^0.5.21",
"ws": "^8.13.0"
"ws": "^8.17.0"
},
"scripts": {
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",

View File

@@ -168,7 +168,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name}}!</div>
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
</td>
</tr>
<tr>
@@ -475,4 +475,4 @@
</div>
</body>
</html>
</html>

View File

@@ -1,4 +1,4 @@
Hello {{name}}!
Hello {{name|abbreviate:25}}!
We received a request to change your current email to {{ pending-email }}.

View File

@@ -11,7 +11,7 @@
{% if profile %}
<span>
<span>Name: </span>
<span><code>{{profile.fullname}}</code></span>
<span><code>{{profile.fullname|abbreviate:25}}</code></span>
</span>
<br />
@@ -34,7 +34,7 @@
</p>
<p>
<strong>Subject:</strong><br />
<span>{{subject}}</span>
<span>{{subject|abbreviate:300}}</span>
</p>
<p>

View File

@@ -173,7 +173,7 @@
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">{{invited-by}} has invited you to join the team “{{ team }}”.</div>
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
</td>
</tr>
<tr>
@@ -465,4 +465,4 @@
</div>
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
Hello!
{{invited-by}} has invited you to join the team “{{ team }}”.
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.
Accept invitation using this link:

View File

@@ -168,7 +168,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name}}!</div>
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
</td>
</tr>
<tr>
@@ -470,4 +470,4 @@
</div>
</body>
</html>
</html>

View File

@@ -1,4 +1,4 @@
Hello {{name}}!
Hello {{name|abbreviate:25}}!
We received a request to reset your password. Click the link below to choose a
new one:

View File

@@ -168,7 +168,7 @@
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name}}!</div>
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
</td>
</tr>
<tr>

View File

@@ -1,4 +1,4 @@
Hello {{name}}!
Hello {{name|abbreviate:25}}!
Thanks for signing up for your Penpot account! Please verify your email using the
link below and get started building mockups and prototypes today!

View File

@@ -1,4 +1,16 @@
[{:id "tutorial-for-beginners"
[{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
{:id "prototype-examples"
:name "Prototipe template"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
{:id "plants-app"
:name "UI mockup example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
{:id "penpot-design-system"
:name "Design system example"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
{:id "lucide-icons"
@@ -7,12 +19,6 @@
{:id "font-awesome"
:name "Font Awesome"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"}
{:id "plants-app"
:name "Plants app"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
{:id "wireframing-kit"
:name "Wireframing Kit"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
{:id "black-white-mobile-templates"
:name "Black & White Mobile Templates"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"}

View File

@@ -6,9 +6,7 @@
(ns app.auth
(:require
[app.config :as cf]
[buddy.hashers :as hashers]
[cuerdas.core :as str]))
[buddy.hashers :as hashers]))
(def default-params
{:alg :argon2id
@@ -27,17 +25,3 @@
(catch Throwable _
{:update false
:valid false})))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string."
([email]
(let [domains (cf/get :registration-domain-whitelist)]
(email-domain-in-whitelist? domains email)))
([domains email]
(if (or (nil? domains) (empty? domains))
true
(let [[_ candidate] (-> (str/lower email)
(str/split #"@" 2))]
(contains? domains candidate)))))

View File

@@ -7,7 +7,6 @@
(ns app.auth.oidc
"OIDC client implementation."
(:require
[app.auth :as auth]
[app.auth.oidc.providers :as-alias providers]
[app.common.data :as d]
[app.common.data.macros :as dm]
@@ -17,13 +16,17 @@
[app.common.uri :as u]
[app.config :as cf]
[app.db :as db]
[app.email.blacklist :as email.blacklist]
[app.email.whitelist :as email.whitelist]
[app.http.client :as http]
[app.http.errors :as errors]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as rpc]
[app.rpc.commands.profile :as profile]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.util.inet :as inet]
[app.util.json :as json]
[app.util.time :as dt]
[buddy.sign.jwk :as jwk]
@@ -32,6 +35,7 @@
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[ring.request :as rreq]
[ring.response :as-alias rres]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -129,8 +133,8 @@
(-> body json/decode :keys process-oidc-jwks)
(do
(l/warn :hint "unable to retrieve JWKs (unexpected response status code)"
:http-status status
:http-body body)
:response-status status
:response-body body)
nil)))
(catch Throwable cause
(l/warn :hint "unable to retrieve JWKs (unexpected exception)"
@@ -144,18 +148,18 @@
(when (contains? cf/flags :login-with-oidc)
(if-let [opts (prepare-oidc-opts cfg)]
(let [jwks (fetch-oidc-jwks cfg opts)]
(l/info :hint "provider initialized"
:provider "oidc"
:method (if (:discover? opts) "discover" "manual")
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts))
:scopes (str/join "," (:scopes opts))
:auth-uri (:auth-uri opts)
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts)
:keys (str/join "," (map str (keys jwks))))
(l/inf :hint "provider initialized"
:provider "oidc"
:method (if (:discover? opts) "discover" "manual")
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts))
:scopes (str/join "," (:scopes opts))
:auth-uri (:auth-uri opts)
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts)
:keys (str/join "," (map str (keys jwks))))
(assoc opts :jwks jwks))
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc")
@@ -179,10 +183,10 @@
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider "google"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
(l/inf :hint "provider initialized"
:provider "google"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
@@ -207,8 +211,9 @@
(ex/raise :type :internal
:code :unable-to-retrieve-github-emails
:hint "unable to retrieve github emails"
:http-status status
:http-body body))
:request-uri (:uri params)
:response-status status
:response-body body))
(->> body json/decode (filter :primary) first :email))))
@@ -233,10 +238,10 @@
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider "github"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
(l/inf :hint "provider initialized"
:provider "github"
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
@@ -248,7 +253,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::providers/gitlab
[_ _]
[_ cfg]
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
opts {:base-uri base
:client-id (cf/get :gitlab-client-id)
@@ -257,17 +262,18 @@
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/oauth/userinfo")
:jwks-uri (str base "/oauth/discovery/keys")
:name "gitlab"}]
(when (contains? cf/flags :login-with-gitlab)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider "gitlab"
:base-uri base
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(let [jwks (fetch-oidc-jwks cfg opts)]
(l/inf :hint "provider initialized"
:provider "gitlab"
:base-uri base
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
(assoc opts :jwks jwks))
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider "gitlab")
@@ -283,12 +289,12 @@
(into [(keyword (:name provider) fitem)] (map keyword) items)))
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
[{:keys [::provider] :as cfg}]
(let [public (u/uri (cf/get :public-uri))]
(str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback")))))
(defn- build-auth-uri
[{:keys [provider] :as cfg} state]
[{:keys [::provider] :as cfg} state]
(let [params {:client_id (:client-id provider)
:redirect_uri (build-redirect-uri cfg)
:response_type "code"
@@ -299,15 +305,19 @@
(assoc :query query)
(str))))
(defn- qualify-prop-key
[provider k]
(keyword (:name provider) (name k)))
(defn- qualify-props
[provider props]
(reduce-kv (fn [result k v]
(assoc result (keyword (:name provider) (name k)) v))
(assoc result (qualify-prop-key provider k) v))
{}
props))
(defn fetch-access-token
[{:keys [provider] :as cfg} code]
(defn- fetch-access-token
[{:keys [::provider] :as cfg} code]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code code
@@ -319,26 +329,31 @@
:uri (:token-uri provider)
:body (u/map->query-string params)}]
(l/trace :hint "request access token"
:provider (:name provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(l/trc :hint "fetch access token"
:provider (:name provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(let [{:keys [status body]} (http/req! cfg req {:sync? true})]
(l/trace :hint "access token response" :status status :body body)
(l/trc :hint "access token fetched" :status status :body body)
(if (= status 200)
(let [data (json/decode body)]
{:token/access (get data :access_token)
:token/id (get data :id_token)
:token/type (get data :token_type)})
(let [data (json/decode body)
data {:token/access (get data :access_token)
:token/id (get data :id_token)
:token/type (get data :token_type)}]
(l/trc :hint "access token fetched"
:token-id (:token/id data)
:token-type (:token/type data)
:token (:token/access data))
data)
(ex/raise :type :internal
:code :unable-to-retrieve-token
:hint "unable to retrieve token"
:http-status status
:http-body body)))))
:code :unable-to-fetch-access-token
:hint "unable to fetch access token"
:request-uri (:uri req)
:response-status status
:response-body body)))))
(defn- process-user-info
[provider tdata info]
@@ -364,10 +379,10 @@
:props props})))
(defn- fetch-user-info
[{:keys [provider] :as cfg} tdata]
(l/trace :hint "fetch user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata)))
[{:keys [::provider] :as cfg} tdata]
(l/trc :hint "fetch user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata)))
(let [params {:uri (:user-uri provider)
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
@@ -375,9 +390,9 @@
:method :get}
response (http/req! cfg params {:sync? true})]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(l/trc :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
@@ -389,7 +404,7 @@
(-> response :body json/decode)))
(defn- get-user-info
[{:keys [provider]} tdata]
[{:keys [::provider]} tdata]
(try
(when (:token/id tdata)
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
@@ -413,14 +428,8 @@
::fullname
::props]))
(defn get-info
[{:keys [provider ::setup/props] :as cfg} {:keys [params] :as request}]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
(defn- get-info
[{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}]
(let [state (get params :state)
code (get params :code)
state (tokens/verify props {:token state :iss :oauth})
@@ -433,7 +442,7 @@
info (process-user-info provider tdata info)]
(l/trace :hint "user info" :info info)
(l/trc :hint "user info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
@@ -466,111 +475,173 @@
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state))
(some? (:external-session-id state))
(assoc :external-session-id (:external-session-id state))
;; If state token comes with props, merge them. The state token
;; props can contain pm_ and utm_ prefixed query params.
(map? (:props state))
(update :props merge (:props state)))))
(defn- get-profile
[{:keys [::db/pool] :as cfg} info]
(dm/with-open [conn (db/open pool)]
(some->> (:email info)
(profile/clean-email)
(profile/get-profile-by-email conn))))
[cfg info]
(db/run! cfg (fn [{:keys [::db/conn]}]
(some->> (:email info)
(profile/clean-email)
(profile/get-profile-by-email conn)))))
(defn- redirect-response
[uri]
{::rres/status 302
::rres/headers {"location" (str uri)}})
(defn- generate-error-redirect
[_ cause]
(let [data (if (ex/error? cause) (ex-data cause) nil)
code (or (:code data) :unexpected)
type (or (:type data) :internal)
hint (or (:hint data)
(if (ex/exception? cause)
(ex-message cause)
(str cause)))
(defn- redirect-with-error
([error] (redirect-with-error error nil))
([error hint]
(let [params {:error error :hint hint}
params (d/without-nils params)
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))))
params {:error "unable-to-auth"
:hint hint
:type type
:code code}
(defn- redirect-to-register
[cfg info request]
(let [info (assoc info
:iss :prepared-register
:exp (dt/in-future {:hours 48}))
params {:token (tokens/generate (::setup/props cfg) info)
:provider (:provider (:path-params request))
:fullname (:fullname info)}
params (d/without-nils params)]
(redirect-response
(-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params))))))
(defn- redirect-to-verify-token
[token]
(let [params {:token token}
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string params)))]
(redirect-response uri)))
(defn- generate-redirect
(defn- provider-has-email-verified?
[{:keys [::provider] :as cfg} {:keys [props] :as info}]
(let [prop (qualify-prop-key provider :email_verified)]
(true? (get props prop))))
(defn- profile-has-provider-props?
[{:keys [::provider] :as cfg} profile]
(let [prop (qualify-prop-key provider :email)]
(contains? (:props profile) prop)))
(defn- provider-matches-profile?
[{:keys [::provider] :as cfg} profile info]
(or (= (:auth-backend profile) (:name provider))
(profile-has-provider-props? cfg profile)
(provider-has-email-verified? cfg info)))
(defn- process-callback
[cfg request info profile]
(if profile
(let [sxf (session/create-fn cfg (:id profile))
token (or (:invitation-token info)
(tokens/generate (::setup/props cfg)
{:iss :auth
:exp (dt/in-future "15m")
:profile-id (:id profile)}))
params {:token token}
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string params)))]
(cond
(some? profile)
(cond
(:is-blocked profile)
(redirect-with-error "profile-blocked")
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(not (provider-matches-profile? cfg profile info))
(redirect-with-error "auth-provider-not-allowed")
(audit/submit! cfg {::audit/type "command"
::audit/name "login-with-oidc"
::audit/profile-id (:id profile)
::audit/ip-addr (audit/parse-client-ip request)
::audit/props (audit/profile->props profile)})
(not (:is-active profile))
(let [info (assoc info :profile-id (:id profile))]
(redirect-to-register cfg info request))
(->> (redirect-response uri)
(sxf request)))
:else
(let [sxf (session/create-fn cfg (:id profile))
token (or (:invitation-token info)
(tokens/generate (::setup/props cfg)
{:iss :auth
:exp (dt/in-future "15m")
:props (:props info)
:profile-id (:id profile)}))
props (audit/profile->props profile)
context (d/without-nils {:external-session-id (:external-session-id info)})]
(audit/submit! cfg {::audit/type "action"
::audit/name "login-with-oidc"
::audit/profile-id (:id profile)
::audit/ip-addr (inet/parse-request request)
::audit/props props
::audit/context context})
(if (auth/email-domain-in-whitelist? (:email info))
(let [info (assoc info
:iss :prepared-register
:is-active true
:exp (dt/in-future {:hours 48}))
token (tokens/generate (::setup/props cfg) info)
params (d/without-nils
{:token token
:provider (:provider (:path-params request))
:fullname (:fullname info)})
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(->> (redirect-to-verify-token token)
(sxf request))))
(redirect-response uri))
(generate-error-redirect cfg "email-domain-not-allowed"))))
(and (email.blacklist/enabled? cfg)
(email.blacklist/contains? cfg (:email info)))
(redirect-with-error "email-domain-not-allowed")
(and (email.whitelist/enabled? cfg)
(not (email.whitelist/contains? cfg (:email info))))
(redirect-with-error "email-domain-not-allowed")
:else
(let [info (assoc info :is-active (provider-has-email-verified? cfg info))]
(if (or (contains? cf/flags :registration)
(contains? cf/flags :oidc-registration))
(redirect-to-register cfg info request)
(redirect-with-error "registration-disabled")))))
(defn- get-external-session-id
[request]
(let [session-id (rreq/get-header request "x-external-session-id")]
(when (string? session-id)
(if (or (> (count session-id) 256)
(= session-id "null")
(str/blank? session-id))
nil
session-id))))
(defn- auth-handler
[cfg {:keys [params] :as request}]
(let [props (audit/extract-utm-params params)
state (tokens/generate (::setup/props cfg)
{:iss :oauth
:invitation-token (:invitation-token params)
:props props
:exp (dt/in-future "4h")})
uri (build-auth-uri cfg state)]
(let [props (audit/extract-utm-params params)
esid (rpc/get-external-session-id request)
params {:iss :oauth
:invitation-token (:invitation-token params)
:external-session-id esid
:props props
:exp (dt/in-future "4h")}
state (tokens/generate (::setup/props cfg)
(d/without-nils params))
uri (build-auth-uri cfg state)]
{::rres/status 200
::rres/body {:redirect-uri uri}}))
(defn- callback-handler
[cfg request]
[{:keys [::provider] :as cfg} request]
(try
(let [info (get-info cfg request)
profile (get-profile cfg info)]
(generate-redirect cfg request info profile))
(if-let [error (dm/get-in request [:params :error])]
(redirect-with-error "unable-to-auth" error)
(let [info (get-info cfg request)
profile (get-profile cfg info)]
(process-callback cfg request info profile)))
(catch Throwable cause
(l/warn :hint "error on oauth process" :cause cause)
(generate-error-redirect cfg cause))))
(binding [l/*context* (-> (errors/request->context request)
(assoc :auth/provider (:name provider)))]
(let [edata (ex-data cause)]
(cond
(= :validation (:type edata))
(l/wrn :hint "invalid token received" :cause cause)
:else
(l/err :hint "error on oauth process" :cause cause))))
(redirect-with-error "unable-to-auth" (ex-message cause)))))
(def provider-lookup
{:compile
@@ -579,13 +650,12 @@
(fn [request]
(let [provider (some-> request :path-params :provider keyword)]
(if-let [provider (get providers provider)]
(handler (assoc cfg :provider provider) request)
(handler (assoc cfg ::provider provider) request)
(ex/raise :type :restriction
:code :provider-not-configured
:provider provider
:hint "provider not configured"))))))})
(s/def ::client-id ::cf/oidc-client-id)
(s/def ::client-secret ::cf/oidc-client-secret)
(s/def ::base-uri ::cf/oidc-base-uri)
@@ -598,7 +668,6 @@
(s/def ::email-attr ::cf/oidc-email-attr)
(s/def ::name-attr ::cf/oidc-name-attr)
;; FIXME: migrate to qualified-keywords
(s/def ::provider
(s/keys :req-un [::client-id
::client-secret]

View File

@@ -15,6 +15,7 @@
[app.common.files.migrations :as fmg]
[app.common.files.validate :as fval]
[app.common.logging :as l]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -331,54 +332,12 @@
(defn embed-assets
[cfg data file-id]
(letfn [(walk-map-form [form state]
(cond
(uuid? (:fill-color-ref-file form))
(do
(vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)])
(assoc form :fill-color-ref-file file-id))
(uuid? (:stroke-color-ref-file form))
(do
(vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)])
(assoc form :stroke-color-ref-file file-id))
(uuid? (:typography-ref-file form))
(do
(vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)])
(assoc form :typography-ref-file file-id))
(uuid? (:component-file form))
(do
(vswap! state conj [(:component-file form) :components (:component-id form)])
(assoc form :component-file file-id))
:else
form))
(process-group-of-assets [data [lib-id items]]
;; NOTE: there is a possibility that shape refers to an
;; non-existant file because the file was removed. In this
;; case we just ignore the asset.
(if-let [lib (get-file cfg lib-id)]
(reduce (partial process-asset lib) data items)
data))
(process-asset [lib data [bucket asset-id]]
(let [asset (get-in lib [:data bucket asset-id])
;; Add a special case for colors that need to have
;; correctly set the :file-id prop (pending of the
;; refactor that will remove it).
asset (cond-> asset
(= bucket :colors) (assoc :file-id file-id))]
(update data bucket assoc asset-id asset)))]
(let [assets (volatile! [])]
(walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data)
(->> (deref assets)
(filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id))))
(d/group-by first rest)
(reduce (partial process-group-of-assets) data)))))
(let [library-ids (get-libraries cfg [file-id])]
(reduce (fn [data library-id]
(let [library (get-file cfg library-id)]
(ctf/absorb-assets data (:data library))))
data
library-ids)))
(defn- fix-version
[file]

View File

@@ -130,7 +130,6 @@
(.writeLong output (long data))
(swap! *position* + 8))
(defn read-long!
[^DataInputStream input]
(let [v (.readLong input)]

View File

@@ -87,7 +87,10 @@
:ldap-attrs-fullname "cn"
;; a server prop key where initial project is stored.
:initial-project-skey "initial-project"})
:initial-project-skey "initial-project"
;; time to avoid email sending after profile modification
:email-verify-threshold "15m"})
(s/def ::default-rpc-rlimit ::us/vector-of-strings)
(s/def ::rpc-rlimit-config ::fs/path)
@@ -101,6 +104,9 @@
(s/def ::audit-log-archive-uri ::us/string)
(s/def ::audit-log-http-handler-concurrency ::us/integer)
(s/def ::email-domain-blacklist ::fs/path)
(s/def ::email-domain-whitelist ::fs/path)
(s/def ::deletion-delay ::dt/duration)
(s/def ::admins ::us/set-of-valid-emails)
@@ -210,6 +216,7 @@
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
(s/def ::email-verify-threshold ::dt/duration)
(s/def ::config
(s/keys :opt-un [::secret-key
@@ -230,6 +237,8 @@
::database-max-pool-size
::default-blob-version
::default-rpc-rlimit
::email-domain-blacklist
::email-domain-whitelist
::error-report-webhook
::default-executor-parallelism
::scheduled-executor-parallelism
@@ -329,7 +338,8 @@
::telemetry-uri
::telemetry-referer
::telemetry-with-taiga
::tenant]))
::tenant
::email-verify-threshold]))
(def default-flags
[:enable-backend-api-doc

View File

@@ -407,6 +407,7 @@
(ex/raise :type :not-found
:code :object-not-found
:table table
:params params
:hint "database object not found"))
row))

View File

@@ -262,13 +262,12 @@
(let [email (if factory
(factory context)
(dissoc context ::conn))]
(wrk/submit! (merge
{::wrk/task :sendmail
::wrk/delay 0
::wrk/max-retries 4
::wrk/priority 200
::wrk/conn conn}
email))))
(wrk/submit! {::wrk/task :sendmail
::wrk/delay 0
::wrk/max-retries 4
::wrk/priority 200
::db/conn conn
::wrk/params email})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SENDMAIL FN / TASK HANDLER
@@ -307,6 +306,8 @@
(let [session (create-smtp-session cfg)]
(with-open [transport (.getTransport session (if (::ssl cfg) "smtps" "smtp"))]
(.connect ^Transport transport
^String (::host cfg)
^String (::port cfg)
^String (::username cfg)
^String (::password cfg))
@@ -449,3 +450,11 @@
{:email email :type "bounce"}
{:limit 10}))]
(>= (count reports) threshold))))
(defn has-reports?
([conn email] (has-reports? conn email nil))
([conn email {:keys [threshold] :or {threshold 1}}]
(let [reports (db/exec! conn (sql/select :global-complaint-report
{:email email}
{:limit 10}))]
(>= (count reports) threshold))))

View File

@@ -0,0 +1,47 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.email.blacklist
"Email blacklist provider"
(:refer-clojure :exclude [contains?])
(:require
[app.common.logging :as l]
[app.config :as cf]
[app.email :as-alias email]
[clojure.core :as c]
[clojure.java.io :as io]
[cuerdas.core :as str]
[integrant.core :as ig]))
(defmethod ig/init-key ::email/blacklist
[_ _]
(when (c/contains? cf/flags :email-blacklist)
(try
(let [path (cf/get :email-domain-blacklist)
result (with-open [reader (io/reader path)]
(reduce (fn [result line]
(if (str/starts-with? line "#")
result
(conj result (-> line str/trim str/lower))))
#{}
(line-seq reader)))]
(l/inf :hint "initializing email blacklist" :domains (count result))
(not-empty result))
(catch Throwable cause
(l/wrn :hint "unexpected exception on initializing email blacklist"
:cause cause)))))
(defn contains?
"Check if email is in the blacklist."
[{:keys [::email/blacklist]} email]
(let [[_ domain] (str/split email "@" 2)]
(c/contains? blacklist (str/lower domain))))
(defn enabled?
"Check if the blacklist is enabled"
[{:keys [::email/blacklist]}]
(some? blacklist))

View File

@@ -0,0 +1,59 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.email.whitelist
"Email whitelist provider"
(:refer-clojure :exclude [contains?])
(:require
[app.common.logging :as l]
[app.config :as cf]
[app.email :as-alias email]
[clojure.core :as c]
[clojure.java.io :as io]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[integrant.core :as ig]))
(defn- read-whitelist
[path]
(when (and path (fs/exists? path))
(try
(with-open [reader (io/reader path)]
(reduce (fn [result line]
(if (str/starts-with? line "#")
result
(conj result (-> line str/trim str/lower))))
#{}
(line-seq reader)))
(catch Throwable cause
(l/wrn :hint "unexpected exception on reading email whitelist"
:cause cause)))))
(defmethod ig/init-key ::email/whitelist
[_ _]
(let [whitelist (or (cf/get :registration-domain-whitelist) #{})
whitelist (if (c/contains? cf/flags :email-whitelist)
(into whitelist (read-whitelist (cf/get :email-domain-whitelist)))
whitelist)
whitelist (not-empty whitelist)]
(when whitelist
(l/inf :hint "initializing email whitelist" :domains (count whitelist)))
whitelist))
(defn contains?
"Check if email is in the whitelist."
[{:keys [::email/whitelist]} email]
(let [[_ domain] (str/split email "@" 2)]
(c/contains? whitelist (str/lower domain))))
(defn enabled?
"Check if the whitelist is enabled"
[{:keys [::email/whitelist]}]
(some? whitelist))

View File

@@ -12,7 +12,6 @@
[app.common.files.changes :as cp]
[app.common.files.changes-builder :as fcb]
[app.common.files.helpers :as cfh]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.migrations :as fmg]
[app.common.files.shapes-helpers :as cfsh]
[app.common.files.validate :as cfv]
@@ -23,6 +22,7 @@
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.path :as gshp]
[app.common.logging :as l]
[app.common.logic.libraries :as cll]
[app.common.math :as mth]
[app.common.schema :as sm]
[app.common.svg :as csvg]
@@ -1450,16 +1450,15 @@
page
(cons shape children))
[_ _ changes2]
(cflh/generate-add-component nil
[shape]
(:objects page)
(:id page)
file-id
true
nil
cfsh/prepare-create-artboard-from-selection)
changes (fcb/concat-changes changes changes2)]
[_ _ changes]
(cll/generate-add-component changes
[shape]
(:objects page)
(:id page)
file-id
true
nil
cfsh/prepare-create-artboard-from-selection)]
(shape-cb shape)
(:redo-changes changes)))

View File

@@ -58,28 +58,26 @@
(defn load-pointer
"A database loader pointer helper"
[system file-id id]
(let [{:keys [content]} (db/get system :file-data-fragment
{:id id :file-id file-id}
{::sql/columns [:content]
::db/check-deleted false})]
(let [fragment (db/get* system :file-data-fragment
{:id id :file-id file-id}
{::sql/columns [:data]})]
(l/trc :hint "load pointer"
:file-id (str file-id)
:id (str id)
:found (some? content))
:found (some? fragment))
(when-not content
(when-not fragment
(ex/raise :type :internal
:code :fragment-not-found
:hint "fragment not found"
:file-id file-id
:fragment-id id))
(blob/decode content)))
(blob/decode (:data fragment))))
(defn persist-pointers!
"Given a database connection and the final file-id, persist all
pointers to the underlying storage (the database)."
"Persist all currently tracked pointer objects"
[system file-id]
(let [conn (db/get-connection system)]
(doseq [[id item] @pmap/*tracked*]
@@ -89,7 +87,7 @@
(db/insert! conn :file-data-fragment
{:id id
:file-id file-id
:content content}))))))
:data content}))))))
(defn process-pointers
"Apply a function to all pointers on the file. Usuly used for

View File

@@ -114,7 +114,7 @@
(partial not-found-handler request)))
(on-error [cause request]
(let [{:keys [body] :as response} (errors/handle cause request)]
(let [{:keys [::rres/body] :as response} (errors/handle cause request)]
(cond-> response
(map? body)
(-> (update ::rres/headers assoc "content-type" "application/transit+json")
@@ -151,9 +151,9 @@
[mw/params]
[mw/format-response]
[mw/parse-request]
[mw/errors errors/handle]
[session/soft-auth cfg]
[actoken/soft-auth cfg]
[mw/errors errors/handle]
[mw/restrict-methods]]}
(::mtx/routes cfg)

View File

@@ -9,6 +9,7 @@
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.db :as db]
[app.db.sql :as sql]
[app.http.client :as http]
@@ -16,10 +17,10 @@
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[clojure.data.json :as j]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[jsonista.core :as j]
[promesa.exec :as px]
[ring.request :as rreq]
[ring.response :as-alias rres]))
@@ -136,83 +137,110 @@
(defn- parse-json
[v]
(ex/ignoring
(j/read-value v)))
(try
(j/read-str v)
(catch Throwable cause
(l/wrn :hint "unable to decode request body"
:cause cause))))
(defn- register-bounce-for-profile
[{:keys [::db/pool]} {:keys [type kind profile-id] :as report}]
(when (= kind "permanent")
(db/with-atomic [conn pool]
(db/insert! conn :profile-complaint-report
(try
(db/insert! pool :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
;; TODO: maybe also try to find profiles by mail and if exists
;; register profile reports for them?
(doseq [recipient (:recipients report)]
(db/insert! conn :global-complaint-report
{:email (:email recipient)
:type (name type)
:content (db/tjson report)}))
(catch Throwable cause
(l/warn :hint "unable to persist profile complaint"
:cause cause)))
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
(when (some #(= (:email profile) (:email %)) (:recipients report))
;; If the report matches the profile email, this means that
;; the report is for itself, can be caused when a user
;; registers with an invalid email or the user email is
;; permanently rejecting receiving the email. In this case we
;; have no option to mark the user as muted (and in this case
;; the profile will be also inactive.
(db/update! conn :profile
{:is-muted true}
{:id profile-id}))))))
(defn- register-complaint-for-profile
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
(db/with-atomic [conn pool]
(db/insert! conn :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
;; TODO: maybe also try to find profiles by email and if exists
;; register profile reports for them?
(doseq [email (:recipients report)]
(db/insert! conn :global-complaint-report
{:email email
(doseq [recipient (:recipients report)]
(db/insert! pool :global-complaint-report
{:email (:email recipient)
:type (name type)
:content (db/tjson report)}))
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
(when (some #(= % (:email profile)) (:recipients report))
(let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
(when (some #(= (:email profile) (:email %)) (:recipients report))
;; If the report matches the profile email, this means that
;; the report is for itself, rare case but can happen; In this
;; case just mark profile as muted (very rare case).
(db/update! conn :profile
;; the report is for itself, can be caused when a user
;; registers with an invalid email or the user email is
;; permanently rejecting receiving the email. In this case we
;; have no option to mark the user as muted (and in this case
;; the profile will be also inactive.
(l/inf :hint "mark profile: muted"
:profile-id (str (:id profile))
:email (:email profile)
:reason "bounce report"
:report-id (:feedback-id report))
(db/update! pool :profile
{:is-muted true}
{:id profile-id})))))
{:id profile-id}
{::db/return-keys false})))))
(defn- register-complaint-for-profile
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
(try
(db/insert! pool :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
(catch Throwable cause
(l/warn :hint "unable to persist profile complaint"
:cause cause)))
;; TODO: maybe also try to find profiles by email and if exists
;; register profile reports for them?
(doseq [email (:recipients report)]
(db/insert! pool :global-complaint-report
{:email email
:type (name type)
:content (db/tjson report)}))
(let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
(when (some #(= % (:email profile)) (:recipients report))
;; If the report matches the profile email, this means that
;; the report is for itself, rare case but can happen; In this
;; case just mark profile as muted (very rare case).
(l/inf :hint "mark profile: muted"
:profile-id (str (:id profile))
:email (:email profile)
:reason "complaint report"
:report-id (:feedback-id report))
(db/update! pool :profile
{:is-muted true}
{:id profile-id}
{::db/return-keys false}))))
(defn- process-report
[cfg {:keys [type profile-id] :as report}]
(l/trace :action "processing report" :report (pr-str report))
(cond
;; In this case we receive a bounce/complaint notification without
;; confirmed identity, we just emit a warning but do nothing about
;; it because this is not a normal case. All notifications should
;; come with profile identity.
(nil? profile-id)
(l/warn :msg "a notification without identity received from AWS"
:report (pr-str report))
(l/wrn :hint "not-identified report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(= "bounce" type)
(register-bounce-for-profile cfg report)
(do
(l/trc :hint "bounce report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(register-bounce-for-profile cfg report))
(= "complaint" type)
(register-complaint-for-profile cfg report)
(do
(l/trc :hint "complaint report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(register-complaint-for-profile cfg report))
:else
(l/warn :msg "unrecognized report received from AWS"
:report (pr-str report))))
(l/wrn :hint "unrecognized report"
::l/body (pp/pprint-str report {:length 20 :level 4}))))

View File

@@ -16,7 +16,6 @@
[app.config :as cf]
[app.db :as db]
[app.http.session :as session]
[app.main :as-alias main]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.files-create :refer [create-file]]
[app.rpc.commands.profile :as profile]
@@ -341,57 +340,57 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- resend-email-notification
[{:keys [::db/pool ::setup/props] :as cfg} {:keys [params] :as request}]
[cfg {:keys [params] :as request}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(let [profile (some->> params
:email
(profile/clean-email)
(profile/get-profile-by-email conn))]
(let [profile (some->> params
:email
(profile/clean-email)
(profile/get-profile-by-email pool))]
(when-not profile
(ex/raise :type :validation
:code :missing-profile
:hint "unable to find profile by email"))
(when-not profile
(ex/raise :type :validation
:code :missing-profile
:hint "unable to find profile by email"))
(cond
(contains? params :block)
(do
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
(db/delete! conn :http-session {:profile-id (:id profile)})
(cond
(contains? params :block)
(do
(db/update! pool :profile {:is-blocked true} {:id (:id profile)})
(db/delete! pool :http-session {:profile-id (:id profile)})
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))})
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))})
(contains? params :unblock)
(do
(db/update! conn :profile {:is-blocked false} {:id (:id profile)})
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "PROFILE '%' UNBLOCKED" (:email profile))})
(contains? params :unblock)
(do
(db/update! pool :profile {:is-blocked false} {:id (:id profile)})
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "PROFILE '%' UNBLOCKED" (:email profile))})
(contains? params :resend)
(if (:is-blocked profile)
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body "PROFILE ALREADY BLOCKED"}
(do
(#'auth/send-email-verification! cfg profile)
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "RESENDED FOR '%'" (:email profile))}))
(contains? params :resend)
(if (:is-blocked profile)
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body "PROFILE ALREADY BLOCKED"}
(do
(auth/send-email-verification! pool props profile)
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "RESENDED FOR '%'" (:email profile))}))
:else
(do
(db/update! pool :profile {:is-active true} {:id (:id profile)})
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))}))))
:else
(do
(db/update! conn :profile {:is-active true} {:id (:id profile)})
{::rres/status 200
::rres/headers {"content-type" "text/plain"}
::rres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))}))))))
(defn- reset-file-version

View File

@@ -14,32 +14,28 @@
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.session :as-alias session]
[app.util.inet :as inet]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[ring.request :as rreq]
[ring.response :as rres]))
(defn- parse-client-ip
[request]
(or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first)
(rreq/get-header request "x-real-ip")
(rreq/remote-addr request)))
(defn request->context
"Extracts error report relevant context data from request."
[request]
(let [claims (-> {}
(into (::session/token-claims request))
(into (::actoken/token-claims request)))]
{:request/path (:path request)
:request/method (:method request)
:request/params (:params request)
:request/user-agent (rreq/get-header request "user-agent")
:request/ip-addr (parse-client-ip request)
:request/ip-addr (inet/parse-request request)
:request/profile-id (:uid claims)
:version/frontend (or (rreq/get-header request "x-frontend-version") "unknown")
:version/backend (:full cf/version)}))
(defmulti handle-error
(fn [cause _ _]
(-> cause ex-data :type)))

View File

@@ -10,16 +10,14 @@
[app.common.logging :as l]
[app.common.transit :as t]
[app.config :as cf]
[app.util.json :as json]
[app.http.errors :as errors]
[clojure.data.json :as json]
[cuerdas.core :as str]
[ring.request :as rreq]
[ring.response :as rres]
[yetti.adapter :as yt]
[yetti.middleware :as ymw])
(:import
com.fasterxml.jackson.core.JsonParseException
com.fasterxml.jackson.core.io.JsonEOFException
com.fasterxml.jackson.databind.exc.MismatchedInputException
io.undertow.server.RequestTooBigException
java.io.InputStream
java.io.OutputStream))
@@ -34,11 +32,22 @@
{:name ::params
:compile (constantly ymw/wrap-params)})
(def ^:private json-mapper
(json/mapper
{:encode-key-fn str/camel
:decode-key-fn (comp keyword str/kebab)
:pretty true}))
(defn- get-reader
^java.io.BufferedReader
[request]
(let [^InputStream body (rreq/body request)]
(java.io.BufferedReader.
(java.io.InputStreamReader. body))))
(defn- read-json-key
[k]
(-> k str/kebab keyword))
(defn- write-json-key
[k]
(if (or (keyword? k) (symbol? k))
(str/camel k)
(str k)))
(defn wrap-parse-request
[handler]
@@ -53,8 +62,8 @@
(update :params merge params))))
(str/starts-with? header "application/json")
(with-open [^InputStream is (rreq/body request)]
(let [params (json/decode is json-mapper)]
(with-open [reader (get-reader request)]
(let [params (json/read reader :key-fn read-json-key)]
(-> request
(assoc :body-params params)
(update :params merge params))))
@@ -62,35 +71,33 @@
:else
request)))
(handle-error [cause]
(handle-error [cause request]
(cond
(instance? RuntimeException cause)
(if-let [cause (ex-cause cause)]
(handle-error cause)
(throw cause))
(handle-error cause request)
(errors/handle cause request))
(instance? RequestTooBigException cause)
(ex/raise :type :validation
:code :request-body-too-large
:hint (ex-message cause))
(or (instance? JsonEOFException cause)
(instance? JsonParseException cause)
(instance? MismatchedInputException cause))
(instance? java.io.EOFException cause)
(ex/raise :type :validation
:code :malformed-json
:hint (ex-message cause)
:cause cause)
:else
(throw cause)))]
(errors/handle cause request)))]
(fn [request]
(if (= (rreq/method request) :post)
(let [request (ex/try! (process-request request))]
(if (ex/exception? request)
(handle-error request)
(handler request)))
(try
(-> request process-request handler)
(catch Throwable cause
(handle-error cause request)))
(handler request)))))
(def parse-request
@@ -128,7 +135,8 @@
(-write-body-to-stream [_ _ output-stream]
(try
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
(json/write! bos data json-mapper))
(with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)]
(json/write data writer :key-fn write-json-key)))
(catch java.io.IOException _)
(catch Throwable cause

View File

@@ -61,6 +61,8 @@
(let [result (handler)]
(events/tap :end result))
(catch Throwable cause
(l/err :hint "unexpected error on processing sse response"
:cause cause)
(events/tap :error (errors/handle' cause request)))
(finally
(sp/close! events/*channel*)

View File

@@ -21,24 +21,18 @@
[app.rpc :as-alias rpc]
[app.rpc.retry :as rtry]
[app.setup :as-alias setup]
[app.util.inet :as inet]
[app.util.services :as-alias sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[ring.request :as rreq]))
[integrant.core :as ig]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn parse-client-ip
[request]
(or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first)
(rreq/get-header request "x-real-ip")
(some-> (rreq/remote-addr request) str)))
(defn extract-utm-params
"Extracts additional data from params and namespace them under
`penpot` ns."
@@ -53,8 +47,7 @@
(assoc (->> sk str/kebab (keyword "penpot")) v))))]
(reduce-kv process-param {} params)))
(def ^:private
profile-props
(def profile-props
[:id
:is-active
:is-muted
@@ -87,8 +80,19 @@
(remove #(contains? reserved-props (key %))))
props))
;; --- SPECS
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context {:external-session-id (::rpc/external-session-id params)
:external-event-origin (::rpc/external-event-origin params)
:triggered-by (::rpc/handler-name params)}]
{::type "action"
::profile-id (::rpc/profile-id params)
::ip-addr (::rpc/ip-addr params)
::context (d/without-nils context)}))
;; --- SPECS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COLLECTOR
@@ -141,24 +145,31 @@
(::rpc/profile-id params)
uuid/zero)
props (-> (or (::replace-props resultm)
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
session-id (get params ::rpc/external-session-id)
event-origin (get params ::rpc/external-event-origin)
props (-> (or (::replace-props resultm)
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props))
(clean-props))
token-id (::actoken/id request)
context (d/without-nils
{:access-token-id (some-> token-id str)})]
context (-> (::context resultm)
(assoc :external-session-id session-id)
(assoc :external-event-origin event-origin)
(assoc :access-token-id (some-> token-id str))
(d/without-nils))
ip-addr (inet/parse-request request)]
{::type (or (::type resultm)
(::rpc/type cfg))
::name (or (::name resultm)
(::sv/name mdata))
::profile-id profile-id
::ip-addr (some-> request parse-client-ip)
::ip-addr ip-addr
::props props
::context context
@@ -180,15 +191,33 @@
(::webhooks/event? resultm)
false)}))
(defn- handle-event!
[cfg event]
(defn- event->params
[event]
(let [params {:id (uuid/next)
:name (::name event)
:type (::type event)
:profile-id (::profile-id event)
:ip-addr (::ip-addr event)
:context (::context event)
:props (::props event)}
:context (::context event {})
:props (::props event {})
:source "backend"}
tnow (::tracked-at event)]
(cond-> params
(some? tnow)
(assoc :tracked-at tnow))))
(defn- append-audit-entry!
[cfg params]
(let [params (-> params
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet))]
(db/insert! cfg :audit-log params)))
(defn- handle-event!
[cfg event]
(let [params (event->params event)
tnow (dt/now)]
(when (contains? cf/flags :audit-log)
@@ -197,12 +226,8 @@
;; this case we just retry the operation.
(let [params (-> params
(assoc :created-at tnow)
(assoc :tracked-at tnow)
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet)
(assoc :source "backend"))]
(db/insert! cfg :audit-log params)))
(update :tracked-at #(or % tnow)))]
(append-audit-entry! cfg params)))
(when (and (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
@@ -214,12 +239,10 @@
;; NOTE: this is only executed when general audit log is disabled
(let [params (-> params
(assoc :created-at tnow)
(assoc :tracked-at tnow)
(assoc :props (db/tjson {}))
(assoc :context (db/tjson {}))
(assoc :ip-addr (db/inet "0.0.0.0"))
(assoc :source "backend"))]
(db/insert! cfg :audit-log params)))
(update :tracked-at #(or % tnow))
(assoc :props {})
(assoc :context {}))]
(append-audit-entry! cfg params)))
(when (and (contains? cf/flags :webhooks)
(::webhooks/event? event))
@@ -232,25 +255,23 @@
:else label)
dedupe? (boolean (and batch-key batch-timeout))]
(wrk/submit! ::wrk/conn (::db/conn cfg)
::wrk/task :process-webhook-event
::wrk/queue :webhooks
::wrk/max-retries 0
::wrk/delay (or batch-timeout 0)
::wrk/dedupe dedupe?
::wrk/label label
::webhooks/event
(-> params
(dissoc :ip-addr)
(dissoc :type)))))
(wrk/submit! (-> cfg
(assoc ::wrk/task :process-webhook-event)
(assoc ::wrk/queue :webhooks)
(assoc ::wrk/max-retries 0)
(assoc ::wrk/delay (or batch-timeout 0))
(assoc ::wrk/dedupe dedupe?)
(assoc ::wrk/label label)
(assoc ::wrk/params (-> params
(dissoc :ip-addr)
(dissoc :type)))))))
params))
(defn submit!
"Submit audit event to the collector."
[cfg params]
[cfg event]
(try
(let [event (d/without-nils params)
(let [event (d/without-nils event)
cfg (-> cfg
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 6)
@@ -259,3 +280,18 @@
(rtry/invoke! cfg db/tx-run! handle-event! event))
(catch Throwable cause
(l/error :hint "unexpected error processing event" :cause cause))))
(defn insert!
"Submit audit event to the collector, intended to be used only from
command line helpers because this skips all webhooks and telemetry
logic."
[cfg event]
(when (contains? cf/flags :audit-log)
(let [event (d/without-nils event)]
(us/verify! ::event event)
(db/run! cfg (fn [cfg]
(let [tnow (dt/now)
params (-> (event->params event)
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(append-audit-entry! cfg params)))))))

View File

@@ -64,22 +64,22 @@
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::process-event-handler
[_ {:keys [::db/pool] :as cfg}]
[_ cfg]
(fn [{:keys [props] :as task}]
(let [event (::event props)]
(let [event (:event props)]
(l/dbg :hint "process webhook event" :name (:name event))
(when-let [items (lookup-webhooks cfg event)]
(l/trc :hint "webhooks found for event" :total (count items))
(db/with-atomic [conn pool]
(doseq [item items]
(wrk/submit! ::wrk/conn conn
::wrk/task :run-webhook
::wrk/queue :webhooks
::wrk/max-retries 3
::event event
::config item)))))))
(db/tx-run! cfg (fn [cfg]
(doseq [item items]
(wrk/submit! (-> cfg
(assoc ::wrk/task :run-webhook)
(assoc ::wrk/queue :webhooks)
(assoc ::wrk/max-retries 3)
(assoc ::wrk/params {:event event
:config item}))))))))))
;; --- RUN
@@ -128,8 +128,8 @@
:rsp-data (db/tjson rsp)}))]
(fn [{:keys [props] :as task}]
(let [event (::event props)
whook (::config props)
(let [event (:event props)
whook (:config props)
body (case (:mtype whook)
"application/json" (json/write-str event json-write-opts)

View File

@@ -102,13 +102,13 @@
{::mdef/name "penpot_tasks_timing"
::mdef/help "Background tasks timing (milliseconds)."
::mdef/labels ["name"]
::mdef/type :summary}
::mdef/type :histogram}
:redis-eval-timing
{::mdef/name "penpot_redis_eval_timing"
::mdef/help "Redis EVAL commands execution timings (ms)"
::mdef/labels ["name"]
::mdef/type :summary}
::mdef/type :histogram}
:rpc-climit-queue
{::mdef/name "penpot_rpc_climit_queue"
@@ -126,7 +126,7 @@
{::mdef/name "penpot_rpc_climit_timing"
::mdef/help "Summary of the time between queuing and executing on the CLIMIT"
::mdef/labels ["name"]
::mdef/type :summary}
::mdef/type :histogram}
:audit-http-handler-queue-size
{::mdef/name "penpot_audit_http_handler_queue_size"
@@ -144,7 +144,7 @@
{::mdef/name "penpot_audit_http_handler_timing"
::mdef/help "Summary of the time between queuing and executing on the audit log http handler"
::mdef/labels []
::mdef/type :summary}
::mdef/type :histogram}
:executors-active-threads
{::mdef/name "penpot_executors_active_threads"
@@ -254,7 +254,7 @@
{::http.client/client (ig/ref ::http.client/client)}
::oidc.providers/gitlab
{}
{::http.client/client (ig/ref ::http.client/client)}
::oidc.providers/generic
{::http.client/client (ig/ref ::http.client/client)}
@@ -267,7 +267,9 @@
:github (ig/ref ::oidc.providers/github)
:gitlab (ig/ref ::oidc.providers/gitlab)
:oidc (ig/ref ::oidc.providers/generic)}
::session/manager (ig/ref ::session/manager)}
::session/manager (ig/ref ::session/manager)
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)}
:app.http/router
{::session/manager (ig/ref ::session/manager)
@@ -322,7 +324,10 @@
::rpc/climit (ig/ref ::rpc/climit)
::rpc/rlimit (ig/ref ::rpc/rlimit)
::setup/templates (ig/ref ::setup/templates)
::setup/props (ig/ref ::setup/props)}
::setup/props (ig/ref ::setup/props)
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)}
:app.rpc.doc/routes
{:methods (ig/ref :app.rpc/methods)}
@@ -338,7 +343,6 @@
::wrk/tasks
{:sendmail (ig/ref ::email/handler)
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
:orphan-teams-gc (ig/ref :app.tasks.orphan-teams-gc/handler)
:file-gc (ig/ref :app.tasks.file-gc/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
@@ -349,13 +353,19 @@
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
:object-update
(ig/ref :app.tasks.object-update/handler)
:delete-object
(ig/ref :app.tasks.delete-object/handler)
:process-webhook-event
(ig/ref ::webhooks/process-event-handler)
:run-webhook
(ig/ref ::webhooks/run-webhook-handler)}}
::email/blacklist
{}
::email/whitelist
{}
::email/sendmail
{::email/host (cf/get :smtp-host)
::email/port (cf/get :smtp-port)
@@ -377,10 +387,7 @@
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.orphan-teams-gc/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.object-update/handler
:app.tasks.delete-object/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.file-gc/handler
@@ -442,7 +449,9 @@
{::db/pool (ig/ref ::db/pool)
::sto/backends
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
:assets-fs (ig/ref [::assets :app.storage.fs/backend])}}
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
:s3 (ig/ref [::assets :app.storage.s3/backend])
:fs (ig/ref [::assets :app.storage.fs/backend])}}
[::assets :app.storage.s3/backend]
{::sto.s3/region (cf/get :storage-assets-s3-region)
@@ -468,9 +477,6 @@
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :orphan-teams-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-gc-deleted}

View File

@@ -379,7 +379,10 @@
:fn (mg/resource "app/migrations/sql/0119-mod-file-table.sql")}
{:name "0120-mod-audit-log-table"
:fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")}])
:fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")}
{:name "0121-mod-file-data-fragment-table"
:fn (mg/resource "app/migrations/sql/0121-mod-file-data-fragment-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -0,0 +1,8 @@
ALTER TABLE file_data_fragment
ADD COLUMN data bytea NULL;
UPDATE file_data_fragment
SET data = content;
ALTER TABLE file_data_fragment
DROP COLUMN content;

View File

@@ -29,6 +29,7 @@
[app.rpc.rlimit :as rlimit]
[app.setup :as-alias setup]
[app.storage :as-alias sto]
[app.util.inet :as inet]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
@@ -70,6 +71,22 @@
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata))))
(defn get-external-session-id
[request]
(when-let [session-id (rreq/get-header request "x-external-session-id")]
(when-not (or (> (count session-id) 256)
(= session-id "null")
(str/blank? session-id))
session-id)))
(defn- get-external-event-origin
[request]
(when-let [origin (rreq/get-header request "x-event-origin")]
(when-not (or (> (count origin) 256)
(= origin "null")
(str/blank? origin))
origin)))
(defn- rpc-handler
"Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow."
@@ -79,8 +96,16 @@
profile-id (or (::session/profile-id request)
(::actoken/profile-id request))
ip-addr (inet/parse-request request)
session-id (get-external-session-id request)
event-origin (get-external-event-origin request)
data (-> params
(assoc ::handler-name handler-name)
(assoc ::ip-addr ip-addr)
(assoc ::request-at (dt/now))
(assoc ::external-session-id session-id)
(assoc ::external-event-origin event-origin)
(assoc ::session/id (::session/id request))
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)
@@ -188,10 +213,10 @@
(defn- wrap-all
[cfg f mdata]
(as-> f $
(wrap-metrics cfg $ mdata)
(cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(climit/wrap cfg $ mdata)
(wrap-metrics cfg $ mdata)
(rlimit/wrap cfg $ mdata)
(wrap-audit cfg $ mdata)
(wrap-spec-conform cfg $ mdata)

View File

@@ -14,11 +14,12 @@
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
[app.loggers.audit :as audit]
[app.loggers.audit :as-alias audit]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.inet :as inet]
[app.util.services :as sv]
[app.util.time :as dt]))
@@ -61,7 +62,7 @@
(defn- handle-events
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request)
ip-addr (audit/parse-client-ip request)
ip-addr (inet/parse-request request)
tnow (dt/now)
xform (comp
(map (fn [event]

View File

@@ -6,7 +6,6 @@
(ns app.rpc.commands.auth
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
@@ -17,9 +16,10 @@
[app.config :as cf]
[app.db :as db]
[app.email :as eml]
[app.email.blacklist :as email.blacklist]
[app.email.whitelist :as email.whitelist]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.profile :as profile]
@@ -38,6 +38,12 @@
(def schema:token
[::sm/word-string {:max 6000}])
(defn- elapsed-verify-threshold?
[profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))
verify-threshold (cf/get :email-verify-threshold)]
(pos? (compare elapsed verify-threshold))))
;; ---- COMMAND: login with password
(defn login-with-password
@@ -122,12 +128,21 @@
;; ---- COMMAND: Logout
(def ^:private schema:logout
[:map {:title "logoug"}
[:profile-id {:optional true} ::sm/uuid]])
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
{::rpc/auth false
::doc/added "1.15"}
[cfg _]
(rph/with-transform {} (session/delete-fn cfg)))
::doc/changes [["2.1" "Now requires profile-id passed in the body"]]
::doc/added "1.0"
::sm/params schema:logout}
[cfg params]
(if (= (:profile-id params)
(::rpc/profile-id params))
(rph/with-transform {} (session/delete-fn cfg))
{}))
;; ---- COMMAND: Recover Profile
@@ -139,7 +154,7 @@
(update-password [conn profile-id]
(let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd} {:id profile-id})
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
nil))]
(db/with-atomic [conn pool]
@@ -162,46 +177,52 @@
;; ---- COMMAND: Prepare Register
(defn validate-register-attempt!
[{:keys [::db/pool] :as cfg} params]
(defn- validate-register-attempt!
[cfg params]
(when-not (contains? cf/flags :registration)
(when-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)))
(when (or
(not (contains? cf/flags :registration))
(not (contains? cf/flags :login-with-password)))
(ex/raise :type :restriction
:code :registration-disabled))
(when (contains? params :invitation-token)
(let [invitation (tokens/verify (::setup/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
(let [invitation (tokens/verify (::setup/props cfg)
{:token (:invitation-token params)
:iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
:hint "email should match the invitation"))))
(when-not (auth/email-domain-in-whitelist? (:email params))
(ex/raise :type :validation
(when (and (email.blacklist/enabled? cfg)
(email.blacklist/contains? cfg (:email params)))
(ex/raise :type :restriction
:code :email-domain-is-not-allowed))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spammer.
(when (eml/has-bounce-reports? pool (:email params))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email has one or many bounces reported"))
(when (and (email.whitelist/enabled? cfg)
(not (email.whitelist/contains? cfg (:email params))))
(ex/raise :type :restriction
:code :email-domain-is-not-allowed))
;; Perform a basic validation of email & password
(when (= (str/lower (:email params))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password")))
:hint "you can't use your email as password"))
(def register-retry-threshold
(dt/duration "15m"))
(when (eml/has-bounce-reports? cfg (:email params))
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (:email params)
:hint "looks like the email has bounce reports"))
(defn- elapsed-register-retry-threshold?
[profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
(pos? (compare elapsed register-retry-threshold))))
(when (eml/has-complaint-reports? cfg (:email params))
(ex/raise :type :restriction
:code :email-has-complaints
:email (:email params)
:hint "looks like the email has complaint reports")))
(defn prepare-register
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
@@ -209,21 +230,7 @@
(validate-register-attempt! cfg params)
(let [email (profile/clean-email email)
profile (when-let [profile (profile/get-profile-by-email pool email)]
(cond
(:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked)
(and (not (:is-active profile))
(elapsed-register-retry-threshold? profile))
profile
:else
(ex/raise :type :validation
:code :email-already-exists
:hint "profile already exists")))
profile (profile/get-profile-by-email pool email)
params {:email email
:password (:password params)
:invitation-token (:invitation-token params)
@@ -233,7 +240,6 @@
:exp (dt/in-future {:days 7})}
params (d/without-nils params)
token (tokens/generate (::setup/props cfg) params)]
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
@@ -277,6 +283,7 @@
is-demo (:is-demo params false)
is-muted (:is-muted params false)
is-active (:is-active params false)
theme (:theme params nil)
email (str/lower email)
params {:id id
@@ -287,20 +294,24 @@
:password password
:deleted-at (:deleted-at params)
:props props
:theme theme
:is-active is-active
:is-muted is-muted
:is-demo is-demo}]
(try
(-> (db/insert! conn :profile params)
(profile/decode-row))
(catch org.postgresql.util.PSQLException e
(let [state (.getSQLState e)]
(catch org.postgresql.util.PSQLException cause
(let [state (.getSQLState cause)]
(if (not= state "23505")
(throw e)
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"
:cause e)))))))
(throw cause)
(do
(l/error :hint "not an error" :cause cause)
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"
:cause cause))))))))
(defn create-profile-rels!
[conn {:keys [id] :as profile}]
@@ -317,17 +328,16 @@
{::db/return-keys true})
(profile/decode-row))))
(defn send-email-verification!
[conn props profile]
(let [vtoken (tokens/generate props
[{:keys [::db/conn] :as cfg} profile]
(let [vtoken (tokens/generate (::setup/props cfg)
{:iss :verify-email
:exp (dt/in-future "72h")
:profile-id (:id profile)
:email (:email profile)})
;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook
ptoken (tokens/generate props
ptoken (tokens/generate (::setup/props cfg)
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
@@ -340,95 +350,132 @@
:extra-data ptoken})))
(defn register-profile
[{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
[{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}]
(let [theme (when (= theme "light") theme)
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
params (-> claims
(into params)
(assoc :fullname fullname))
is-active (or (:is-active params)
(not (contains? cf/flags :email-verification)))
(assoc :fullname fullname)
(assoc :theme theme))
profile (if-let [profile-id (:profile-id claims)]
(profile/get-profile conn profile-id)
(let [params (-> params
(assoc :is-active is-active)
(update :password #(profile/derive-password cfg %)))]
(->> (create-profile! conn params)
(create-profile-rels! conn))))
;; NOTE: we first try to match existing profile
;; by email, that in normal circumstances will
;; not return anything, but when a user tries to
;; reuse the same token multiple times, we need
;; to detect if the profile is already registered
(or (profile/get-profile-by-email conn (:email claims))
(let [is-active (or (boolean (:is-active claims))
(not (contains? cf/flags :email-verification)))
params (-> params
(assoc :is-active is-active)
(update :password #(profile/derive-password cfg %)))
profile (->> (create-profile! conn params)
(create-profile-rels! conn))]
(vary-meta profile assoc :created true))))
created? (-> profile meta :created true?)
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
props (audit/profile->props profile)]
;; If profile is filled in claims, means it tries to register
;; again, so we proceed to update the modified-at attr
;; accordingly.
(when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
(audit/submit! cfg
{::audit/type "fact"
::audit/name "register-profile-retry"
::audit/profile-id id}))
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged). This happens
;; only if the invitation email matches with the register
;; email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
;; When profile is blocked, we just ignore it and return plain data
(:is-blocked profile)
(do
(l/wrn :hint "register attempt for already blocked profile"
:profile-id (str (:id profile))
:profile-email (:email profile))
(rph/with-meta {:email (:email profile)}
{::audit/replace-props props
::audit/context {:action "ignore-because-blocked"}
::audit/profile-id (:id profile)
::audit/name "register-profile-retry"}))
;; If invitation token comes in params, this is because the user
;; comes from team-invitation process; in this case, regenerate
;; token and send back to the user a new invitation token (and
;; mark current session as logged). This happens only if the
;; invitation email matches with the register email.
(and (some? invitation)
(= (:email profile)
(:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens/generate (::setup/props cfg) claims)
resp {:invitation-token token}]
(-> resp
token (tokens/generate (::setup/props cfg) claims)]
(-> {:invitation-token token}
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
(rph/with-meta {::audit/replace-props props
::audit/context {:action "accept-invitation"}
::audit/profile-id (:id profile)})))
;; If auth backend is different from "penpot" means user is
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
;; When a new user is created and it is already activated by
;; configuration or specified by OIDC, we just mark the profile
;; as logged-in
created?
(if (:is-active profile)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta
{::audit/replace-props props
::audit/context {:action "login"}
::audit/profile-id (:id profile)}))
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
(do
(when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)}
{::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)})))
;; In all other cases, send a verification email.
:else
(do
(send-email-verification! conn (::setup/props cfg) profile)
(rph/with-meta profile
(let [elapsed? (elapsed-verify-threshold? profile)
reports? (eml/has-reports? conn (:email profile))
action (if reports?
"ignore-because-complaints"
(if elapsed?
"resend-email-verification"
"ignore"))]
(l/wrn :hint "repeated registry detected"
:profile-id (str (:id profile))
:profile-email (:email profile)
:context-action action)
(when (= action "resend-email-verification")
(db/update! conn :profile
{:modified-at (dt/now)}
{:id (:id profile)})
(send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)}
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
::audit/context {:action action}
::audit/profile-id (:id profile)
::audit/name "register-profile-retry"})))))
(def schema:register-profile
[:map {:title "register-profile"}
[:token schema:token]
[:fullname [::sm/word-string {:max 100}]]])
[:fullname [::sm/word-string {:max 100}]]
[:theme {:optional true} [:string {:max 10}]]])
(sv/defmethod ::register-profile
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:register-profile
::climit/id :auth/global}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg ::db/conn conn)
(register-profile params))))
[cfg params]
(db/tx-run! cfg register-profile params))
;; ---- COMMAND: Request Profile Recovery
(defn request-profile-recovery
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
(defn- request-profile-recovery
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate (::setup/props cfg)
{:iss :password-recovery
@@ -450,27 +497,42 @@
:extra-data ptoken})
nil))]
(db/with-atomic [conn pool]
(when-let [profile (->> (profile/clean-email email)
(profile/get-profile-by-email conn))]
(when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(let [profile (->> (profile/clean-email email)
(profile/get-profile-by-email conn))]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :profile-not-verified
:hint "the user need to validate profile before recover password"))
(cond
(not profile)
(l/wrn :hint "attempt of profile recovery: no profile found"
:profile-email email)
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(not (eml/allow-send-emails? conn profile))
(l/wrn :hint "attempt of profile recovery: profile is muted"
:profile-id (str (:id profile))
:profile-email (:email profile))
(->> profile
(create-recovery-token)
(send-email-notification conn))))))
(eml/has-bounce-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: email has bounces"
:profile-id (str (:id profile))
:profile-email (:email profile))
(eml/has-complaint-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: email has complaints"
:profile-id (str (:id profile))
:profile-email (:email profile))
(not (elapsed-verify-threshold? profile))
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
:profile-id (str (:id profile))
:profile-email (:email profile))
:else
(do
(db/update! conn :profile
{:modified-at (dt/now)}
{:id (:id profile)})
(->> profile
(create-recovery-token)
(send-email-notification conn)))))))
(def schema:request-profile-recovery
@@ -482,6 +544,6 @@
::doc/added "1.15"
::sm/params schema:request-profile-recovery}
[cfg params]
(request-profile-recovery cfg params))
(db/tx-run! cfg request-profile-recovery params))

View File

@@ -30,14 +30,12 @@
;; --- Command: export-binfile
(def ^:private
schema:export-binfile
(sm/define
[:map {:title "export-binfile"}
[:name :string]
[:file-id ::sm/uuid]
[:include-libraries :boolean]
[:embed-assets :boolean]]))
(def ^:private schema:export-binfile
[:map {:title "export-binfile"}
[:name :string]
[:file-id ::sm/uuid]
[:include-libraries :boolean]
[:embed-assets :boolean]])
(sv/defmethod ::export-binfile
"Export a penpot file in a binary format."
@@ -76,13 +74,11 @@
{:id project-id})
result))
(def ^:private
schema:import-binfile
(sm/define
[:map {:title "import-binfile"}
[:name :string]
[:project-id ::sm/uuid]
[:file ::media/upload]]))
(def ^:private schema:import-binfile
[:map {:title "import-binfile"}
[:name :string]
[:project-id ::sm/uuid]
[:file ::media/upload]])
(sv/defmethod ::import-binfile
"Import a penpot file in a binary format."

View File

@@ -35,6 +35,7 @@
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
@@ -340,9 +341,9 @@
[:share-id {:optional true} ::sm/uuid]])
(defn- get-file-fragment
[conn file-id fragment-id]
(some-> (db/get conn :file-data-fragment {:file-id file-id :id fragment-id})
(update :content blob/decode)))
[cfg file-id fragment-id]
(some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id})
(update :data blob/decode)))
(sv/defmethod ::get-file-fragment
"Retrieve a file fragment by its ID. Only authenticated users."
@@ -350,12 +351,12 @@
::rpc/auth false
::sm/params schema:get-file-fragment
::sm/result schema:file-fragment}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id file-id share-id)]
(check-read-permissions! perms)
(-> (get-file-fragment conn file-id fragment-id)
(rph/with-http-cache long-cache-duration)))))
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(db/run! cfg (fn [cfg]
(let [perms (get-permissions cfg profile-id file-id share-id)]
(check-read-permissions! perms)
(-> (get-file-fragment cfg file-id fragment-id)
(rph/with-http-cache long-cache-duration))))))
;; --- COMMAND QUERY: get-project-files
@@ -670,7 +671,7 @@
f.modified_at,
f.name,
f.is_shared,
ft.media_id,
ft.media_id AS thumbnail_id,
row_number() over w as row_num
from file as f
inner join project as p on (p.id = f.project_id)
@@ -689,10 +690,8 @@
[conn team-id]
(->> (db/exec! conn [sql:team-recent-files team-id])
(mapv (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(if-let [media-id (:thumbnail-id row)]
(assoc row :thumbnail-uri (resolve-public-uri media-id))
(dissoc row :media-id))))))
(def ^:private schema:get-team-recent-files
@@ -822,7 +821,7 @@
(feat.fdata/persist-pointers! cfg file-id))))
(defn- absorb-library!
(defn- absorb-library
"Find all files using a shared library, and absorb all library assets
into the file local libraries"
[cfg {:keys [id] :as library}]
@@ -840,7 +839,26 @@
:library-id (str id)
:files (str/join "," (map str ids)))
(run! (partial absorb-library-by-file! cfg ldata) ids)))
(run! (partial absorb-library-by-file! cfg ldata) ids)
library))
(defn absorb-library!
[{:keys [::db/conn] :as cfg} id]
(let [file (-> (get-file cfg id
:lock-for-update? true
:include-deleted? true)
(check-version!))
proj (db/get* conn :project {:id (:project-id file)}
{::db/remove-deleted false})
team (-> (db/get* conn :team {:id (:team-id proj)}
{::db/remove-deleted false})
(teams/decode-row))]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file)))
(absorb-library cfg file)))
(defn- set-file-shared
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
@@ -853,25 +871,14 @@
;; file, we need to perform more complex operation,
;; so in this case we retrieve the complete file and
;; perform all required validations.
(let [file (-> (get-file cfg id :lock-for-update? true)
(check-version!)
(assoc :is-shared false))
team (teams/get-team conn
:profile-id profile-id
:project-id (:project-id file))]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file)))
(absorb-library! cfg file)
(let [file (-> (absorb-library! cfg id)
(assoc :is-shared false))]
(db/delete! conn :file-library-rel {:library-file-id id})
(db/update! conn :file
{:is-shared false
:modified-at (dt/now)}
{:id id})
file)
(select-keys file [:id :name :is-shared]))
(and (false? (:is-shared file))
(true? (:is-shared params)))
@@ -911,12 +918,19 @@
;; --- MUTATION COMMAND: delete-file
(defn- mark-file-deleted!
(defn- mark-file-deleted
[conn file-id]
(db/update! conn :file
{:deleted-at (dt/now)}
{:id file-id}
{::db/return-keys [:id :name :is-shared :project-id :created-at :modified-at]}))
(let [file (db/update! conn :file
{:deleted-at (dt/now)}
{:id file-id}
{::db/return-keys [:id :name :is-shared :deleted-at
:project-id :created-at :modified-at]})]
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at (:deleted-at file)
:id file-id}})
file))
(def ^:private
schema:delete-file
@@ -927,29 +941,7 @@
(defn- delete-file
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
(check-edition-permissions! conn profile-id id)
(let [file (mark-file-deleted! conn id)]
;; NOTE: when a file is a shared library, then we proceed to load
;; the whole file, proceed with feature checking and properly execute
;; the absorb-library procedure
(when (:is-shared file)
(let [file (-> (get-file cfg id
:lock-for-update? true
:include-deleted? true)
(check-version!))
team (teams/get-team conn
:profile-id profile-id
:project-id (:project-id file))]
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file)))
(absorb-library! cfg file)))
(let [file (mark-file-deleted conn id)]
(rph/with-meta (rph/wrap)
{::audit/props {:project-id (:project-id file)
:name (:name file)

View File

@@ -6,13 +6,10 @@
(ns app.rpc.commands.files-create
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.defaults :refer [version]]
[app.common.schema :as sm]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.features.fdata :as feat.fdata]
@@ -40,7 +37,7 @@
(defn create-file
[{:keys [::db/conn] :as cfg}
{:keys [id name project-id is-shared revn
modified-at deleted-at create-page
modified-at deleted-at create-page page-id
ignore-sync-until features]
:or {is-shared false revn 0 create-page true}
:as params}]
@@ -51,23 +48,17 @@
(binding [pmap/*tracked* (pmap/create-tracked)
cfeat/*current* features]
(let [id (or id (uuid/next))
data (if create-page
(ctf/make-file-data id)
(ctf/make-file-data id nil))
file {:id id
:project-id project-id
:name name
:revn revn
:is-shared is-shared
:version version
:data data
:features features
:ignore-sync-until ignore-sync-until
:modified-at modified-at
:deleted-at deleted-at}
(let [file (ctf/make-file {:id id
:project-id project-id
:name name
:revn revn
:is-shared is-shared
:features features
:ignore-sync-until ignore-sync-until
:modified-at modified-at
:deleted-at deleted-at
:create-page create-page
:page-id page-id})
file (if (contains? features "fdata/objects-map")
(feat.fdata/enable-objects-map file)
@@ -75,9 +66,7 @@
file (if (contains? features "fdata/pointer-map")
(feat.fdata/enable-pointer-map file)
file)
file (d/without-nils file)]
file)]
(db/insert! conn :file
(-> file
@@ -86,9 +75,9 @@
{::db/return-keys false})
(when (contains? features "fdata/pointer-map")
(feat.fdata/persist-pointers! cfg id))
(feat.fdata/persist-pointers! cfg (:id file)))
(->> (assoc params :file-id id :role :owner)
(->> (assoc params :file-id (:id file) :role :owner)
(create-file-role! conn))
(db/update! conn :project

View File

@@ -87,10 +87,7 @@
::sm/params [:map {:title "get-file-object-thumbnails"}
[:file-id ::sm/uuid]
[:tag {:optional true} :string]]
::sm/result [:map-of :string :string]
::cond/get-object #(files/get-minimal-file %1 (:file-id %2))
::cond/reuse-key? true
::cond/key-fn files/get-file-etag}
::sm/result [:map-of :string :string]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id tag] :as params}]
(dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
@@ -271,7 +268,7 @@
(when (and (some? th1)
(not= (:media-id th1)
(:media-id th2)))
(sto/touch-object! storage (:media-id th1) :async true))
(sto/touch-object! storage (:media-id th1)))
th2))
@@ -324,18 +321,14 @@
(sv/defmethod ::delete-file-object-thumbnail
{::doc/added "1.19"
::doc/module :files
::doc/deprecated "1.20"
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
[:file-thumbnail-ops/global]]
::audit/skip true}
[cfg {:keys [::rpc/profile-id file-id object-id]}]
(files/check-edition-permissions! cfg profile-id file-id)
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage conn)
(delete-file-object-thumbnail! file-id object-id))
nil))))
(-> cfg
(update ::sto/storage media/configure-assets-storage conn)
(delete-file-object-thumbnail! file-id object-id))
nil)))
;; --- MUTATION COMMAND: create-file-thumbnail
@@ -413,4 +406,5 @@
(when-not (db/read-only? conn)
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)
media (create-file-thumbnail! cfg params)]
{:uri (files/resolve-public-uri (:id media))})))))
{:uri (files/resolve-public-uri (:id media))
:id (:id media)})))))

View File

@@ -262,8 +262,8 @@
;; Send asynchronous notifications
(send-notifications! cfg params)
;; Retrieve and return lagged data
(get-lagged-changes conn params))))
{:revn (:revn file)
:lagged (get-lagged-changes conn params)})))
(defn- soft-validate-file-schema!
[file]
@@ -324,19 +324,21 @@
(update :data cpc/process-changes changes)
(update :data d/without-nils))]
(when (contains? cf/flags :soft-file-validation)
(soft-validate-file! file libs))
(when (contains? cf/flags :soft-file-schema-validation)
(soft-validate-file-schema! file))
(binding [pmap/*tracked* nil]
(when (contains? cf/flags :soft-file-validation)
(soft-validate-file! file libs))
(when (and (contains? cf/flags :file-validation)
(not skip-validate))
(val/validate-file! file libs))
(when (contains? cf/flags :soft-file-schema-validation)
(soft-validate-file-schema! file))
(when (and (contains? cf/flags :file-schema-validation)
(not skip-validate))
(val/validate-file-schema! file))
(when (and (contains? cf/flags :file-validation)
(not skip-validate))
(val/validate-file! file libs))
(when (and (contains? cf/flags :file-schema-validation)
(not skip-validate))
(val/validate-file-schema! file)))
(cond-> file
(contains? cfeat/*current* "fdata/objects-map")

View File

@@ -12,7 +12,6 @@
[app.db :as db]
[app.http.session :as session]
[app.loggers.audit :as-alias audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
@@ -73,7 +72,7 @@
(rph/with-meta {::audit/props (:props profile)
::audit/profile-id (:id profile)})))
(-> profile
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/props (:props profile)
::audit/profile-id (:id profile)}))))))

View File

@@ -16,6 +16,7 @@
[app.config :as cf]
[app.db :as db]
[app.http.sse :as sse]
[app.loggers.audit :as audit]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
@@ -397,17 +398,30 @@
;; --- COMMAND: Clone Template
(defn- clone-template
[{:keys [::wrk/executor ::bf.v1/project-id] :as cfg} template]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
;; NOTE: the importation process performs some operations that
;; are not very friendly with virtual threads, and for avoid
;; unexpected blocking of other concurrent operations we
;; dispatch that operation to a dedicated executor.
(let [result (px/submit! executor (partial bf.v1/import-files! cfg template))]
(let [cfg (-> cfg
(assoc ::bf.v1/project-id project-id)
(assoc ::bf.v1/profile-id profile-id))
result (px/invoke! executor (partial bf.v1/import-files! cfg template))]
(db/update! conn :project
{:modified-at (dt/now)}
{:id project-id})
(deref result)))))
(let [props (audit/clean-props params)]
(doseq [file-id result]
(let [props (assoc props :id file-id)
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-file")
(assoc ::audit/props props))]
(audit/submit! cfg event))))
result))))
(def ^:private
schema:clone-template
@@ -425,16 +439,14 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
template (tmpl/get-template-stream cfg template-id)
params (-> cfg
(assoc ::bf.v1/project-id (:id project))
(assoc ::bf.v1/profile-id profile-id))]
template (tmpl/get-template-stream cfg template-id)]
(when-not template
(ex/raise :type :not-found
:code :template-not-found
:hint "template not found"))
(sse/response #(clone-template params template))))
(sse/response #(clone-template cfg params template))))
;; --- COMMAND: Get list of builtin templates

View File

@@ -28,7 +28,7 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[app.worker :as wrk]
[cuerdas.core :as str]
[promesa.exec :as px]))
@@ -91,8 +91,8 @@
(defn get-profile
"Get profile by id. Throws not-found exception if no profile found."
[conn id & {:as attrs}]
(-> (db/get-by-id conn :profile id attrs)
[conn id & {:as opts}]
(-> (db/get-by-id conn :profile id opts)
(decode-row)))
;; --- MUTATION: Update Profile (own)
@@ -102,7 +102,7 @@
(sm/define
[:map {:title "update-profile"}
[:fullname [::sm/word-string {:max 250}]]
[:lang {:optional true} [:string {:max 5}]]
[:lang {:optional true} [:string {:max 8}]]
[:theme {:optional true} [:string {:max 250}]]]))
(sv/defmethod ::update-profile
@@ -110,7 +110,6 @@
::sm/params schema:update-profile
::sm/result schema:profile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
(db/with-atomic [conn pool]
;; NOTE: we need to retrieve the profile independently if we use
;; it or not for explicit locking and avoid concurrent updates of
@@ -277,19 +276,19 @@
(sv/defmethod ::request-email-change
{::doc/added "1.0"
::sm/params schema:request-email-change}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}]
(db/with-atomic [conn pool]
(let [profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg ::conn conn)
params (assoc params
:profile profile
:email (clean-email email))]
(if (contains? cf/flags :smtp)
(request-email-change! cfg params)
(change-email-immediately! cfg params)))))
[cfg {:keys [::rpc/profile-id email] :as params}]
(db/tx-run! cfg
(fn [cfg]
(let [profile (db/get-by-id cfg :profile profile-id)
params (assoc params
:profile profile
:email (clean-email email))]
(if (contains? cf/flags :smtp)
(request-email-change! cfg params)
(change-email-immediately! cfg params))))))
(defn- change-email-immediately!
[{:keys [::conn]} {:keys [profile email] :as params}]
[{:keys [::db/conn]} {:keys [profile email] :as params}]
(when (not= email (:email profile))
(check-profile-existence! conn params))
@@ -300,7 +299,7 @@
{:changed true})
(defn- request-email-change!
[{:keys [::conn] :as cfg} {:keys [profile email] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}]
(let [token (tokens/generate (::setup/props cfg)
{:iss :change-email
:exp (dt/in-future "15m")
@@ -320,9 +319,28 @@
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
:email email
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? conn email)
(ex/raise :type :restriction
:code :email-has-complaints
:email email
:hint "looks like the email has spam complaint reports"))
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (:email profile)
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? conn (:email profile))
(ex/raise :type :restriction
:code :email-has-complaints
:email (:email profile)
:hint "looks like the email has spam complaint reports"))
(eml/send! {::eml/conn conn
::eml/factory eml/change-email
@@ -367,13 +385,13 @@
;; --- MUTATION: Delete Profile
(declare ^:private get-owned-teams-with-participants)
(declare ^:private get-owned-teams)
(sv/defmethod ::delete-profile
{::doc/added "1.0"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(let [teams (get-owned-teams-with-participants conn profile-id)
(let [teams (get-owned-teams conn profile-id)
deleted-at (dt/now)]
;; If we found owned teams with participants, we don't allow
@@ -385,37 +403,39 @@
:hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :id teams)}))
(doseq [{:keys [id]} teams]
(db/update! conn :team
{:deleted-at deleted-at}
{:id id}))
;; Mark profile deleted immediatelly
(db/update! conn :profile
{:deleted-at deleted-at}
{:id profile-id})
;; Schedule cascade deletion to a worker
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :profile
:deleted-at deleted-at
:id profile-id}})
(rph/with-transform {} (session/delete-fn cfg)))))
;; --- HELPERS
(def sql:owned-teams
"with owner_teams as (
select tpr.team_id as id
from team_profile_rel as tpr
where tpr.is_owner is true
and tpr.profile_id = ?
"WITH owner_teams AS (
SELECT tpr.team_id AS id
FROM team_profile_rel AS tpr
WHERE tpr.is_owner IS TRUE
AND tpr.profile_id = ?
)
select tpr.team_id as id,
count(tpr.profile_id) - 1 as participants
from team_profile_rel as tpr
where tpr.team_id in (select id from owner_teams)
and tpr.profile_id != ?
group by 1")
SELECT tpr.team_id AS id,
count(tpr.profile_id) - 1 AS participants
FROM team_profile_rel AS tpr
WHERE tpr.team_id IN (SELECT id from owner_teams)
GROUP BY 1")
(defn- get-owned-teams-with-participants
(defn get-owned-teams
[conn profile-id]
(db/exec! conn [sql:owned-teams profile-id profile-id]))
(db/exec! conn [sql:owned-teams profile-id]))
(def ^:private sql:profile-existence
"select exists (select * from profile

View File

@@ -7,6 +7,7 @@
(ns app.rpc.commands.projects
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.db.sql :as-alias sql]
@@ -20,6 +21,7 @@
[app.rpc.quotes :as quotes]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]))
(s/def ::id ::us/uuid)
@@ -244,28 +246,39 @@
;; --- MUTATION: Delete Project
(defn- delete-project
[conn project-id]
(let [project (db/update! conn :project
{:deleted-at (dt/now)}
{:id project-id}
{::db/return-keys true})]
(when (:is-default project)
(ex/raise :type :validation
:code :non-deletable-project
:hint "impossible to delete default project"))
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :project
:deleted-at (:deleted-at project)
:id project-id}})
project))
(s/def ::delete-project
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
;; TODO: right now, we just don't allow delete default projects, in a
;; future we need to ensure raise a correct exception signaling that
;; this is not allowed.
(sv/defmethod ::delete-project
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id)
(let [project (db/update! conn :project
{:deleted-at (dt/now)}
{:id id :is-default false}
{::db/return-keys true})]
(let [project (delete-project conn id)]
(rph/with-meta (rph/wrap)
{::audit/props {:team-id (:team-id project)
:name (:name project)
:created-at (:created-at project)
:modified-at (:modified-at project)}}))))

View File

@@ -12,7 +12,6 @@
[app.common.features :as cfeat]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -31,16 +30,11 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[app.worker :as wrk]
[cuerdas.core :as str]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(def ^:private sql:team-permissions
"select tpr.is_owner,
tpr.is_admin,
@@ -350,7 +344,7 @@
(def ^:private schema:create-team
[:map {:title "create-team"}
[:name :string]
[:name [:string {:max 250}]]
[:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid]])
@@ -363,10 +357,12 @@
::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))]
(create-team cfg (assoc params
:profile-id profile-id
:features features))))))
(cfeat/check-client-features! (:features params)))
team (create-team cfg (assoc params
:profile-id profile-id
:features features))]
(with-meta team
{::audit/props {:id (:id team)}})))))
(defn create-team
"This is a complete team creation process, it creates the team
@@ -437,12 +433,14 @@
;; --- Mutation: Update Team
(s/def ::update-team
(s/keys :req [::rpc/profile-id]
:req-un [::name ::id]))
(def ^:private schema:update-team
[:map {:title "update-team"}
[:name [:string {:max 250}]]
[:id ::sm/uuid]])
(sv/defmethod ::update-team
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:update-team}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id)
@@ -502,30 +500,49 @@
nil))
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]
:opt-un [::reassign-to]))
(def ^:private schema:leave-team
[:map {:title "leave-team"}
[:id ::sm/uuid]
[:reassign-to {:optional true} ::sm/uuid]])
(sv/defmethod ::leave-team
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:leave-team}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(leave-team conn (assoc params :profile-id profile-id))))
;; --- Mutation: Delete Team
(s/def ::delete-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
(defn- delete-team
"Mark a team for deletion"
[conn team-id]
;; TODO: right now just don't allow delete default team, in future it
;; should raise a specific exception for signal that this action is
;; not allowed.
(let [deleted-at (dt/now)
team (db/update! conn :team
{:deleted-at deleted-at}
{:id team-id}
{::db/return-keys true})]
(when (:is-default team)
(ex/raise :type :validation
:code :non-deletable-team
:hint "impossible to delete default team"))
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :team
:deleted-at deleted-at
:id team-id}})
team))
(def ^:private schema:delete-team
[:map {:title "delete-team"}
[:id ::sm/uuid]])
(sv/defmethod ::delete-team
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:delete-team}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id id)]
@@ -533,18 +550,11 @@
(ex/raise :type :validation
:code :only-owner-can-delete-team))
(db/update! conn :team
{:deleted-at (dt/now)}
{:id id :is-default false})
(delete-team conn id)
nil)))
;; --- Mutation: Team Update Role
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
(s/def ::role #{:owner :admin :editor})
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/penpot/issue/1083
(def valid-roles
@@ -608,25 +618,29 @@
:profile-id member-id})
nil)))
(s/def ::update-team-member-role
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::member-id ::role]))
(def ^:private schema:update-team-member-role
[:map {:title "update-team-member-role"}
[:team-id ::sm/uuid]
[:member-id ::sm/uuid]
[:role schema:role]])
(sv/defmethod ::update-team-member-role
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:update-team-member-role}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(update-team-member-role conn (assoc params :profile-id profile-id))))
;; --- Mutation: Delete Team Member
(s/def ::delete-team-member
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::member-id]))
(def ^:private schema:delete-team-member
[:map {:title "delete-team-member"}
[:team-id ::sm/uuid]
[:member-id ::sm/uuid]])
(sv/defmethod ::delete-team-member
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:delete-team-member}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)]
@@ -649,13 +663,14 @@
(declare upload-photo)
(declare ^:private update-team-photo)
(s/def ::file ::media/upload)
(s/def ::update-team-photo
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::file]))
(def ^:private schema:update-team-photo
[:map {:title "update-team-photo"}
[:team-id ::sm/uuid]
[:file ::media/upload]])
(sv/defmethod ::update-team-photo
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:update-team-photo}
[cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
@@ -719,12 +734,19 @@
:email email
:hint "the profile has reported repeatedly as spam or has bounces"))
;; Secondly check if the invited member email is part of the global spam/bounce report.
;; Secondly check if the invited member email is part of the global bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email email
:hint "the email you invite has been repeatedly reported as spam or bounce"))
:hint "the email you invite has been repeatedly reported as bounce"))
;; Secondly check if the invited member email is part of the global complain report.
(when (eml/has-complaint-reports? conn email)
(ex/raise :type :restriction
:code :email-has-complaints
:email email
:hint "the email you invite has been repeatedly reported as spam"))
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
@@ -750,6 +772,7 @@
{:id (:id member)}))
nil)
(let [id (uuid/next)
expire (dt/in-future "168h") ;; 7 days
invitation (db/exec-one! conn [sql:upsert-team-invitation id
@@ -770,14 +793,16 @@
(when (contains? cf/flags :log-invitation-tokens)
(l/info :hint "invitation token" :token itoken))
(audit/submit! cfg
{::audit/type "action"
::audit/name (if updated?
"update-team-invitation"
"create-team-invitation")
::audit/profile-id (:id profile)
::audit/props (-> (dissoc tprops :profile-id)
(d/without-nils))})
(let [props (-> (dissoc tprops :profile-id)
(audit/clean-props))
evname (if updated?
"update-team-invitation"
"create-team-invitation")
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name evname)
(assoc ::audit/props props))]
(audit/submit! cfg event))
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
@@ -793,7 +818,7 @@
(def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role [::sm/one-of #{:owner :admin :editor}]]
[:role schema:role]
[:emails ::sm/set-of-emails]])
(sv/defmethod ::create-team-invitations
@@ -837,25 +862,19 @@
;; We don't re-send inviation to already existing members
(remove (partial contains? members))
(map (fn [email]
{:email email
:team team
:profile profile
:role role}))
(-> params
(assoc :email email)
(assoc :team team)
(assoc :profile profile)
(assoc :role role))))
(keep (partial create-invitation cfg)))
emails)]
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}})))))
;; --- Mutation: Create Team & Invite Members
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-with-invitations
(s/merge ::create-team
(s/keys :req-un [::emails ::role])))
(def ^:private schema:create-team-with-invitations
[:map {:title "create-team-with-invitations"}
[:name :string]
@@ -867,59 +886,62 @@
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"
::sm/params schema:create-team-with-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
(db/with-atomic [conn pool]
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (assoc params
:profile-id profile-id
:features features)
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
{:team team
:profile profile
:email email
:role role}))
(run! (partial create-invitation cfg)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
(audit/submit! cfg
{::audit/type "command"
::audit/name "create-team-invitations"
::audit/profile-id profile-id
::audit/props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)}})
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg)))
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
;; --- Query: get-team-invitation-token
(s/def ::get-team-invitation-token
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::email]))
(def ^:private schema:get-team-invitation-token
[:map {:title "get-team-invitation-token"}
[:team-id ::sm/uuid]
[:email ::sm/email]])
(sv/defmethod ::get-team-invitation-token
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:get-team-invitation-token}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
(check-read-permissions! pool profile-id team-id)
(let [email (profile/clean-email email)
@@ -940,12 +962,15 @@
;; --- Mutation: Update invitation role
(s/def ::update-team-invitation-role
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::email ::role]))
(def ^:private schema:update-team-invitation-role
[:map {:title "update-team-invitation-role"}
[:team-id ::sm/uuid]
[:email ::sm/email]
[:role schema:role]])
(sv/defmethod ::update-team-invitation-role
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:update-team-invitation-role}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)]
@@ -961,12 +986,14 @@
;; --- Mutation: Delete invitation
(s/def ::delete-team-invitation
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::email]))
(def ^:private schema:delete-team-invition
[:map {:title "delete-team-invitation"}
[:team-id ::sm/uuid]
[:email ::sm/email]])
(sv/defmethod ::delete-team-invitation
{::doc/added "1.17"}
{::doc/added "1.17"
::sm/params schema:delete-team-invition}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)]

View File

@@ -8,7 +8,9 @@
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
@@ -82,8 +84,16 @@
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)]
(assoc claims :profile profile)))
(let [profile (profile/get-profile conn profile-id {::sql/for-update true})
props (merge (:props profile)
(:props claims))]
(when (not= props (:props profile))
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id}))
(let [profile (assoc profile :props props)]
(assoc claims :profile profile))))
;; --- Team Invitation
@@ -138,16 +148,17 @@
(defmethod process-token :team-invitation
[{:keys [conn] :as cfg}
{:keys [::rpc/profile-id token]}
{:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}]
(us/verify! ::team-invitation-claims claims)
(let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email})
profile (db/get* conn :profile
{:id profile-id}
{:columns [:id :email]})]
(let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email})
profile (db/get* conn :profile
{:id profile-id}
{:columns [:id :email]})
registration-disabled? (not (contains? cf/flags :registration))]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token
@@ -160,25 +171,28 @@
;; if we have logged-in user and it matches the invitation we proceed
;; with accepting the invitation and joining the current profile to the
;; invited team.
(let [profile (accept-invitation cfg claims invitation profile)]
(-> (assoc claims :state :created)
(rph/with-meta {::audit/name "accept-team-invitation"
::audit/profile-id (:id profile)
::audit/props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}})))
(let [props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "accept-team-invitation")
(assoc ::audit/props props))]
(accept-invitation cfg claims invitation profile)
(audit/submit! cfg event)
(assoc claims :state :created))
(ex/raise :type :validation
:code :invalid-token
:hint "logged-in user does not matches the invitation"))
;; If we have not logged-in user, and invitation comes with member-id we
;; redirect user to login, if no memeber-id is present in the invitation
;; token, we redirect user the the register page.
;; redirect user to login, if no memeber-id is present and in the invitation
;; token and registration is enabled, we redirect user the the register page.
{:invitation-token token
:iss :team-invitation
:redirect-to (if member-id :auth-login :auth-register)
:redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register)
:state :pending})))
;; --- Default

View File

@@ -83,17 +83,17 @@
"- Quote ID: '~(::target params)'\n"
"- Max: ~(::quote params)\n"
"- Total: ~(::total params) (INCR ~(::incr params 1))\n")]
(wrk/submit! {::wrk/task :sendmail
(wrk/submit! {::db/conn conn
::wrk/task :sendmail
::wrk/delay (dt/duration "30s")
::wrk/max-retries 4
::wrk/priority 200
::wrk/conn conn
::wrk/dedupe true
::wrk/label "quotes-notification"
:to (vec admins)
:subject subject
:body [{:type "text/plain"
:content content}]}))))
::wrk/params {:to (vec admins)
:subject subject
:body [{:type "text/plain"
:content content}]}}))))
(defn- generic-check!
[{:keys [::db/conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}]

View File

@@ -51,12 +51,12 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.http :as-alias http]
[app.loggers.audit :refer [parse-client-ip]]
[app.redis :as rds]
[app.redis.script :as-alias rscript]
[app.rpc :as-alias rpc]
[app.rpc.helpers :as rph]
[app.rpc.rlimit.result :as-alias lresult]
[app.util.inet :as inet]
[app.util.services :as-alias sv]
[app.util.time :as dt]
[app.worker :as wrk]
@@ -215,7 +215,7 @@
[{:keys [::rpc/profile-id] :as params}]
(let [request (-> params meta ::http/request)]
(or profile-id
(some-> request parse-client-ip)
(some-> request inet/parse-request)
uuid/zero)))
(defn process-request!

View File

@@ -184,10 +184,7 @@
(ctk/instance-head? child))
(let [slot (guess-swap-slot component-child component-container)]
(l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot)
(ctn/update-shape container (:id child)
#(update % :touched
cfh/set-touched-group
(ctk/build-swap-slot-group slot))))
(ctn/update-shape container (:id child) #(ctk/set-swap-slot % slot)))
container)]
(recur (process-copy-head container child)
(rest children)
@@ -237,3 +234,44 @@
file (-> file
(update :data process-fdata))))
(defn fix-find-duplicated-slots
[file _]
;; Find the shapes whose children have duplicated slots
(let [check-duplicate-swap-slot
(fn [shape page]
(let [shapes (map #(get (:objects page) %) (:shapes shape))
slots (->> (map #(ctk/get-swap-slot %) shapes)
(remove nil?))
counts (frequencies slots)]
#_(when (some (fn [[_ count]] (> count 1)) counts)
(l/trc :info "This shape has children with the same swap slot" :id (:id shape) :file-id (str (:id file))))
(some (fn [[_ count]] (> count 1)) counts)))
count-slots-shape
(fn [page shape]
(if (ctk/instance-root? shape)
(check-duplicate-swap-slot shape page)
false))
count-slots-page
(fn [page]
(->> (:objects page)
(vals)
(mapv #(count-slots-shape page %))
(filter true?)
count))
count-slots-data
(fn [data]
(->> (:pages-index data)
(vals)
(mapv count-slots-page)
(reduce +)))
num-missing-slots (count-slots-data (:data file))]
(when (pos? num-missing-slots)
(l/trc :info (str "Shapes with children with the same swap slot: " num-missing-slots) :file-id (str (:id file))))
file))

View File

@@ -21,8 +21,10 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.components-v2 :as feat.comp-v2]
[app.features.fdata :as feat.fdata]
[app.loggers.audit :as audit]
[app.main :as main]
[app.msgbus :as mbus]
[app.rpc.commands.auth :as auth]
@@ -30,16 +32,20 @@
[app.rpc.commands.files-snapshot :as fsnap]
[app.rpc.commands.management :as mgmt]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.srepl.fixes :as fixes]
[app.srepl.helpers :as h]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.java.io :as io]
[clojure.pprint :refer [print-table]]
[clojure.stacktrace :as strace]
[clojure.tools.namespace.repl :as repl]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[promesa.exec :as px]
[promesa.exec.semaphore :as ps]
[promesa.util :as pu]))
@@ -57,42 +63,35 @@
([tname]
(run-task! tname {}))
([tname params]
(let [tasks (:app.worker/registry main/system)
tname (if (keyword? tname) (name tname) name)]
(if-let [task-fn (get tasks tname)]
(task-fn params)
(println (format "no task '%s' found" tname))))))
(wrk/invoke! (-> main/system
(assoc ::wrk/task tname)
(assoc ::wrk/params params)))))
(defn schedule-task!
([name]
(schedule-task! name {}))
([name props]
(let [pool (:app.db/pool main/system)]
(wrk/submit!
::wrk/conn pool
::wrk/task name
::wrk/props props))))
([name params]
(wrk/submit! (-> main/system
(assoc ::wrk/task name)
(assoc ::wrk/params params)))))
(defn send-test-email!
[destination]
(us/verify!
:expr (string? destination)
:hint "destination should be provided")
(let [handler (:app.email/sendmail main/system)]
(handler {:body "test email"
:subject "test email"
:to [destination]})))
(assert (string? destination) "destination should be provided")
(-> main/system
(assoc ::wrk/task :sendmail)
(assoc ::wrk/params {:body "test email"
:subject "test email"
:to [destination]})
(wrk/invoke!)))
(defn resend-email-verification-email!
[email]
(let [sprops (:app.setup/props main/system)
pool (:app.db/pool main/system)
email (profile/clean-email email)
profile (profile/get-profile-by-email pool email)]
(auth/send-email-verification! pool sprops profile)
:email-sent))
(db/tx-run! main/system
(fn [{:keys [::db/conn] :as cfg}]
(let [email (profile/clean-email email)
profile (profile/get-profile-by-email conn email)]
(#'auth/send-email-verification! cfg profile)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROFILES MANAGEMENT
@@ -194,8 +193,13 @@
;; NOTIFICATIONS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn notify!
"Send flash notifications.
This method allows send flash notifications to specified target destinations.
The message can be a free text or a preconfigured one.
The destination can be: all, profile-id, team-id, or a coll of them."
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
:or {code :generic level :info}
:as params}]
@@ -203,10 +207,6 @@
["invalid level %" level]
(contains? #{:success :error :info :warning} level))
(dm/verify!
["invalid code: %" code]
(contains? #{:generic :upgrade-version} code))
(letfn [(send [dest]
(l/inf :hint "sending notification" :dest (str dest))
(let [message {:type :notification
@@ -232,6 +232,9 @@
(resolve-dest [dest]
(cond
(= :all dest)
[uuid/zero]
(uuid? dest)
[dest]
@@ -247,14 +250,15 @@
(mapcat resolve-dest))
dest)
(and (coll? dest)
(every? coll? dest))
(and (vector? dest)
(every? vector? dest))
(sequence (comp
(map vec)
(mapcat resolve-dest))
dest)
(vector? dest)
(and (vector? dest)
(keyword? (first dest)))
(let [[op param] dest]
(cond
(= op :email)
@@ -476,6 +480,332 @@
:rollback rollback?
:elapsed elapsed))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn delete-file!
"Mark a project for deletion"
[file-id]
(let [file-id (h/parse-uuid file-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-file"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props {:id file-id}
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-file!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :file
:deleted-at tnow
:id file-id})))
:deleted))
(defn- restore-file*
[{:keys [::db/conn]} file-id]
(db/update! conn :file
{:deleted-at nil
:has-media-trimmed false}
{:id file-id})
;; Fragments are not handled here because they
;; use the database cascade operation and they
;; are not marked for deletion with objects-gc
;; task
(db/update! conn :file-media-object
{:deleted-at nil}
{:file-id file-id})
;; Mark thumbnails to be deleted
(db/update! conn :file-thumbnail
{:deleted-at nil}
{:file-id file-id})
(db/update! conn :file-tagged-object-thumbnail
{:deleted-at nil}
{:file-id file-id})
:restored)
(defn restore-file!
"Mark a file and all related objects as not deleted"
[file-id]
(let [file-id (h/parse-uuid file-id)]
(db/tx-run! main/system
(fn [system]
(when-let [file (some-> (db/get* system :file
{:id file-id}
{::db/remove-deleted false
::sql/columns [:id :name]})
(files/decode-row))]
(audit/insert! system
{::audit/name "restore-file"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props file
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-file!"}
::audit/tracked-at (dt/now)})
(restore-file* system file-id))))))
(defn delete-project!
"Mark a project for deletion"
[project-id]
(let [project-id (h/parse-uuid project-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-project"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props {:id project-id}
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-project!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :project
:deleted-at tnow
:id project-id})))
:deleted))
(defn- restore-project*
[{:keys [::db/conn] :as cfg} project-id]
(db/update! conn :project
{:deleted-at nil}
{:id project-id})
(doseq [{:keys [id]} (db/query conn :file
{:project-id project-id}
{::sql/columns [:id]})]
(restore-file* cfg id))
:restored)
(defn restore-project!
"Mark a project and all related objects as not deleted"
[project-id]
(let [project-id (h/parse-uuid project-id)]
(db/tx-run! main/system
(fn [system]
(when-let [project (db/get* system :project
{:id project-id}
{::db/remove-deleted false})]
(audit/insert! system
{::audit/name "restore-project"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props project
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-team!"}
::audit/tracked-at (dt/now)})
(restore-project* system project-id))))))
(defn delete-team!
"Mark a team for deletion"
[team-id]
(let [team-id (h/parse-uuid team-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-team"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props {:id team-id}
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-profile!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :team
:deleted-at tnow
:id team-id})))
:deleted))
(defn- restore-team*
[{:keys [::db/conn] :as cfg} team-id]
(db/update! conn :team
{:deleted-at nil}
{:id team-id})
(db/update! conn :team-font-variant
{:deleted-at nil}
{:team-id team-id})
(doseq [{:keys [id]} (db/query conn :project
{:team-id team-id}
{::sql/columns [:id]})]
(restore-project* cfg id))
:restored)
(defn restore-team!
"Mark a team and all related objects as not deleted"
[team-id]
(let [team-id (h/parse-uuid team-id)]
(db/tx-run! main/system
(fn [system]
(when-let [team (some-> (db/get* system :team
{:id team-id}
{::db/remove-deleted false})
(teams/decode-row))]
(audit/insert! system
{::audit/name "restore-team"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props team
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-team!"}
::audit/tracked-at (dt/now)})
(restore-team* system team-id))))))
(defn delete-profile!
"Mark a profile for deletion."
[profile-id]
(let [profile-id (h/parse-uuid profile-id)
tnow (dt/now)]
(audit/insert! main/system
{::audit/name "delete-profile"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-profile!"}
::audit/tracked-at tnow})
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :profile
:deleted-at tnow
:id profile-id})))
:deleted))
(defn restore-profile!
"Mark a team and all related objects as not deleted"
[profile-id]
(let [profile-id (h/parse-uuid profile-id)]
(db/tx-run! main/system
(fn [system]
(when-let [profile (some-> (db/get* system :profile
{:id profile-id}
{::db/remove-deleted false})
(profile/decode-row))]
(audit/insert! system
{::audit/name "restore-profile"
::audit/type "action"
::audit/profile-id uuid/zero
::audit/props (audit/profile->props profile)
::audit/context {:triggered-by "srepl"
:cause "explicit call to restore-profile!"}
::audit/tracked-at (dt/now)})
(db/update! system :profile
{:deleted-at nil}
{:id profile-id}
{::db/return-keys false})
(doseq [{:keys [id]} (profile/get-owned-teams system profile-id)]
(restore-team* system id))
:restored)))))
(defn delete-profiles-in-bulk!
[system path]
(letfn [(process-data! [system deleted-at emails]
(loop [emails emails
deleted 0
total 0]
(if-let [email (first emails)]
(if-let [profile (db/get* system :profile
{:email (str/lower email)}
{::db/remove-deleted false})]
(do
(audit/insert! system
{::audit/name "delete-profile"
::audit/type "action"
::audit/tracked-at deleted-at
::audit/props (audit/profile->props profile)
::audit/context {:triggered-by "srepl"
:cause "explicit call to delete-profiles-in-bulk!"}})
(wrk/invoke! (-> system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :profile
:deleted-at deleted-at
:id (:id profile)})))
(recur (rest emails)
(inc deleted)
(inc total)))
(recur (rest emails)
deleted
(inc total)))
{:deleted deleted :total total})))]
(let [path (fs/path path)
deleted-at (dt/minus (dt/now) cf/deletion-delay)]
(when-not (fs/exists? path)
(throw (ex-info "path does not exists" {:path path})))
(db/tx-run! system
(fn [system]
(with-open [reader (io/reader path)]
(process-data! system deleted-at (line-seq reader))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CASCADE FIXING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn process-deleted-profiles-cascade
[]
(->> (db/exec! main/system ["select id, deleted_at from profile where deleted_at is not null"])
(run! (fn [{:keys [id deleted-at]}]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :profile
:deleted-at deleted-at
:id id})))))))
(defn process-deleted-teams-cascade
[]
(->> (db/exec! main/system ["select id, deleted_at from team where deleted_at is not null"])
(run! (fn [{:keys [id deleted-at]}]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :team
:deleted-at deleted-at
:id id})))))))
(defn process-deleted-projects-cascade
[]
(->> (db/exec! main/system ["select id, deleted_at from project where deleted_at is not null"])
(run! (fn [{:keys [id deleted-at]}]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :project
:deleted-at deleted-at
:id id})))))))
(defn process-deleted-files-cascade
[]
(->> (db/exec! main/system ["select id, deleted_at from file where deleted_at is not null"])
(run! (fn [{:keys [id deleted-at]}]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :file
:deleted-at deleted-at
:id id})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MISC
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -16,7 +16,6 @@
[app.storage.impl :as impl]
[app.storage.s3 :as ss3]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[datoteka.fs :as fs]
[integrant.core :as ig]
@@ -28,7 +27,7 @@
;; Storage Module State
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::id #{:assets-fs :assets-s3})
(s/def ::id #{:assets-fs :assets-s3 :s3 :fs})
(s/def ::s3 ::ss3/backend)
(s/def ::fs ::sfs/backend)
(s/def ::type #{:fs :s3})
@@ -171,28 +170,16 @@
(impl/put-object object content))
object)))
(def ^:private default-touch-delay
"A default delay for the asynchronous touch operation"
(dt/duration "5m"))
(defn touch-object!
"Mark object as touched."
[{:keys [::db/pool-or-conn] :as storage} object-or-id & {:keys [async]}]
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
(us/assert! ::storage storage)
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
(if async
(wrk/submit! ::wrk/conn pool-or-conn
::wrk/task :object-update
::wrk/delay default-touch-delay
:object :storage-object
:id id
:key :touched-at
:val (dt/now))
(-> (db/update! pool-or-conn :storage-object
{:touched-at (dt/now)}
{:id id})
(db/get-update-count)
(pos?)))))
(-> (db/update! pool-or-conn :storage-object
{:touched-at (dt/now)}
{:id id})
(db/get-update-count)
(pos?))))
(defn get-object-data
"Return an input stream instance of the object content."

View File

@@ -110,8 +110,8 @@
(defmethod ig/init-key ::handler
[_ {:keys [::min-age] :as cfg}]
(fn [params]
(let [min-age (dt/duration (or (:min-age params) min-age))]
(fn [{:keys [props] :as task}]
(let [min-age (dt/duration (or (:min-age props) min-age))]
(db/tx-run! cfg (fn [cfg]
(let [cfg (assoc cfg ::min-age min-age)
total (clean-deleted! cfg)]

View File

@@ -0,0 +1,122 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.tasks.delete-object
"A generic task for object deletion cascade handling"
(:require
[app.common.logging :as l]
[app.db :as db]
[app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(def ^:dynamic *team-deletion* false)
(defmulti delete-object
(fn [_ props] (:object props)))
(defmethod delete-object :file
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false})]
(l/trc :hint "marking for deletion" :rel "file" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :file
{:deleted-at deleted-at}
{:id id}
{::db/return-keys false})
(when (and (:is-shared file)
(not *team-deletion*))
;; NOTE: we don't prevent file deletion on absorb operation failure
(try
(db/tx-run! cfg files/absorb-library! id)
(catch Throwable cause
(l/warn :hint "error on absorbing library"
:file-id id
:cause cause))))
;; Mark file media objects to be deleted
(db/update! conn :file-media-object
{:deleted-at deleted-at}
{:file-id id})
;; Mark thumbnails to be deleted
(db/update! conn :file-thumbnail
{:deleted-at deleted-at}
{:file-id id})
(db/update! conn :file-tagged-object-thumbnail
{:deleted-at deleted-at}
{:file-id id})))
(defmethod delete-object :project
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "project" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :project
{:deleted-at deleted-at}
{:id id}
{::db/return-keys false})
(doseq [file (db/query conn :file
{:project-id id}
{::db/columns [:id :deleted-at]})]
(delete-object cfg (assoc file
:object :file
:deleted-at deleted-at))))
(defmethod delete-object :team
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "team" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :team
{:deleted-at deleted-at}
{:id id}
{::db/return-keys false})
(db/update! conn :team-font-variant
{:deleted-at deleted-at}
{:team-id id}
{::db/return-keys false})
(binding [*team-deletion* true]
(doseq [project (db/query conn :project
{:team-id id}
{::db/columns [:id :deleted-at]})]
(delete-object cfg (assoc project
:object :project
:deleted-at deleted-at)))))
(defmethod delete-object :profile
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "profile" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :profile
{:deleted-at deleted-at}
{:id id}
{::db/return-keys false})
(doseq [team (profile/get-owned-teams conn id)]
(delete-object cfg (assoc team
:object :team
:deleted-at deleted-at))))
(defmethod delete-object :default
[_cfg props]
(l/wrn :hint "not implementation found" :rel (:object props)))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [props] :as task}]
(db/tx-run! cfg delete-object props)))

View File

@@ -299,13 +299,13 @@
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [file-id] :as params}]
(fn [{:keys [props] :as task}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [min-age (dt/duration (or (:min-age params) (::min-age cfg)))
(let [min-age (dt/duration (or (:min-age props) (::min-age cfg)))
cfg (-> cfg
(update ::sto/storage media/configure-assets-storage conn)
(assoc ::file-id file-id)
(assoc ::file-id (:file-id props))
(assoc ::min-age min-age))
total (reduce (fn [total file]
@@ -319,7 +319,7 @@
:processed total)
;; Allow optional rollback passed by params
(when (:rollback? params)
(when (:rollback? props)
(db/rollback! conn))
{:processed total})))))

View File

@@ -29,8 +29,8 @@
(defmethod ig/init-key ::handler
[_ {:keys [::db/pool] :as cfg}]
(fn [params]
(let [min-age (or (:min-age params) (::min-age cfg))]
(fn [{:keys [props] :as task}]
(let [min-age (or (:min-age props) (::min-age cfg))]
(db/with-atomic [conn pool]
(let [interval (db/interval min-age)
result (db/exec-one! conn [sql:delete-files-xlog interval])
@@ -38,7 +38,7 @@
(l/info :hint "task finished" :min-age (dt/format-duration min-age) :total result)
(when (:rollback? params)
(when (:rollback? props)
(db/rollback! conn))
result)))))

View File

@@ -1,32 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.tasks.object-update
"A task used for perform simple object properties update
in an asynchronous flow."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.db :as db]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(defn- update-object
[{:keys [::db/conn] :as cfg} {:keys [id object key val] :as props}]
(l/trc :hint "update object prop"
:id (str id)
:object (d/name object)
:key (d/name key)
:val val)
(db/update! conn object {key val} {:id id} {::db/return-keys false}))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [props] :as params}]
(db/tx-run! cfg update-object props)))

View File

@@ -17,78 +17,24 @@
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare ^:private delete-file-data-fragments!)
(declare ^:private delete-file-media-objects!)
(declare ^:private delete-file-object-thumbnails!)
(declare ^:private delete-file-thumbnails!)
(declare ^:private delete-files!)
(declare ^:private delete-fonts!)
(declare ^:private delete-profiles!)
(declare ^:private delete-projects!)
(declare ^:private delete-teams!)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool ::sto/storage]))
(defmethod ig/prep-key ::handler
[_ cfg]
(assoc cfg ::min-age cf/deletion-delay))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [params]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
;; Disable deletion protection for the current transaction
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(let [min-age (dt/duration (or (:min-age params) (::min-age cfg)))
cfg (-> cfg
(assoc ::min-age (db/interval min-age))
(update ::sto/storage media/configure-assets-storage conn))
total (reduce + 0
[(delete-profiles! cfg)
(delete-teams! cfg)
(delete-fonts! cfg)
(delete-projects! cfg)
(delete-files! cfg)
(delete-file-thumbnails! cfg)
(delete-file-object-thumbnails! cfg)
(delete-file-data-fragments! cfg)
(delete-file-media-objects! cfg)])]
(l/info :hint "task finished"
:deleted total
:rollback? (boolean (:rollback? params)))
(when (:rollback? params)
(db/rollback! conn))
{:processed total})))))
(def ^:private sql:get-profiles
"SELECT id, photo_id FROM profile
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-profiles!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-profiles min-age])
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-profiles min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [id photo-id]}]
(l/trc :hint "permanently delete" :rel "profile" :id (str id))
;; Mark as deleted the storage object
(some->> photo-id (sto/touch-object! storage))
;; And finally, permanently delete the profile. The
;; relevant objects will be deleted using DELETE
;; CASCADE database triggers. This may leave orphan
;; teams, but there is a special task for deleting
;; orphaned teams.
(db/delete! conn :profile {:id id})
(inc total))
@@ -99,13 +45,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-teams!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-teams min-age])
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-teams min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [id photo-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "team"
@@ -118,15 +64,6 @@
;; And finally, permanently delete the team.
(db/delete! conn :team {:id id})
;; Mark for deletion in cascade
(db/update! conn :team-font-variant
{:deleted-at deleted-at}
{:team-id id})
(db/update! conn :project
{:deleted-at deleted-at}
{:team-id id})
(inc total))
0)))
@@ -136,12 +73,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-fonts!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-fonts min-age])
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-fonts min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [id team-id deleted-at] :as font}]
(l/trc :hint "permanently delete"
:rel "team-font-variant"
@@ -167,12 +105,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-projects!
[{:keys [::db/conn ::min-age] :as cfg}]
(->> (db/cursor conn [sql:get-projects min-age])
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
(->> (db/cursor conn [sql:get-projects min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [id team-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "project"
@@ -183,11 +122,6 @@
;; And finally, permanently delete the project.
(db/delete! conn :project {:id id})
;; Mark files to be deleted
(db/update! conn :file
{:deleted-at deleted-at}
{:project-id id})
(inc total))
0)))
@@ -197,12 +131,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-files!
[{:keys [::db/conn ::min-age] :as cfg}]
(->> (db/cursor conn [sql:get-files min-age])
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
(->> (db/cursor conn [sql:get-files min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [id deleted-at project-id]}]
(l/trc :hint "permanently delete"
:rel "file"
@@ -210,26 +145,9 @@
:project-id (str project-id)
:deleted-at (dt/format-instant deleted-at))
;; NOTE: fragments not handled here because they have
;; cascade.
;; And finally, permanently delete the file.
(db/delete! conn :file {:id id})
;; Mark file media objects to be deleted
(db/update! conn :file-media-object
{:deleted-at deleted-at}
{:file-id id})
;; Mark thumbnails to be deleted
(db/update! conn :file-thumbnail
{:deleted-at deleted-at}
{:file-id id})
(db/update! conn :file-tagged-object-thumbnail
{:deleted-at deleted-at}
{:file-id id})
(inc total))
0)))
@@ -239,12 +157,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn delete-file-thumbnails!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-thumbnails min-age])
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-thumbnails min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-thumbnail"
@@ -267,12 +186,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn delete-file-object-thumbnails!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-object-thumbnails min-age])
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-object-thumbnails min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-tagged-object-thumbnail"
@@ -295,12 +215,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-data-fragments!
[{:keys [::db/conn ::min-age] :as cfg}]
(->> (db/cursor conn [sql:get-file-data-fragments min-age])
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
(->> (db/cursor conn [sql:get-file-data-fragments min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [file-id id deleted-at]}]
(l/trc :hint "permanently delete"
:rel "file-data-fragment"
@@ -319,12 +240,13 @@
WHERE deleted_at IS NOT NULL
AND deleted_at < now() - ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-media-objects!
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-media-objects min-age])
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
(->> (db/cursor conn [sql:get-file-media-objects min-age chunk-size] {:chunk-size 1})
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
(l/trc :hint "permanently delete"
:rel "file-media-object"
@@ -340,3 +262,53 @@
(inc total))
0)))
(def ^:private deletion-proc-vars
[#'delete-profiles!
#'delete-file-media-objects!
#'delete-file-data-fragments!
#'delete-file-object-thumbnails!
#'delete-file-thumbnails!
#'delete-files!
#'delete-projects!
#'delete-fonts!
#'delete-teams!])
(defn- execute-proc!
"A generic function that executes the specified proc iterativelly
until 0 results is returned"
[cfg proc-fn]
(loop [total 0]
(let [result (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
(proc-fn cfg)))]
(if (pos? result)
(recur (+ total result))
total))))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool ::sto/storage]))
(defmethod ig/prep-key ::handler
[_ cfg]
(assoc cfg
::min-age cf/deletion-delay
::chunk-size 10))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [props] :as task}]
(let [min-age (dt/duration (or (:min-age props) (::min-age cfg)))
cfg (-> cfg
(assoc ::min-age (db/interval min-age))
(update ::sto/storage media/configure-assets-storage))]
(loop [procs (map deref deletion-proc-vars)
total 0]
(if-let [proc-fn (first procs)]
(let [result (execute-proc! cfg proc-fn)]
(recur (rest procs)
(+ total result)))
(do
(l/inf :hint "task finished" :deleted total)
{:processed total}))))))

View File

@@ -1,59 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.tasks.orphan-teams-gc
"A maintenance task that performs orphan teams GC."
(:require
[app.common.logging :as l]
[app.db :as db]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(declare ^:private delete-orphan-teams!)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [params]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(l/inf :hint "gc started" :rollback? (boolean (:rollback? params)))
(let [total (delete-orphan-teams! cfg)]
(l/inf :hint "task finished"
:teams total
:rollback? (boolean (:rollback? params)))
(when (:rollback? params)
(db/rollback! conn))
{:processed total})))))
(def ^:private sql:get-orphan-teams
"SELECT t.id
FROM team AS t
LEFT JOIN team_profile_rel AS tpr
ON (t.id = tpr.team_id)
WHERE tpr.profile_id IS NULL
AND t.deleted_at IS NULL
ORDER BY t.created_at ASC
FOR UPDATE OF t
SKIP LOCKED")
(defn- delete-orphan-teams!
"Find all orphan teams (with no members) and mark them for
deletion (soft delete)."
[{:keys [::db/conn] :as cfg}]
(->> (db/cursor conn sql:get-orphan-teams)
(map :id)
(reduce (fn [total team-id]
(l/trc :hint "mark orphan team for deletion" :id (str team-id))
(db/update! conn :team
{:deleted-at (dt/now)}
{:id team-id})
(inc total))
0)))

View File

@@ -27,8 +27,8 @@
(defmethod ig/init-key ::handler
[_ {:keys [::db/pool ::min-age] :as cfg}]
(fn [params]
(let [min-age (or (:min-age params) min-age)]
(fn [{:keys [props] :as task}]
(let [min-age (or (:min-age props) min-age)]
(db/with-atomic [conn pool]
(let [interval (db/interval min-age)
result (db/exec-one! conn [sql:delete-completed-tasks interval])
@@ -36,7 +36,7 @@
(l/debug :hint "task finished" :total result)
(when (:rollback? params)
(when (:rollback? props)
(db/rollback! conn))
result)))))

View File

@@ -206,14 +206,16 @@
(defmethod ig/init-key ::handler
[_ {:keys [::db/pool ::setup/props] :as cfg}]
(fn [{:keys [send? enabled?] :or {send? true enabled? false}}]
(let [subs {:newsletter-updates (get-subscriptions-newsletter-updates pool)
:newsletter-news (get-subscriptions-newsletter-news pool)}
enabled? (or enabled?
(fn [task]
(let [params (:props task)
send? (get params :send? true)
enabled? (or (get params :enabled? false)
(contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
subs {:newsletter-updates (get-subscriptions-newsletter-updates pool)
:newsletter-news (get-subscriptions-newsletter-news pool)}
data {:subscriptions subs
:version (:full cf/version)
:instance-id (:instance-id props)}]

View File

@@ -8,18 +8,19 @@
"Tokens generation API."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.transit :as t]
[app.util.time :as dt]
[buddy.sign.jwe :as jwe]
[clojure.spec.alpha :as s]))
(s/def ::tokens-key bytes?)
[buddy.sign.jwe :as jwe]))
(defn generate
[{:keys [tokens-key]} claims]
(us/assert! ::tokens-key tokens-key)
(dm/assert!
"expexted token-key to be bytes instance"
(bytes? tokens-key))
(let [payload (-> claims
(assoc :iat (dt/now))
(d/without-nils)
@@ -39,15 +40,13 @@
(ex/raise :type :validation
:code :invalid-token
:reason :token-expired
:params params
:claims claims))
:params params))
(when (and (contains? params :iss)
(not= (:iss claims)
(:iss params)))
(ex/raise :type :validation
:code :invalid-token
:reason :invalid-issuer
:claims claims
:params params))
claims))

View File

@@ -0,0 +1,37 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.inet
"INET addr parsing and validation helpers"
(:require
[cuerdas.core :as str]
[ring.request :as rreq])
(:import
com.google.common.net.InetAddresses
java.net.InetAddress))
(defn valid?
[s]
(InetAddresses/isInetAddress s))
(defn normalize
[s]
(try
(let [addr (InetAddresses/forString s)]
(.getHostAddress ^InetAddress addr))
(catch Throwable _cause
nil)))
(defn parse-request
[request]
(or (some-> (rreq/get-header request "x-real-ip")
(normalize))
(some-> (rreq/get-header request "x-forwarded-for")
(str/split #"\s*,\s*")
(first)
(normalize))
(some-> (rreq/remote-addr request)
(normalize))))

View File

@@ -19,7 +19,8 @@
[app.common.fressian :as fres]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[clojure.core :as c])
[clojure.core :as c]
[clojure.data.json :as json])
(:import
clojure.lang.Counted
clojure.lang.IHashEq
@@ -83,6 +84,10 @@
^:unsynchronized-mutable loaded?
^:unsynchronized-mutable modified?]
json/JSONWriter
(-write [this writter options]
(json/-write (into {} this) writter options))
IHashEq
(hasheq [this]
(when-not hash

View File

@@ -40,7 +40,8 @@
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.util.time :as dt]
[clojure.core :as c])
[clojure.core :as c]
[clojure.data.json :as json])
(:import
clojure.lang.Counted
clojure.lang.IDeref
@@ -75,6 +76,14 @@
^:unsynchronized-mutable modified?
^:unsynchronized-mutable loaded?]
json/JSONWriter
(-write [this writter options]
(json/-write {:type "pointer"
:id (get-id this)
:meta (meta this)}
writter
options))
IPointerMap
(load! [_]
(when-not *load-fn*

View File

@@ -11,7 +11,7 @@
[app.common.logging :as l]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.loggers.audit :refer [parse-client-ip]]
[app.util.inet :as inet]
[app.util.time :as dt]
[promesa.exec :as px]
[promesa.exec.csp :as sp]
@@ -84,7 +84,7 @@
output-ch (sp/chan :buf output-buff-size)
hbeat-ch (sp/chan :buf (sp/sliding-buffer 6))
close-ch (sp/chan)
ip-addr (parse-client-ip request)
ip-addr (inet/parse-request request)
uagent (rreq/get-header request "user-agent")
id (uuid/next)
state (atom {})

View File

@@ -8,6 +8,7 @@
"Async tasks abstraction (impl)."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
@@ -58,17 +59,6 @@
;; SUBMIT API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- extract-props
[options]
(let [cns (namespace ::sample)]
(persistent!
(reduce-kv (fn [res k v]
(cond-> res
(not= (namespace k) cns)
(assoc! k v)))
(transient {})
options))))
(def ^:private sql:insert-new-task
"insert into task (id, name, props, queue, label, priority, max_retries, scheduled_at)
values (?, ?, ?, ?, ?, ?, ?, now() + ?)
@@ -87,14 +77,13 @@
(s/def ::task (s/or :kw keyword? :str string?))
(s/def ::queue (s/or :kw keyword? :str string?))
(s/def ::delay (s/or :int integer? :duration dt/duration?))
(s/def ::conn (s/or :pool ::db/pool :connection some?))
(s/def ::priority integer?)
(s/def ::max-retries integer?)
(s/def ::dedupe boolean?)
(s/def ::submit-options
(s/and
(s/keys :req [::task ::conn]
(s/keys :req [::task]
:opt [::label ::delay ::queue ::priority ::max-retries ::dedupe])
(fn [{:keys [::dedupe ::label] :or {label ""}}]
(if dedupe
@@ -102,21 +91,23 @@
true))))
(defn submit!
[& {:keys [::task ::delay ::queue ::priority ::max-retries ::conn ::dedupe ::label]
[& {:keys [::params ::task ::delay ::queue ::priority ::max-retries ::dedupe ::label]
:or {delay 0 queue :default priority 100 max-retries 3 label ""}
:as options}]
(us/verify! ::submit-options options)
(let [duration (dt/duration delay)
interval (db/interval duration)
props (-> options extract-props db/tjson)
props (db/tjson params)
id (uuid/next)
tenant (cf/get :tenant)
task (d/name task)
queue (str/ffmt "%:%" tenant (d/name queue))
conn (db/get-connectable options)
deleted (when dedupe
(-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label])
:next.jdbc/update-count))]
(l/trc :hint "submit task"
:name task
:task-id (str id)
@@ -126,7 +117,13 @@
:delay (dt/format-duration duration)
:replace (or deleted 0))
(db/exec-one! conn [sql:insert-new-task id task props queue
label priority max-retries interval])
id))
(defn invoke!
[{:keys [::task ::params] :as cfg}]
(assert (contains? cfg :app.worker/registry)
"missing worker registry on `cfg`")
(let [task-fn (dm/get-in cfg [:app.worker/registry (name task)])]
(task-fn {:props params})))

View File

@@ -35,8 +35,92 @@
[_ item]
{:params item})
(defn- get-task
[{:keys [::db/pool]} task-id]
(ex/try!
(some-> (db/get* pool :task {:id task-id})
(decode-task-row))))
(defn- run-task
[{:keys [::wrk/registry ::id ::queue] :as cfg} task]
(try
(l/dbg :hint "start"
:name (:name task)
:task-id (str (:id task))
:queue queue
:runner-id id
:retry (:retry-num task))
(let [tpoint (dt/tpoint)
task-fn (get registry (:name task))
result (if task-fn
(task-fn task)
{:status :completed :task task})
elapsed (dt/format-duration (tpoint))]
(when-not task-fn
(l/wrn :hint "no task handler found" :name (:name task)))
(l/dbg :hint "end"
:name (:name task)
:task-id (str (:id task))
:queue queue
:runner-id id
:retry (:retry-num task)
:elapsed elapsed)
result)
(catch InterruptedException cause
(throw cause))
(catch Throwable cause
(let [edata (ex-data cause)]
(if (and (< (:retry-num task)
(:max-retries task))
(= ::retry (:type edata)))
(cond-> {:status :retry :task task :error cause}
(dt/duration? (:delay edata))
(assoc :delay (:delay edata))
(= ::noop (:strategy edata))
(assoc :inc-by 0))
(do
(l/err :hint "unhandled exception on task"
::l/context (get-error-context cause task)
:cause cause)
(if (>= (:retry-num task) (:max-retries task))
{:status :failed :task task :error cause}
{:status :retry :task task :error cause})))))))
(defn- run-task!
[{:keys [::rds/rconn ::id] :as cfg} task-id]
(loop [task (get-task cfg task-id)]
(cond
(ex/exception? task)
(if (or (db/connection-error? task)
(db/serialization-error? task))
(do
(l/wrn :hint "connection error on retrieving task from database (retrying in some instants)"
:id id
:cause task)
(px/sleep (::rds/timeout rconn))
(recur (get-task cfg task-id)))
(do
(l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)"
:id id
:cause task)
(px/sleep (::rds/timeout rconn))
(recur (get-task cfg task-id))))
(nil? task)
(l/wrn :hint "no task found on the database"
:id id
:task-id task-id)
:else
(run-task cfg task))))
(defn- run-worker-loop!
[{:keys [::db/pool ::rds/rconn ::wrk/registry ::timeout ::queue ::id]}]
[{:keys [::db/pool ::rds/rconn ::timeout ::queue] :as cfg}]
(letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}]
(let [explain (ex-message error)
nretry (+ (:retry-num task) inc-by)
@@ -82,88 +166,6 @@
:length (alength payload)
:cause cause))))
(handle-task [{:keys [name] :as task}]
(let [task-fn (get registry name)]
(if task-fn
(task-fn task)
(l/wrn :hint "no task handler found" :name name))
{:status :completed :task task}))
(handle-task-exception [cause task]
(let [edata (ex-data cause)]
(if (and (< (:retry-num task)
(:max-retries task))
(= ::retry (:type edata)))
(cond-> {:status :retry :task task :error cause}
(dt/duration? (:delay edata))
(assoc :delay (:delay edata))
(= ::noop (:strategy edata))
(assoc :inc-by 0))
(do
(l/err :hint "unhandled exception on task"
::l/context (get-error-context cause task)
:cause cause)
(if (>= (:retry-num task) (:max-retries task))
{:status :failed :task task :error cause}
{:status :retry :task task :error cause})))))
(get-task [task-id]
(ex/try!
(some-> (db/get* pool :task {:id task-id})
(decode-task-row))))
(run-task [task-id]
(loop [task (get-task task-id)]
(cond
(ex/exception? task)
(if (or (db/connection-error? task)
(db/serialization-error? task))
(do
(l/wrn :hint "connection error on retrieving task from database (retrying in some instants)"
:id id
:cause task)
(px/sleep (::rds/timeout rconn))
(recur (get-task task-id)))
(do
(l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)"
:id id
:cause task)
(px/sleep (::rds/timeout rconn))
(recur (get-task task-id))))
(nil? task)
(l/wrn :hint "no task found on the database"
:id id
:task-id task-id)
:else
(try
(l/dbg :hint "start"
:name (:name task)
:task-id (str task-id)
:queue queue
:runner-id id
:retry (:retry-num task))
(let [tpoint (dt/tpoint)
result (handle-task task)
elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "end"
:name (:name task)
:task-id (str task-id)
:queue queue
:runner-id id
:retry (:retry-num task)
:elapsed elapsed)
result)
(catch InterruptedException cause
(throw cause))
(catch Throwable cause
(handle-task-exception cause task))))))
(process-result [{:keys [status] :as result}]
(ex/try!
(case status
@@ -173,7 +175,7 @@
nil)))
(run-task-loop [task-id]
(loop [result (run-task task-id)]
(loop [result (run-task! cfg task-id)]
(when-let [cause (process-result result)]
(if (or (db/connection-error? cause)
(db/serialization-error? cause))

View File

@@ -34,6 +34,8 @@
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as wrk]
[app.worker.runner]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[clojure.test :as t]
@@ -77,47 +79,6 @@
:enable-feature-components-v2
:disable-file-validation])
(def test-init-sql
["alter table project_profile_rel set unlogged;\n"
"alter table file_profile_rel set unlogged;\n"
"alter table presence set unlogged;\n"
"alter table presence set unlogged;\n"
"alter table http_session set unlogged;\n"
"alter table team_profile_rel set unlogged;\n"
"alter table team_project_profile_rel set unlogged;\n"
"alter table comment_thread_status set unlogged;\n"
"alter table comment set unlogged;\n"
"alter table comment_thread set unlogged;\n"
"alter table profile_complaint_report set unlogged;\n"
"alter table file_change set unlogged;\n"
"alter table team_font_variant set unlogged;\n"
"alter table share_link set unlogged;\n"
"alter table usage_quote set unlogged;\n"
"alter table access_token set unlogged;\n"
"alter table profile set unlogged;\n"
"alter table file_library_rel set unlogged;\n"
"alter table file_thumbnail set unlogged;\n"
"alter table file_object_thumbnail set unlogged;\n"
"alter table file_tagged_object_thumbnail set unlogged;\n"
"alter table file_media_object set unlogged;\n"
"alter table file_data_fragment set unlogged;\n"
"alter table file set unlogged;\n"
"alter table project set unlogged;\n"
"alter table team_invitation set unlogged;\n"
"alter table webhook_delivery set unlogged;\n"
"alter table webhook set unlogged;\n"
"alter table team set unlogged;\n"
;; For some reason, modifying the task realted tables is very very
;; slow (5s); so we just don't alter them
;; "alter table task set unlogged;\n"
;; "alter table task_default set unlogged;\n"
;; "alter table task_completed set unlogged;\n"
"alter table audit_log set unlogged ;\n"
"alter table storage_object set unlogged;\n"
"alter table server_error_report set unlogged;\n"
"alter table server_prop set unlogged;\n"
"alter table global_complaint_report set unlogged;\n"])
(defn state-init
[next]
(with-redefs [app.config/flags (flags/parse flags/default default-flags)
@@ -164,9 +125,6 @@
(try
(binding [*system* system
*pool* (:app.db/pool system)]
(db/with-atomic [conn *pool*]
(doseq [sql test-init-sql]
(db/exec! conn [sql])))
(next))
(finally
(ig/halt! system))))))
@@ -181,8 +139,7 @@
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
(let [result (->> (db/exec! conn [sql])
(map :table-name)
(remove #(= "task" %)))]
(map :table-name))]
(doseq [table result]
(db/exec! conn [(str "delete from " table ";")]))))
@@ -263,7 +220,7 @@
([params]
(mark-file-deleted* *system* params))
([conn {:keys [id] :as params}]
(#'files/mark-file-deleted! conn id)))
(#'files/mark-file-deleted conn id)))
(defn create-team*
([i params] (create-team* *system* i params))
@@ -421,9 +378,21 @@
([name]
(run-task! name {}))
([name params]
(let [tasks (:app.worker/registry *system*)]
(let [task-fn (get tasks (d/name name))]
(task-fn params)))))
(wrk/invoke! (-> *system*
(assoc ::wrk/task name)
(assoc ::wrk/params params)))))
(def sql:pending-tasks
"select t.* from task as t
where t.status = 'new'
order by t.priority desc, t.scheduled_at")
(defn run-pending-tasks!
[]
(db/tx-run! *system* (fn [{:keys [::db/conn] :as cfg}]
(let [tasks (->> (db/exec! conn [sql:pending-tasks])
(map #'app.worker.runner/decode-task-row))]
(run! (partial #'app.worker.runner/run-task cfg) tasks)))))
;; --- UTILS

View File

@@ -21,11 +21,10 @@
(with-mocks [submit-mock {:target 'app.worker/submit! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
res (th/run-task! :process-webhook-event
{:props
{:app.loggers.webhooks/event
{:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}}}})]
{:event
{:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}}})]
(t/is (= 0 (:call-count @submit-mock)))
(t/is (nil? res)))))
@@ -35,11 +34,10 @@
(let [prof (th/create-profile* 1 {:is-active true})
whk (th/create-webhook* {:team-id (:default-team-id prof)})
res (th/run-task! :process-webhook-event
{:props
{:app.loggers.webhooks/event
{:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}}}})]
{:event
{:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}}})]
(t/is (= 1 (:call-count @submit-mock)))
(t/is (nil? res)))))
@@ -52,9 +50,8 @@
:name "create-project"
:props {:team-id (:default-team-id prof)}}
res (th/run-task! :run-webhook
{:props
{:app.loggers.webhooks/event evt
:app.loggers.webhooks/config whk}})]
{:event evt
:config whk})]
(t/is (= 1 (:call-count @http-mock)))
@@ -75,9 +72,8 @@
:name "create-project"
:props {:team-id (:default-team-id prof)}}
res (th/run-task! :run-webhook
{:props
{:app.loggers.webhooks/event evt
:app.loggers.webhooks/config whk}})]
{:event evt
:config whk})]
(t/is (= 1 (:call-count @http-mock)))
@@ -94,14 +90,12 @@
;; RUN 2 times more
(th/run-task! :run-webhook
{:props
{:app.loggers.webhooks/event evt
:app.loggers.webhooks/config whk}})
{:event evt
:config whk})
(th/run-task! :run-webhook
{:props
{:app.loggers.webhooks/event evt
:app.loggers.webhooks/config whk}})
{:event evt
:config whk})
(let [rows (th/db-query :webhook-delivery {:webhook-id (:id whk)})]

View File

@@ -28,7 +28,8 @@
ring.request/Request
(get-header [_ name]
(case name
"x-forwarded-for" "127.0.0.44"))))
"x-forwarded-for" "127.0.0.44"
"x-real-ip" "127.0.0.43"))))
(t/deftest push-events-1
(with-redefs [app.config/flags #{:audit-log}]
@@ -46,6 +47,7 @@
:profile-id (:id prof)
:timestamp (dt/now)
:type "action"}]}
params (with-meta params
{:app.http/request http-request})

View File

@@ -1189,6 +1189,7 @@
(t/is (nil? error))
(t/is (map? result)))
;; insert another thumbnail with different revn
(let [data {::th/type :create-file-thumbnail
::rpc/profile-id (:id prof)
:file-id (:id file)
@@ -1207,8 +1208,6 @@
(t/is (= 2 (count rows)))))
(t/testing "gc task"
;; make the file eligible for GC waiting 300ms (configured
;; timeout for testing)
(let [res (th/run-task! :file-gc {:min-age 0})]
(t/is (= 1 (:processed res))))

View File

@@ -346,13 +346,5 @@
(assoc :size 312043))))
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (map? (:result out))))
(t/is (map? (:result out))))))
(let [[row1 :as rows]
(->> (th/db-query :task {:name "object-update"})
(map #(update % :props db/decode-transit-pgobject)))]
;; (app.common.pprint/pprint rows)
(t/is (= 1 (count rows)))
(t/is (> (inst-ms (dt/diff (:created-at row1) (:scheduled-at row1)))
(inst-ms (dt/duration "4m")))))))

View File

@@ -6,10 +6,11 @@
(ns backend-tests.rpc-profile-test
(:require
[app.auth :as auth]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.email.blacklist :as email.blacklist]
[app.email.whitelist :as email.whitelist]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.tokens :as tokens]
@@ -126,7 +127,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))))))
(t/deftest profile-deletion-simple
(t/deftest profile-deletion-1
(let [prof (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
@@ -152,23 +153,22 @@
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out)))))
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted false})]
(t/is (nil? (:deleted-at row))))
(let [result (th/run-task! :orphan-teams-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(th/run-pending-tasks!)
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted false})]
(t/is (dt/instant? (:deleted-at row))))
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 4 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted false})]
(t/is (nil? row)))
;; query profile after delete
(let [params {::th/type :get-profile
::rpc/profile-id (:id prof)}
@@ -177,14 +177,187 @@
(let [result (:result out)]
(t/is (= uuid/zero (:id result)))))))
(t/deftest registration-domain-whitelist
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]
(t/testing "allowed email domain"
(t/is (true? (auth/email-domain-in-whitelist? whitelist "username@ya.ru")))
(t/is (true? (auth/email-domain-in-whitelist? #{} "username@somedomain.com"))))
(t/deftest profile-deletion-2
(let [prof1 (th/create-profile* 1)
prof2 (th/create-profile* 2)
file1 (th/create-file* 1 {:profile-id (:id prof1)
:project-id (:default-project-id prof1)
:is-shared false})
team1 (th/create-team* 1 {:profile-id (:id prof1)})
(t/testing "not allowed email domain"
(t/is (false? (auth/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
role1 (th/create-team-role* {:team-id (:id team1)
:profile-id (:id prof2)
:role :editor})]
;; Assert all roles for team
(let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})]
(t/is (= 2 (count roles))))
;; Request profile to be deleted
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-teams-with-people))))))
(t/deftest profile-deletion-3
(let [prof1 (th/create-profile* 1)
prof2 (th/create-profile* 2)
prof3 (th/create-profile* 3)
file1 (th/create-file* 1 {:profile-id (:id prof1)
:project-id (:default-project-id prof1)
:is-shared false})
team1 (th/create-team* 1 {:profile-id (:id prof1)})
role1 (th/create-team-role* {:team-id (:id team1)
:profile-id (:id prof2)
:role :editor})
role2 (th/create-team-role* {:team-id (:id team1)
:profile-id (:id prof3)
:role :editor})]
;; Assert all roles for team
(let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})]
(t/is (= 3 (count roles))))
;; Request profile to be deleted (it should fail)
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-teams-with-people))))
;; Leave team by role 1
(let [params {::th/type :leave-team
::rpc/profile-id (:id prof2)
:id (:id team1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
;; Request profile to be deleted (it should fail)
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-teams-with-people))))
;; Leave team by role 0 (the default) and reassing owner to role 3
;; without reassinging it (should fail)
(let [params {::th/type :leave-team
::rpc/profile-id (:id prof1)
;; :reassign-to (:id prof3)
:id (:id team1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-cant-leave-team))))
;; Leave team by role 0 (the default) and reassing owner to role 3
(let [params {::th/type :leave-team
::rpc/profile-id (:id prof1)
:reassign-to (:id prof3)
:id (:id team1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
;; Request profile to be deleted
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (= {} (:result out)))
(t/is (nil? (:error out))))
;; query files after profile soft deletion
(let [params {::th/type :get-project-files
::rpc/profile-id (:id prof1)
:project-id (:default-project-id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out)))))
(th/run-pending-tasks!)
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 4 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof1)}
{::db/remove-deleted false})]
(t/is (nil? row)))
;; query profile after delete
(let [params {::th/type :get-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [result (:result out)]
(t/is (= uuid/zero (:id result)))))))
(t/deftest profile-deletion-4
(let [prof1 (th/create-profile* 1)
file1 (th/create-file* 1 {:profile-id (:id prof1)
:project-id (:default-project-id prof1)
:is-shared false})
team1 (th/create-team* 1 {:profile-id (:id prof1)})
team2 (th/create-team* 2 {:profile-id (:id prof1)})]
;; Request profile to be deleted
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (= {} (:result out)))
(t/is (nil? (:error out))))
(th/run-pending-tasks!)
(let [rows (th/db-exec! ["select id,name,deleted_at from team where deleted_at is not null"])]
(t/is (= 3 (count rows))))
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 8 (:processed result))))))
(t/deftest email-blacklist-1
(t/is (false? (email.blacklist/enabled? th/*system*)))
(t/is (true? (email.blacklist/enabled? (assoc th/*system* :app.email/blacklist []))))
(t/is (true? (email.blacklist/contains? (assoc th/*system* :app.email/blacklist #{"foo.com"}) "AA@FOO.COM"))))
(t/deftest email-whitelist-1
(t/is (false? (email.whitelist/enabled? th/*system*)))
(t/is (true? (email.whitelist/enabled? (assoc th/*system* :app.email/whitelist []))))
(t/is (true? (email.whitelist/contains? (assoc th/*system* :app.email/whitelist #{"foo.com"}) "AA@FOO.COM"))))
(t/deftest prepare-register-and-register-profile-1
(let [data {::th/type :prepare-register-profile
@@ -229,20 +402,51 @@
(t/is (= "mtma" (:penpot/mtm-campaign props)))))))
(t/deftest prepare-register-and-register-profile-2
(with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)]
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [current-token (atom nil)]
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [current-token (atom nil)]
;; PREPARE REGISTER
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
;; PREPARE REGISTER
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (string? token))
(reset! current-token token))
;; DO REGISTRATION
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 1 (:call-count @mock))))
;; DO REGISTRATION: try correct register attempt 1
(th/reset-mock! mock)
;; PREPARE REGISTER: second attempt
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
;; DO REGISTRATION: second attempt
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 0 (:call-count @mock))))
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
;; DO REGISTRATION: third attempt
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
@@ -250,45 +454,105 @@
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 1 (:call-count @mock))))
(t/is (= 1 (:call-count @mock))))))))
(th/reset-mock! mock)
(t/deftest prepare-register-and-register-profile-3
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [current-token (atom nil)]
;; PREPARE REGISTER
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
;; PREPARE REGISTER without waiting for threshold
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (= :validation (-> out :error th/ex-type)))
(t/is (= :email-already-exists (-> out :error th/ex-code))))
;; DO REGISTRATION
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 1 (:call-count @mock))))
(th/sleep {:millis 500})
(th/reset-mock! mock)
(th/reset-mock! mock)
;; PREPARE REGISTER waiting the threshold
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)]
(th/db-update! :profile
{:is-blocked true}
{:email "hello@example.com"})
(t/is (th/success? out))
(t/is (= 0 (:call-count @mock)))
;; PREPARE REGISTER: second attempt
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
(let [result (:result out)]
(t/is (contains? result :token))
(reset! current-token (:token result))))
;; DO REGISTRATION: try correct register attempt 1
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
;; DO REGISTRATION: second attempt
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))))))
(t/is (nil? (:error out)))
(t/is (= 0 (:call-count @mock))))))))
(t/deftest prepare-and-register-with-invitation-and-enabled-registration-1
(let [sprops (:app.setup/props th/*system*)
itoken (tokens/generate sprops
{:iss :team-invitation
:exp (dt/in-future "48h")
:role :editor
:team-id uuid/zero
:member-email "user@example.com"})
data {::th/type :prepare-register-profile
:invitation-token itoken
:email "user@example.com"
:password "foobar"}
{:keys [result error] :as out} (th/command! data)]
(t/is (nil? error))
(t/is (map? result))
(t/is (string? (:token result)))
(let [rtoken (:token result)
data {::th/type :register-profile
:token rtoken
:fullname "foobar"}
{:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (map? result))
(t/is (string? (:invitation-token result))))))
(t/deftest prepare-and-register-with-invitation-and-enabled-registration-2
(let [sprops (:app.setup/props th/*system*)
itoken (tokens/generate sprops
{:iss :team-invitation
:exp (dt/in-future "48h")
:role :editor
:team-id uuid/zero
:member-email "user2@example.com"})
data {::th/type :prepare-register-profile
:invitation-token itoken
:email "user@example.com"
:password "foobar"}
out (th/command! data)]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-does-not-match-invitation (:code edata))))))
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
(with-redefs [app.config/flags [:disable-registration]]
@@ -303,22 +567,12 @@
:invitation-token itoken
:email "user@example.com"
:password "foobar"}
out (th/command! data)]
{:keys [result error] :as out} (th/command! data)]
(t/is (nil? error))
(t/is (map? result))
(t/is (string? (:token result)))
(let [rtoken (:token result)
data {::th/type :register-profile
:token rtoken
:fullname "foobar"}
{:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out)
(t/is (nil? error))
(t/is (map? result))
(t/is (string? (:invitation-token result)))))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :registration-disabled (:code edata)))))))
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-2
(with-redefs [app.config/flags [:disable-registration]]
@@ -339,7 +593,28 @@
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-does-not-match-invitation (:code edata)))))))
(t/is (= :registration-disabled (:code edata)))))))
(t/deftest prepare-and-register-with-invitation-and-disabled-login-with-password
(with-redefs [app.config/flags [:disable-login-with-password]]
(let [sprops (:app.setup/props th/*system*)
itoken (tokens/generate sprops
{:iss :team-invitation
:exp (dt/in-future "48h")
:role :editor
:team-id uuid/zero
:member-email "user2@example.com"})
data {::th/type :prepare-register-profile
:invitation-token itoken
:email "user@example.com"
:password "foobar"}
out (th/command! data)]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :registration-disabled (:code edata)))))))
(t/deftest prepare-register-with-registration-disabled
(with-redefs [app.config/flags #{}]
@@ -359,13 +634,13 @@
:email (:email profile)
:password "foobar"}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token)))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :email-already-exists (:code edata))))))
(t/deftest prepare-register-profile-with-bounced-email
(t/deftest register-profile-with-bounced-email
(let [pool (:app.db/pool th/*system*)
data {::th/type :prepare-register-profile
:email "user@example.com"
@@ -376,7 +651,7 @@
(let [out (th/command! data)]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))))
(t/deftest register-profile-with-complained-email
@@ -388,9 +663,11 @@
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
(let [out (th/command! data)]
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token))))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-complaints (:code edata)))))))
(t/deftest register-profile-with-email-as-password
(let [data {::th/type :prepare-register-profile
@@ -421,20 +698,26 @@
;; with complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email data)})
(let [out (th/command! data)]
(let [out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock))))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-complaints (:code edata))))
(t/is (= 1 (:call-count @mock))))
;; with bounces
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
(let [out (th/command! data)
error (:error out)]
(let [out (th/command! data)]
;; (th/print-result! out)
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
(t/is (= 2 (:call-count @mock)))))))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata))))
(t/is (= 1 (:call-count @mock)))))))
(t/deftest email-change-request-without-smtp
@@ -455,7 +738,7 @@
(t/deftest request-profile-recovery
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1)
(let [profile1 (th/create-profile* 1 {:is-active false})
profile2 (th/create-profile* 2 {:is-active true})
pool (:app.db/pool th/*system*)
data {::th/type :request-profile-recovery}]
@@ -468,38 +751,47 @@
;; with valid email inactive user
(let [data (assoc data :email (:email profile1))
out (th/command! data)
error (:error out)]
out (th/command! data)]
(t/is (= 0 (:call-count @mock)))
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :profile-not-verified)))
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
;; with valid email inactive user
(let [data (assoc data :email (:email profile1))
out (th/command! data)]
(t/is (= 1 (:call-count @mock)))
(t/is (nil? (:result out)))
(t/is (nil? (:error out)))))
(th/reset-mock! mock)
;; with valid email and active user
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 1 (:call-count @mock))))
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 1 (:call-count @mock))))
;; with valid email and active user with global complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock))))
;; with valid email and active user with global complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 1 (:call-count @mock))))
;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
(t/is (= 2 (:call-count @mock)))
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))))))
;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
(t/is (nil? (:result out)))
(t/is (nil? (:error out)))
;; (th/print-result! out)
(t/is (= 1 (:call-count @mock))))))))
(t/deftest update-profile-password

View File

@@ -62,8 +62,8 @@
(th/reset-mock! mock)
(let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
(t/is (not (th/success? out)))
(t/is (= 0 (:call-count (deref mock)))))
;; get invitation token
(let [params {::th/type :get-team-invitation-token
@@ -86,7 +86,7 @@
(t/is (= 0 (:call-count @mock)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))
;; invite internal user that is muted
@@ -391,6 +391,8 @@
(t/is (= 1 (count result)))
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
(th/run-pending-tasks!)
;; run permanent deletion (should be noop)
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
(t/is (= 0 (:processed result))))
@@ -457,6 +459,8 @@
#_(th/print-result! out)
(t/is (nil? (:error out))))
(th/run-pending-tasks!)
(let [rows (th/db-exec! ["select * from team where id = ?" (:id team)])]
(t/is (= 1 (count rows)))
(t/is (dt/instant? (:deleted-at (first rows)))))

View File

@@ -2,4 +2,5 @@
{:tests
[{:id :unit
:test-paths ["test" "src"]
:ns-patterns [".*-test$"]}]}
:ns-patterns [".*-test$"]
:kaocha/reporter [kaocha.report/dots]}]}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,25 @@
{:deps
{org.clojure/clojure {:mvn/version "1.11.1"}
{org.clojure/clojure {:mvn/version "1.11.2"}
org.clojure/data.json {:mvn/version "2.5.0"}
org.clojure/tools.cli {:mvn/version "1.0.219"}
org.clojure/tools.cli {:mvn/version "1.1.230"}
org.clojure/clojurescript {:mvn/version "1.11.132"}
org.clojure/test.check {:mvn/version "1.1.1"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
org.clojure/data.fressian {:mvn/version "1.1.0"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.22.1"}
org.slf4j/slf4j-api {:mvn/version "2.0.10"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.23.1"}
org.slf4j/slf4j-api {:mvn/version "2.0.13"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
selmer/selmer {:mvn/version "1.12.59"}
selmer/selmer {:mvn/version "1.12.61"}
criterium/criterium {:mvn/version "0.4.6"}
metosin/jsonista {:mvn/version "0.3.8"}
metosin/malli {:mvn/version "0.14.0"}
metosin/malli {:mvn/version "0.16.1"}
expound/expound {:mvn/version "0.9.0"}
com.cognitect/transit-clj {:mvn/version "1.0.333"}
@@ -28,7 +28,7 @@
integrant/integrant {:mvn/version "0.8.1"}
org.apache.commons/commons-pool2 {:mvn/version "2.12.0"}
org.graalvm.js/js {:mvn/version "23.0.2"}
org.graalvm.js/js {:mvn/version "23.0.4"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2023.11.09-407"}
@@ -41,7 +41,7 @@
:git/tag "3.0.0"
:git/url "https://github.com/funcool/datoteka"}
lambdaisland/uri {:mvn/version "1.16.134"
lambdaisland/uri {:mvn/version "1.19.155"
:exclusions [org.clojure/data.json]}
frankiesardo/linked {:mvn/version "1.3.0"}
@@ -63,7 +63,7 @@
{:dev
{:extra-deps
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "2.27.4"}
thheller/shadow-cljs {:mvn/version "2.28.8"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}
@@ -72,16 +72,12 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
{io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}}
:ns-default build}
:test
{:extra-paths ["test"]
:extra-deps
{io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}
{:main-opts ["-m" "kaocha.runner"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}

View File

@@ -5,19 +5,19 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.0.2",
"packageManager": "yarn@4.2.2",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
},
"dependencies": {
"luxon": "^3.4.2",
"sax": "^1.2.4"
"luxon": "^3.4.4",
"sax": "^1.4.1"
},
"devDependencies": {
"shadow-cljs": "2.27.4",
"shadow-cljs": "2.28.8",
"source-map-support": "^0.5.21",
"ws": "^8.13.0"
"ws": "^8.17.0"
},
"scripts": {
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",

View File

@@ -224,7 +224,6 @@
[coll]
(into [] (remove nil?) coll))
(defn without-nils
"Given a map, return a map removing key-value
pairs when value is `nil`."

View File

@@ -48,7 +48,8 @@
"fdata/shape-data-type"
"components/v2"
"styles/v2"
"layout/grid"})
"layout/grid"
"plugins/runtime"})
;; A set of features enabled by default
(def default-features
@@ -62,7 +63,8 @@
;; persist on file features field but can be permanently enabled on
;; team feature field
(def frontend-only-features
#{"styles/v2"})
#{"styles/v2"
"plugins/runtime"})
;; Features that are mainly backend only or there are a proper
;; fallback when frontend reports no support for it
@@ -78,7 +80,8 @@
(-> #{"fdata/objects-map"
"fdata/pointer-map"
"layout/grid"
"fdata/shape-data-type"}
"fdata/shape-data-type"
"plugins/runtime"}
(into frontend-only-features)))
(sm/def! ::features
@@ -97,6 +100,7 @@
:feature-grid-layout "layout/grid"
:feature-fdata-objects-map "fdata/objects-map"
:feature-fdata-pointer-map "fdata/pointer-map"
:feature-plugins "plugins/runtime"
nil))
(defn migrate-legacy-features

View File

@@ -38,17 +38,22 @@
fail-on-spec?]
:or {add-container? false
fail-on-spec? false}}]
(let [component-id (:current-component-id file)
change (cond-> change
(and add-container? (some? component-id))
(-> (assoc :component-id component-id)
(cond-> (some? (:current-frame-id file))
(assoc :frame-id (:current-frame-id file))))
(let [components-v2 (dm/get-in file [:data :options :components-v2])
component-id (:current-component-id file)
change (cond-> change
(and add-container? (some? component-id) (not components-v2))
(-> (assoc :component-id component-id)
(cond-> (some? (:current-frame-id file))
(assoc :frame-id (:current-frame-id file))))
(and add-container? (nil? component-id))
(assoc :page-id (:current-page-id file)
:frame-id (:current-frame-id file)))
valid? (ch/check-change! change)]
(and add-container? (or (nil? component-id) components-v2))
(assoc :page-id (:current-page-id file)
:frame-id (:current-frame-id file)))
valid? (or (and components-v2
(nil? (:component-id change))
(nil? (:page-id change)))
(ch/check-change! change))]
(when-not valid?
(let [explain (sm/explain ::ch/change change)]
@@ -60,12 +65,12 @@
::sm/explain explain))))
(cond-> file
valid?
(-> (update :changes conjv change)
(update :data ch/process-changes [change] false))
(and valid? (or (not add-container?) (some? (:component-id change)) (some? (:page-id change))))
(-> (update :changes conjv change) ;; In components-v2 we do not add shapes
(update :data ch/process-changes [change] false)) ;; inside a component
(not valid?)
(update :errors conjv change)))))
(update :errors conjv change)))));)
(defn- lookup-objects
([file]
@@ -135,19 +140,14 @@
(create-file (uuid/next) name))
([id name]
{:id id
:name name
:data (-> ctf/empty-file-data
(assoc :id id))
;; We keep the changes so we can send them to the backend
:changes []}))
(-> (ctf/make-file {:id id :name name :create-page false})
(assoc :changes [])))) ;; We keep the changes so we can send them to the backend
(defn add-page
[file data]
(dm/assert! (nil? (:current-component-id file)))
(let [page-id (or (:id data) (uuid/next))
page (-> (ctp/make-empty-page page-id "Page 1")
page (-> (ctp/make-empty-page {:id page-id :name "Page 1"})
(d/deep-merge data))]
(-> file
(commit-change
@@ -185,10 +185,11 @@
(update :parent-stack conjv (:id obj)))))
(defn close-artboard [file]
(let [parent-id (-> file :parent-stack peek)
(let [components-v2 (dm/get-in file [:data :options :components-v2])
parent-id (-> file :parent-stack peek)
parent (lookup-shape file parent-id)
current-frame-id (or (:frame-id parent)
(when (nil? (:current-component-id file))
(when (or (nil? (:current-component-id file)) components-v2)
root-id))]
(-> file
(assoc :current-frame-id current-frame-id)
@@ -511,17 +512,26 @@
{:type :del-media
:id id}))))
(defn start-component
([file data] (start-component file data :group))
([file data]
(let [components-v2 (dm/get-in file [:data :options :components-v2])
root-type (if components-v2 :frame :group)]
(start-component file data root-type)))
([file data root-type]
;; FIXME: data probably can be a shape instance, then we can use gsh/shape->rect
(let [selrect (or (grc/make-rect (:x data) (:y data) (:width data) (:height data))
(let [components-v2 (dm/get-in file [:data :options :components-v2])
selrect (or (grc/make-rect (:x data) (:y data) (:width data) (:height data))
grc/empty-rect)
name (:name data)
path (:path data)
main-instance-id (:main-instance-id data)
main-instance-page (:main-instance-page data)
;; In components v1 we must create the root shape and set it inside
;; the :objects attribute of the component. When in components-v2,
;; this will be ignored as the root shape has already been created
;; in its page, by the normal page import.
attrs (-> data
(assoc :type root-type)
(assoc :x (:x selrect))
@@ -543,19 +553,43 @@
(-> file
(commit-change
{:type :add-component
:id (:id obj)
:name name
:path path
:main-instance-id main-instance-id
:main-instance-page main-instance-page
:shapes [obj]})
(cond-> {:type :add-component
:id (:id obj)
:name name
:path path
:main-instance-id main-instance-id
:main-instance-page main-instance-page}
(not components-v2)
(assoc :shapes [obj])))
(assoc :last-id (:id obj))
(assoc :parent-stack [(:id obj)])
(assoc :current-component-id (:id obj))
(assoc :current-frame-id (if (= (:type obj) :frame) (:id obj) uuid/zero))))))
(defn start-deleted-component
[file data]
(let [attrs (-> data
(assoc :id (:main-instance-id data))
(assoc :component-file (:id file))
(assoc :component-id (:id data))
(assoc :x (:main-instance-x data))
(assoc :y (:main-instance-y data))
(dissoc :path)
(dissoc :main-instance-id)
(dissoc :main-instance-page)
(dissoc :main-instance-x)
(dissoc :main-instance-y)
(dissoc :main-instance-parent)
(dissoc :main-instance-frame))]
;; To create a deleted component, first we add all shapes of the main instance
;; in the main instance page, and in the finish event we delete it.
(-> file
(update :parent-stack conjv (:main-instance-parent data))
(assoc :current-page-id (:main-instance-page data))
(assoc :current-frame-id (:main-instance-frame data))
(add-artboard attrs))))
(defn finish-component
[file]
(let [component-id (:current-component-id file)
@@ -566,9 +600,11 @@
file
(cond
;; Components-v2 component we skip this step
;; In components-v2 components haven't any shape inside them.
(and component-data (:main-instance-id component-data))
file
(update file :data
(fn [data]
(ctkl/update-component data component-id dissoc :objects)))
(empty? children)
(commit-change
@@ -618,43 +654,18 @@
(update :parent-stack pop))))
(defn finish-deleted-component
[component-id page-id main-instance-x main-instance-y file]
[component-id file]
(let [file (assoc file :current-component-id component-id)
page (ctpl/get-page (:data file) page-id)
component (ctkl/get-component (:data file) component-id)
main-instance-id (:main-instance-id component)
; To obtain a deleted component, we first create the component
; and the main instance in the workspace, and then delete them.
[_ shapes]
(ctn/make-component-instance page
component
(:data file)
(gpt/point main-instance-x
main-instance-y)
true
{:main-instance true
:force-id main-instance-id})]
(as-> file $
(reduce #(commit-change %1
{:type :add-obj
:id (:id %2)
:page-id (:id page)
:parent-id (:parent-id %2)
:frame-id (:frame-id %2)
:ignore-touched true
:obj %2})
$
shapes)
(commit-change $ {:type :del-component
component (ctkl/get-component (:data file) component-id)]
(-> file
(close-artboard)
(commit-change {:type :del-component
:id component-id})
(reduce #(commit-change %1 {:type :del-obj
:page-id page-id
:ignore-touched true
:id (:id %2)})
$
shapes)
(dissoc $ :current-component-id))))
(commit-change {:type :del-obj
:page-id (:main-instance-page component)
:id (:main-instance-id component)
:ignore-touched true})
(dissoc :current-page-id))))
(defn create-component-instance
[file data]
@@ -665,7 +676,6 @@
page-id (:current-page-id file)
page (ctpl/get-page (:data file) page-id)
component (ctkl/get-component (:data file) component-id)
;; main-instance-id (:main-instance-id component)
components-v2 (dm/get-in file [:options :components-v2])

View File

@@ -154,12 +154,12 @@
[:add-color
[:map {:title "AddColorChange"}
[:type [:= :add-color]]
[:color :any]]]
[:color ::ctc/color]]]
[:mod-color
[:map {:title "ModColorChange"}
[:type [:= :mod-color]]
[:color :any]]]
[:color ::ctc/color]]]
[:del-color
[:map {:title "DelColorChange"}
@@ -578,7 +578,7 @@
(ex/raise :type :conflict
:hint "id+name or page should be provided, never both"))
(let [page (if (and (string? name) (uuid? id))
(ctp/make-empty-page id name)
(ctp/make-empty-page {:id id :name name})
page)]
(ctpl/add-page data page)))
@@ -670,52 +670,14 @@
(ctyl/delete-typography data id))
;; === Operations
(defmethod process-operation :set
[on-changed shape op]
(let [attr (:attr op)
group (get ctk/sync-attrs attr)
val (:val op)
shape-val (get shape attr)
ignore (or (:ignore-touched op) (= attr :position-data)) ;; position-data is a derived attribute and
ignore-geometry (:ignore-geometry op) ;; never triggers touched by itself
is-geometry? (and (or (= group :geometry-group)
(and (= group :content-group) (= (:type shape) :path)))
(not (#{:width :height} attr))) ;; :content in paths are also considered geometric
;; TODO: the check of :width and :height probably may be removed
;; after the check added in data/workspace/modifiers/check-delta
;; function. Better check it and test toroughly when activating
;; components-v2 mode.
in-copy? (ctk/in-component-copy? shape)
;; For geometric attributes, there are cases in that the value changes
;; slightly (e.g. when rounding to pixel, or when recalculating text
;; positions in different zoom levels). To take this into account, we
;; ignore geometric changes smaller than 1 pixel.
equal? (if is-geometry?
(gsh/close-attrs? attr val shape-val 1)
(gsh/close-attrs? attr val shape-val))]
;; Notify when value has changed, except when it has not moved relative to the
;; component head.
(when (and group (not equal?) (not (and ignore-geometry is-geometry?)))
(on-changed shape))
(cond-> shape
;; Depending on the origin of the attribute change, we need or not to
;; set the "touched" flag for the group the attribute belongs to.
;; In some cases we need to ignore touched only if the attribute is
;; geometric (position, width or transformation).
(and in-copy? group (not ignore) (not equal?)
(not (and ignore-geometry is-geometry?)))
(-> (update :touched cfh/set-touched-group group)
(dissoc :remote-synced))
(nil? val)
(dissoc attr)
(some? val)
(assoc attr val))))
(ctn/set-shape-attr shape
(:attr op)
(:val op)
:on-changed on-changed
:ignore-touched (:ignore-touched op)
:ignore-geometry (:ignore-geometry op)))
(defmethod process-operation :set-touched
[_ shape op]

View File

@@ -69,6 +69,11 @@
::page page
::page-id (:id page)))
(defn with-page-id
[changes page-id]
(vary-meta changes assoc
::page-id page-id))
(defn with-container
[changes container]
(if (cfh/page? container)
@@ -715,6 +720,7 @@
(map lookupf)
(map mk-change))
updated-shapes))))
(apply-changes-local)))))
(defn update-component
@@ -764,15 +770,6 @@
(update :undo-changes conj {:type :del-component
:id id
:main-instance main-instance})))
(defn ignore-remote
[changes]
(letfn [(add-ignore-remote
[change-list]
(->> change-list
(mapv #(assoc % :ignore-remote? true))))]
(-> changes
(update :redo-changes add-ignore-remote)
(update :undo-changes add-ignore-remote))))
(defn reorder-grid-children
[changes ids]

View File

@@ -6,4 +6,4 @@
(ns app.common.files.defaults)
(def version 48)
(def version 51)

View File

@@ -357,15 +357,6 @@
;; COMPONENTS HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-touched-group
[touched group]
(when group
(conj (or touched #{}) group)))
(defn touched-group?
[shape group]
((or (:touched shape) #{}) group))
(defn make-container
[page-or-component type]
(assoc page-or-component :type type))

View File

@@ -1,103 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.files.libraries-helpers
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.uuid :as uuid]))
(defn generate-add-component-changes
[changes root objects file-id page-id components-v2]
(let [name (:name root)
[path name] (cfh/parse-path-name name)
[root-shape new-shapes updated-shapes]
(if-not components-v2
(ctn/make-component-shape root objects file-id components-v2)
(ctn/convert-shape-in-component root objects file-id))
changes (-> changes
(pcb/add-component (:id root-shape)
path
name
new-shapes
updated-shapes
(:id root)
page-id))]
[root-shape changes]))
(defn generate-add-component
"If there is exactly one id, and it's a frame (or a group in v1), and not already a component,
use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a
component with it, and link all shapes to their corresponding one in the component."
[it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board]
(let [changes (pcb/empty-changes it page-id)
shapes-count (count shapes)
first-shape (first shapes)
from-singe-frame?
(and (= 1 shapes-count)
(cfh/frame-shape? first-shape))
[root changes old-root-ids]
(if (and (= shapes-count 1)
(or (and (cfh/group-shape? first-shape)
(not components-v2))
(cfh/frame-shape? first-shape))
(not (ctk/instance-head? first-shape)))
[first-shape
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
(:shapes first-shape)]
(let [root-name (if (= 1 shapes-count)
(:name first-shape)
"Component 1")
shape-ids (into (d/ordered-set) (map :id) shapes)
[root changes]
(if-not components-v2
(prepare-create-group it ; These functions needs to be passed as argument
objects ; to avoid a circular dependence
page-id
shapes
root-name
(not (ctk/instance-head? first-shape)))
(prepare-create-board changes
(uuid/next)
(:parent-id first-shape)
objects
shape-ids
nil
root-name
true))]
[root changes shape-ids]))
changes
(cond-> changes
(not from-singe-frame?)
(pcb/update-shapes
(:shapes root)
(fn [shape]
(assoc shape :constraints-h :scale :constraints-v :scale))))
objects' (assoc objects (:id root) root)
[root-shape changes] (generate-add-component-changes changes root objects' file-id page-id components-v2)
changes (pcb/update-shapes changes
old-root-ids
#(dissoc % :component-root)
[:component-root])]
[root (:id root-shape) changes]))

View File

@@ -22,6 +22,7 @@
[app.common.schema :as sm]
[app.common.svg :as csvg]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
@@ -937,6 +938,85 @@
(-> data
(update :pages-index update-vals update-page))))
(defn migrate-up-49
"Remove hide-in-viewer for shapes that are origin or destination of an interaction"
[data]
(letfn [(update-object [destinations object]
(cond-> object
(or (:interactions object)
(contains? destinations (:id object)))
(dissoc object :hide-in-viewer)))
(update-page [page]
(let [destinations (->> page
:objects
(vals)
(mapcat :interactions)
(map :destination)
(set))]
(update page :objects update-vals (partial update-object destinations))))]
(update data :pages-index update-vals update-page)))
(defn migrate-up-50
"This migration mainly fixes paths with curve-to segments
without :c1x :c1y :c2x :c2y properties. Additionally, we found a
case where the params instead to be plain hash-map, is a points
instance. This migration normalizes all params to plain map."
[data]
(let [update-segment
(fn [{:keys [command params] :as segment}]
(let [params (into {} params)
params (cond
(= :curve-to command)
(let [x (get params :x)
y (get params :y)]
(cond-> params
(nil? (:c1x params))
(assoc :c1x x)
(nil? (:c1y params))
(assoc :c1y y)
(nil? (:c2x params))
(assoc :c2x x)
(nil? (:c2y params))
(assoc :c2y y)))
:else
params)]
(assoc segment :params params)))
update-shape
(fn [shape]
(if (cfh/path-shape? shape)
(d/update-when shape :content (fn [content] (mapv update-segment content)))
shape))
update-container
(fn [page]
(d/update-when page :objects update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(def ^:private valid-color?
(sm/lazy-validator ::ctc/color))
(defn migrate-up-51
"This migration fixes library invalid colors"
[data]
(let [update-colors
(fn [colors]
(into {} (filter #(-> % val valid-color?) colors)))]
(update data :colors update-colors)))
(def migrations
"A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2}
@@ -976,4 +1056,7 @@
{:id 45 :migrate-up migrate-up-45}
{:id 46 :migrate-up migrate-up-46}
{:id 47 :migrate-up migrate-up-47}
{:id 48 :migrate-up migrate-up-48}])
{:id 48 :migrate-up migrate-up-48}
{:id 49 :migrate-up migrate-up-49}
{:id 50 :migrate-up migrate-up-50}
{:id 51 :migrate-up migrate-up-51}])

View File

@@ -473,6 +473,59 @@
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :duplicate-slot
[_ {:keys [shape page-id] :as error} file-data _]
(let [page (ctpl/get-page file-data page-id)
childs (map #(get (:objects page) %) (:shapes shape))
child-with-duplicate (let [result (reduce (fn [[seen duplicates] item]
(let [swap-slot (ctk/get-swap-slot item)]
(if (contains? seen swap-slot)
[seen (conj duplicates item)]
[(conj seen swap-slot) duplicates])))
[#{} []]
childs)]
(second result))
repair-shape
(fn [shape]
;; Remove the swap slot
(log/debug :hint " -> remove swap-slot" :child-id (:id shape))
(ctk/remove-swap-slot shape))]
(log/dbg :hint "repairing shape :duplicated-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes (map :id child-with-duplicate) repair-shape))))
(defmethod repair-error :component-duplicate-slot
[_ {:keys [shape] :as error} file-data _]
(let [main-shape (get-in shape [:objects (:main-instance-id shape)])
childs (map #(get (:objects shape) %) (:shapes main-shape))
childs-with-duplicate (let [result (reduce (fn [[seen duplicates] item]
(let [swap-slot (ctk/get-swap-slot item)]
(if (contains? seen swap-slot)
[seen (conj duplicates item)]
[(conj seen swap-slot) duplicates])))
[#{} []]
childs)]
(second result))
duplicated-ids (set (mapv :id childs-with-duplicate))
repair-component
(fn [component]
(let [objects (reduce-kv (fn [acc k v]
(if (contains? duplicated-ids k)
(assoc acc k (ctk/remove-swap-slot v))
(assoc acc k v)))
{}
(:objects component))]
(assoc component :objects objects)))]
(log/dbg :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape))
(-> (pcb/empty-changes nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id shape) repair-component))))
(defmethod repair-error :missing-slot
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
@@ -481,7 +534,7 @@
(let [slot (:swap-slot args)]
(when (some? slot)
(log/debug :hint (str " -> set swap-slot to " slot))
(update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group slot)))))]
(ctk/set-swap-slot shape slot))))]
(log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)

View File

@@ -31,9 +31,11 @@
:child-not-found
:frame-not-found
:invalid-frame
:component-duplicate-slot
:component-not-main
:component-main-external
:component-not-found
:duplicate-slot
:invalid-main-instance-id
:invalid-main-instance-page
:invalid-main-instance
@@ -64,7 +66,7 @@
[:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken
[:shape-id {:optional true} ::sm/uuid]
[:file-id ::sm/uuid]
[:page-id ::sm/uuid]]))
[:page-id {:optional true} [:maybe ::sm/uuid]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ERROR HANDLING
@@ -296,6 +298,22 @@
"This shape should not have swap slot"
shape file page)))
(defn- has-duplicate-swap-slot?
[shape container]
(let [shapes (map #(get (:objects container) %) (:shapes shape))
slots (->> (map #(ctk/get-swap-slot %) shapes)
(remove nil?))
counts (frequencies slots)]
(some (fn [[_ count]] (> count 1)) counts)))
(defn- check-duplicate-swap-slot
"Validate that the children of this shape does not have duplicated slots."
[shape file page]
(when (has-duplicate-swap-slot? shape page)
(report-error :duplicate-slot
"This shape has children with the same swap slot"
shape file page)))
(defn- check-shape-main-root-top
"Root shape of a top main instance:
@@ -308,6 +326,7 @@
(check-component-root shape file page)
(check-component-not-ref shape file page)
(check-empty-swap-slot shape file page)
(check-duplicate-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :main-top) (:shapes shape)))
(defn- check-shape-main-root-nested
@@ -335,6 +354,7 @@
(check-component-root shape file page)
(check-component-ref shape file page libraries)
(check-empty-swap-slot shape file page)
(check-duplicate-swap-slot shape file page)
(run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape))))
(defn- check-shape-copy-root-nested
@@ -453,13 +473,24 @@
shape file page)
(check-shape-not-component shape file page libraries))))))))
(defn check-component-duplicate-swap-slot
[component file]
(let [shape (get-in component [:objects (:main-instance-id component)])]
(when (has-duplicate-swap-slot? shape component)
(report-error :component-duplicate-slot
"This deleted component has children with the same swap slot"
component file nil))))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."
[component file]
(when (and (contains? component :objects) (nil? (:objects component)))
(report-error :component-nil-objects-not-allowed
"Objects list cannot be nil"
component file nil)))
component file nil))
(when (:deleted component)
(check-component-duplicate-swap-slot component file)))
(defn- get-orphan-shapes
[{:keys [objects] :as page}]

View File

@@ -269,6 +269,13 @@
(keep (mk-check-auto-layout objects))
shapes)))
(defn full-tree?
"Checks if we need to calculate the full tree or we can calculate just a partial tree. Partial
trees are more efficient but cannot be done when the layout is centered."
[objects layout-id]
(let [layout-justify-content (get-in objects [layout-id :layout-justify-content])]
(contains? #{:center :end :space-around :space-evenly :stretch} layout-justify-content)))
(defn sizing-auto-modifiers
"Recalculates the layouts to adjust the sizing: auto new sizes"
[modif-tree sizing-auto-layouts objects bounds ignore-constraints]
@@ -286,7 +293,7 @@
(d/seek sizing-auto-layouts))
shapes
(if from-layout
(if (and from-layout (not (full-tree? objects from-layout)))
(cgst/resolve-subtree from-layout layout-id objects)
(cgst/resolve-tree #{layout-id} objects))

View File

@@ -0,0 +1,106 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.json
(:refer-clojure :exclude [read clj->js js->clj])
(:require
#?(:clj [clojure.data.json :as j])
[cuerdas.core :as str]))
#?(:clj
(defn read
[reader & {:as opts}]
(j/read reader opts)))
#?(:clj
(defn write
[writer data & {:as opts}]
(j/write data writer opts)))
(defn read-kebab-key
[k]
(if (and (string? k) (not (str/includes? k "/")))
(-> k str/kebab keyword)
k))
(defn write-camel-key
[k]
(if (or (keyword? k) (symbol? k))
(str/camel k)
(str k)))
#?(:cljs
(defn ->js
[x & {:keys [key-fn]
:or {key-fn write-camel-key} :as opts}]
(let [f (fn this-fn [x]
(cond
(nil? x)
nil
(satisfies? cljs.core/IEncodeJS x)
(cljs.core/-clj->js x)
(or (keyword? x)
(symbol? x))
(name x)
(number? x)
x
(boolean? x)
x
(map? x)
(reduce-kv (fn [m k v]
(let [k (key-fn k)]
(unchecked-set m k (this-fn v))
m))
#js {}
x)
(coll? x)
(reduce (fn [arr v]
(.push arr (this-fn v))
arr)
(array)
x)
:else
(str x)))]
(f x))))
#?(:cljs
(defn ->clj
[o & {:keys [key-fn val-fn] :or {key-fn read-kebab-key val-fn identity}}]
(let [f (fn this-fn [x]
(let [x (val-fn x)]
(cond
(array? x)
(persistent!
(.reduce ^js/Array x
#(conj! %1 (this-fn %2))
(transient [])))
(identical? (type x) js/Object)
(persistent!
(.reduce ^js/Array (js-keys x)
#(assoc! %1 (key-fn %2) (this-fn (unchecked-get x %2)))
(transient {})))
:else
x)))]
(f o))))
(defn encode
[data & {:as opts}]
#?(:clj (j/write-str data opts)
:cljs (.stringify js/JSON (->js data opts))))
(defn decode
[data & {:as opts}]
#?(:clj (j/read-str data opts)
:cljs (->clj (.parse js/JSON data) opts)))

View File

@@ -153,14 +153,29 @@
(defn build-message
[props]
(loop [props (seq props)
result []]
result []
body nil]
(if-let [[k v] (first props)]
(if (simple-ident? k)
(cond
(simple-ident? k)
(recur (next props)
(conj result (str (name k) "=" (pr-str v))))
(conj result (str (name k) "=" (pr-str v)))
body)
(= ::body k)
(recur (next props)
result))
(str/join ", " result))))
result
v)
:else
(recur (next props)
result
body))
(let [message (str/join ", " result)]
(if (string? body)
(str message "\n" body)
message)))))
(defn build-stack-trace
[cause]

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