Compare commits

...

1262 Commits

Author SHA1 Message Date
Andrés Moya
582a6d0c03 wip: Add rxjs-spy library to monitor RX streams 2023-09-21 10:32:08 +02:00
Andrés Moya
878f1d4090 Enhance validation script 2023-09-21 10:16:30 +02:00
Andrey Antukh
003dec6c6b 💄 Add cosmetic changes to several viewer related react components 2023-09-21 09:48:51 +02:00
Andrey Antukh
df2d242746 🐛 Fix unexpected exception on viewer caused by nil objects
This issue is started to happening because of an unrelated change
on frame-shape react component where shapes are looked up directly
on objects having in supposition that objects will be exists but on
viewer there are two objects: fixed and not-fixed, and in some cases
objects map can be empty or don't contain the object.

For solve the issue, we just filter not existing objects before
progragate the children down to the inner react components, avoiding
the exception when an object appears as `nil`.
2023-09-21 09:48:51 +02:00
Eva
9e07999537 🔥 Remove all css.json files 2023-09-21 09:31:42 +02:00
Andrey Antukh
8caeaefa98 Adapt frontend build process to the scss modules 2023-09-21 09:26:46 +02:00
Andrés Moya
836b4538dd Add validate & repair functions 2023-09-20 15:40:43 +02:00
Andrés Moya
973affb259 🐛 Fix touched fixer 2023-09-20 15:40:43 +02:00
alonso.torres
f004aa5efd 🐛 Fix problems with boards 2023-09-20 14:21:49 +02:00
alonso.torres
e5b05eff23 🐛 Fix problem when creating groups inside grid 2023-09-20 14:21:49 +02:00
alonso.torres
9d6bd64027 🐛 Fix problem with changes builder 2023-09-20 09:54:46 +02:00
Andrey Antukh
c23cf2a5a6 🐛 Fix issue with minio setup on devenv 2023-09-19 11:40:12 +02:00
Andrey Antukh
9931232a91 Merge pull request #3636 from penpot/palba-show-info-empty-library-on-dialogs
🎉 Warn about empty libraries on the share library dialog
2023-09-19 09:54:21 +02:00
Pablo Alba
d615fbb282 🎉 Warn about empty libraries on the share library dialog 2023-09-19 09:52:40 +02:00
Pablo Alba
dfb7df1eb9 🎉 Allow to make reset override in bulk 2023-09-18 17:01:28 +02:00
Aitor
0494dc843f ♻️ Refactor thumbnails 2023-09-18 17:00:13 +02:00
Aitor
0721fc9d80 Add lazy loading and async decoding to graphics 2023-09-18 17:00:13 +02:00
Aitor
9ce8c2d580 ♻️ Change pixel overlay inner workings 2023-09-18 17:00:13 +02:00
Aitor
537435372a ♻️ Change pixel overlay rendering to use rasterizer 2023-09-18 17:00:13 +02:00
Aitor
0496b1f4e3 ♻️ Change how thumbnails are rendered 2023-09-18 17:00:13 +02:00
Aitor
51a8e8799b ♻️ Change thumbnail-renderer to rasterizer 2023-09-18 17:00:13 +02:00
Aitor Moreno
e2812391c4 Merge pull request #3635 from penpot/alotor-grid-polishing
Grid polishing
2023-09-18 14:16:50 +02:00
alonso.torres
52cbc7e09d Margins for grid elements 2023-09-18 14:08:51 +02:00
alonso.torres
6f2a459cce Instance component to grid layout 2023-09-18 14:08:34 +02:00
alonso.torres
ea4a3d9e27 🐛 Fix problem with duplicate shapes 2023-09-18 14:08:20 +02:00
alonso.torres
17f35cda15 Multiple cells selection and area 2023-09-18 14:07:53 +02:00
alonso.torres
322767701c Highlight on track hover 2023-09-18 14:07:37 +02:00
alonso.torres
495ba6e4a4 Reorder grid tracks 2023-09-18 14:04:16 +02:00
alonso.torres
de4ef1b19d Merge remote-tracking branch 'origin/staging' into develop 2023-09-18 13:48:41 +02:00
Alejandro
859146ddc2 Merge pull request #3641 from penpot/alotor-hotfix
🐛 Fix problem with z-index field in non-absolute items
2023-09-18 13:24:16 +02:00
alonso.torres
4b5e9997e9 🐛 Fix problem with z-index field in non-absolute items 2023-09-18 13:22:47 +02:00
Alejandro
ae10132a07 Merge pull request #3637 from penpot/niwinz-develop-poc-svgo
🎉 Add svg optimization for export and import
2023-09-18 07:00:24 +02:00
Andrey Antukh
630a347184 Add support for svg optimizations on exporter output
Under `enable-exporter-svgo` flag, disabled by default.
2023-09-15 15:00:58 +02:00
Andrey Antukh
7fe446e9de Add support for svg optimizations on workspace svg import
Under `enable-frontend-svgo` flag, disabled by default.
2023-09-15 15:00:58 +02:00
Andrey Antukh
a2e26b8beb Add bundled svgo library and expose it on common module
The svgo bundle is included directly as esm module, no npm dependency
here because the module is bundled from a custom fork located on penpot
github organization:

   https://github.com/penpot/svgo
2023-09-15 15:00:58 +02:00
Alejandro Alonso
175072f546 Merge remote-tracking branch 'origin/staging' into develop 2023-09-15 12:23:27 +02:00
Andrey Antukh
3f3e3e8a81 Revert " Add bundled svgo library and expose it on common module"
This reverts commit 3877eccc29.
2023-09-15 12:19:34 +02:00
Andrey Antukh
11df5ec15e Revert " Add support for svg optimizations on workspace svg import"
This reverts commit b92fcca17c.
2023-09-15 12:19:26 +02:00
Andrey Antukh
9d090ad3d9 Revert " Add support for svg optimizations on exporter output"
This reverts commit 9fc771292a.
2023-09-15 12:19:17 +02:00
Alejandro
aa62b9d248 Merge pull request #3628 from penpot/niwinz-develop-bugfixes-4
 Don't render not visible shapes on workspace
2023-09-15 11:11:05 +02:00
Alejandro Alonso
826b96ad6c Merge remote-tracking branch 'origin/staging' into develop 2023-09-15 10:51:05 +02:00
Alejandro
8bd92aad82 Merge pull request #3634 from penpot/niwinz-staging-svgo
🎉 Add svg optimization support on import and export
2023-09-15 09:07:13 +02:00
Alejandro
f54df5ba80 Merge pull request #3633 from penpot/niwinz-develop-bugfixes-5
🐛 Minor bugfixes and logging improvements
2023-09-15 08:38:17 +02:00
Alejandro
084e114f75 Merge pull request #3624 from penpot/niwinz-develop-experiments-6
♻️ Refacctor shape attrs extraction helpers
2023-09-15 08:37:38 +02:00
Andrey Antukh
9fc771292a Add support for svg optimizations on exporter output
Under `enable-exporter-svgo` flag, disabled by default.
2023-09-14 19:08:39 +02:00
Andrey Antukh
b92fcca17c Add support for svg optimizations on workspace svg import
Under `enable-frontend-svgo` flag, disabled by default.
2023-09-14 19:08:39 +02:00
Andrey Antukh
3877eccc29 Add bundled svgo library and expose it on common module
The svgo bundle is included directly as esm module, no npm dependency
here because the module is bundled from a custom fork located on penpot
github organization:

   https://github.com/penpot/svgo
2023-09-14 19:08:39 +02:00
Andrey Antukh
ef4bd8c598 🐛 Fix incorrect interaction of library-absorb mechanism and storage-pointes 2023-09-14 17:45:56 +02:00
Andrey Antukh
a3f3e31c73 Add minor logging improvement on binfile 2023-09-14 17:45:26 +02:00
Andrey Antukh
b53f7eaa19 Add file version on binfile import logging 2023-09-14 17:44:01 +02:00
Andrey Antukh
1b889cb141 📎 Add proper logging level for file migrations info 2023-09-14 17:43:19 +02:00
Andrey Antukh
9c8103ce44 📎 Change to info the default logger level of tmp storage on devenv 2023-09-14 17:42:27 +02:00
Alejandro Alonso
3a8123314e Merge remote-tracking branch 'origin/staging' into develop 2023-09-14 11:53:00 +02:00
Eva Marco
59eb11ac3f Merge pull request #3626 from penpot/juan-review-design-tab
💄 Tweaks and review design tab
2023-09-14 10:50:39 +02:00
elhombretecla
28010b786d 💄 Adds new UI elements files and visual changes 2023-09-14 10:45:31 +02:00
Andrey Antukh
813c9de636 Merge pull request #3630 from penpot/superalex-fix-authentication-required-on-dashboard
🐛 Fix authentication required on dashboard
2023-09-14 10:20:00 +02:00
Pablo Alba
c291b632a1 🐛 Fix uppercase translations MAIN and COPY 2023-09-14 09:35:29 +02:00
Alejandro Alonso
33c82e2abe 🐛 Fix authentication required on dashboard 2023-09-14 07:13:37 +02:00
Alejandro
a4754a2106 Merge pull request #3599 from penpot/niwinz-develop-experiments-3
🐛 Replace `:use-for-thumbnail?` with `:use-for-thumbnail`
2023-09-14 06:39:06 +02:00
Andrey Antukh
956da67f84 💄 Add mostly cosmetic improvements to text-svg-position ns 2023-09-13 16:41:45 +02:00
Andrey Antukh
56aa751425 🐛 Fix incorrect react vdom on font-selector component 2023-09-13 16:36:49 +02:00
Andrey Antukh
954e5303f0 🐛 Fix incorrect props passed on workspace shape wrapper 2023-09-13 16:36:49 +02:00
Andrey Antukh
ac4343dafd Don't render not visible shapes on workspace 2023-09-13 16:36:49 +02:00
Alejandro
c667d3ad46 Merge pull request #3627 from penpot/niwinz-develop-bugfixes-4
Revert " Don't render not visible shapes on workspace"
2023-09-13 14:00:53 +02:00
Pablo Alba
0699cce389 Merge pull request #3623 from penpot/hiru-fix-touched
🔧 Add script to fix touched attributes
2023-09-13 14:00:41 +02:00
Andrey Antukh
db5621f4ae Revert " Don't render not visible shapes on workspace"
This reverts commit a01c64ea57.
2023-09-13 13:54:40 +02:00
Andrés Moya
afa14dd847 💄 Replace prn with println 2023-09-13 13:54:26 +02:00
Andrés Moya
507cb9f3de 🔧 Add script to fix touched attributes 2023-09-13 13:54:26 +02:00
Alejandro
ebf60f9279 Merge pull request #3625 from penpot/superalex-fix-selection-hover
🐛 Fix selection hover
2023-09-13 12:53:55 +02:00
Alejandro Alonso
f7e5cb4bb2 🐛 Fix selection hover 2023-09-13 12:38:11 +02:00
Andrey Antukh
307cfa287f 🔥 Remove inneficient obj/without helper 2023-09-13 10:53:24 +02:00
Andrey Antukh
393863b29f 🐛 Fix broken hooks rule on shapes fills component 2023-09-13 10:53:24 +02:00
Andrey Antukh
385fd9c4e6 ♻️ Refactor shape attrs extraction helpers 2023-09-13 10:53:24 +02:00
Andrey Antukh
e6f8022de0 Add obj/array? helper 2023-09-13 10:52:32 +02:00
Andrey Antukh
b1e54a9714 Pass explicitly the render-id on props handling in path and svg-raw shapes 2023-09-13 10:52:32 +02:00
Andrey Antukh
85a1f7d69e Add minor optimizations to fills component (shapes) 2023-09-13 10:52:32 +02:00
Andrey Antukh
281251ff87 Add minor optimizations to rect shape 2023-09-13 10:52:32 +02:00
Andrey Antukh
ad58c97cbd Merge pull request #3605 from penpot/palba-fix-export-detach
🐛 Fix export file with components as basic objects
2023-09-13 10:48:51 +02:00
Pablo Alba
88390432f5 🐛 Fix export file with components as basic objects 2023-09-13 09:50:27 +02:00
Alejandro
026510c204 Merge pull request #3608 from penpot/niwinz-develop-experiments-5
 Add performance oriented refactor of custom-stroke related components
2023-09-13 07:00:26 +02:00
Pablo Alba
b4b5aaafe4 🐛 Fix preview of moving a copy of a flex component into its main 2023-09-12 17:05:50 +02:00
Pablo Alba
fe36a9e958 Assets groups review 2023-09-12 16:19:09 +02:00
Andrey Antukh
b03492e187 Merge pull request #3610 from penpot/palba-add-main-copy-label-to-component
🎉 Add main/copy label on component in right bar
2023-09-12 16:15:50 +02:00
Alejandro
732805bf0e Merge pull request #3622 from penpot/azazeln28-fix-blend-mode-select-click
🐛 Fix blend mode select click
2023-09-12 15:50:15 +02:00
Andrey Antukh
1ffca618f9 🐛 Fix react warning on incorrect hooks usage on shapes components 2023-09-12 15:21:46 +02:00
Aitor
72f20301c4 🐛 Fix blend mode select click 2023-09-12 14:29:32 +02:00
Andrey Antukh
34ddc00c8e Merge pull request #3620 from penpot/alotor-fix-over-shapes
🐛 Improved response time of over shapes
2023-09-12 11:59:51 +02:00
Alejandro Alonso
fbff2f103e Select through stroke only rectangle 2023-09-12 11:59:41 +02:00
alonso.torres
fff98b995f 🐛 Improved response time of over shapes 2023-09-12 11:43:22 +02:00
Andrey Antukh
bf2a546f77 ♻️ Refactor custom-stroke render impl 2023-09-12 11:40:41 +02:00
Andrey Antukh
1b420e55f4 Add more DOM attrs friendly render-id generation hook 2023-09-12 11:40:41 +02:00
Andrey Antukh
645b7e4b8d 🐛 Fix react warning on incorrect hooks usage on shapes components 2023-09-12 11:40:41 +02:00
Andrés Moya
b943a034c9 🐛 Fix CI 2023-09-12 11:15:51 +02:00
Andrés Moya
51ab11e91e 🐛 Use helper to normalice behavior of component display in dump_tree 2023-09-12 09:53:41 +02:00
Pablo Alba
3228d0a95f Merge pull request #3613 from penpot/hiru-fix-parent-touched
🐛 Fix parent touched detecion when duplicating or copy&paste
2023-09-11 13:50:38 +02:00
Andrés Moya
2f3ae1d520 🐛 Fix parent touched detecion when duplicating or copy&paste 2023-09-11 13:25:07 +02:00
Pablo Alba
79ecdebfee 🎉 Add main/copy label on component in right bar 2023-09-08 12:16:00 +02:00
Alejandro Alonso
bc45b15b79 :bugfix: Fix multiple selection of shapes 2023-09-08 11:04:58 +02:00
Andrey Antukh
5fec6c807b Merge pull request #3571 from penpot/eva-design-tab
💄 Redesign design tab phase one
2023-09-07 14:13:57 +02:00
Eva
9ed06c4483 💄 Redesign design tab phase one 2023-09-07 13:59:06 +02:00
Alejandro
d7dea040af Merge pull request #3601 from penpot/niwinz-develop-experiments-4
  ♻️
2023-09-07 11:38:59 +02:00
Alejandro Alonso
1ba76cb3f8 Merge remote-tracking branch 'origin/staging' into develop 2023-09-07 11:32:03 +02:00
Andrey Antukh
3fea366a04 Merge pull request #3604 from penpot/superalex-fix-log-out-log-in-with-different-acounts-page-not-exist
🐛 Fix logout and login with different accounts show 404 error page
2023-09-07 11:22:17 +02:00
Alejandro Alonso
98b1ac7b60 🐛 Fix logout and login with different accounts show 404 error page 2023-09-07 11:17:00 +02:00
Andrey Antukh
308b6279c2 Merge pull request #3597 from penpot/superalex-improve-selected-colors
 Improve selected colors
2023-09-07 11:15:13 +02:00
Alejandro Alonso
d29aa00155 Improve selected colors 2023-09-07 11:11:30 +02:00
Andrey Antukh
5940e00053 Add minor optimizations to shapes/gradient related components 2023-09-06 16:28:32 +02:00
Andrey Antukh
140cb43681 🔥 Remove duplicated line on gradients/add-metadata helper 2023-09-06 16:28:32 +02:00
Andrey Antukh
efd4a1ffba Fix inconsistencies on shapes/gradient component 2023-09-06 16:28:32 +02:00
Andrey Antukh
cef74377df Add minor optimizations to workspace shapes/group ns 2023-09-06 16:28:32 +02:00
Andrey Antukh
469de48af2 💄 Add cosmetic improvements to workspace shapes/bool ns 2023-09-06 16:28:32 +02:00
Andrey Antukh
c7ae8b6510 Add minor optimizations on workspace/shapes ns 2023-09-06 16:28:32 +02:00
Andrey Antukh
d3c9bf1e76 Move common code on shape props checking to shapes/common ns 2023-09-06 16:28:32 +02:00
Andrey Antukh
d9c496b131 Add minor optimizations to shapes/mask component 2023-09-06 15:38:43 +02:00
Andrey Antukh
7f9e01711f Add minor optimizations to shapes/mask internal helpers 2023-09-06 15:38:43 +02:00
Andrey Antukh
e8808bc8a4 📎 Add improved kondo hook analyzer for rumext/fnc 2023-09-06 15:38:43 +02:00
Andrey Antukh
4dc41724de Add minor optimizations to shapes/group component 2023-09-06 15:38:43 +02:00
Andrey Antukh
c8b42478b0 Add minor optimizations to shapes/circle component 2023-09-06 15:38:43 +02:00
Andrey Antukh
9993d357da Add minor optimizations to shapes/bool component 2023-09-06 15:38:43 +02:00
Andrey Antukh
c3c2d88245 💄 Fix indentation on shapes/bool component 2023-09-06 14:42:31 +02:00
Andrey Antukh
48e5e86b73 ♻️ Remove redundant components rendering for workspace/frame 2023-09-06 14:42:31 +02:00
Andrey Antukh
2e2ce6bcfe 💄 Add cosmetic improvements to some workspace frame related components 2023-09-06 14:42:31 +02:00
Andrey Antukh
ca8e9b871d Add micro optimizations to shapes/frame-thumbail-image component 2023-09-06 14:42:31 +02:00
Andrey Antukh
f311deda1b 💄 Add cosmetic improvements to shapes/frame-shape component 2023-09-06 14:42:31 +02:00
Andrey Antukh
d5d95a1328 🐛 Fix typo on srepl/analyze-files helper 2023-09-06 14:42:31 +02:00
Andrey Antukh
63e250d9d0 Add micro optimization on refs/children-objects 2023-09-06 14:42:31 +02:00
Andrey Antukh
4d2afd483b 🔥 Remove aparently redundant shape-container usage on workspace frame container 2023-09-06 14:42:31 +02:00
Andrey Antukh
e805f11f12 🔥 Remove unnecesary shape processing on root-shape 2023-09-06 14:42:31 +02:00
Andrey Antukh
d0a796124f Add micro optimization to shape-container component 2023-09-06 14:42:31 +02:00
Andrey Antukh
b158a82a84 💄 Fix indentation on page helpers 2023-09-06 14:42:31 +02:00
Frederik Ring
d06124e378 Allow passing overrides to frontend nginx config 2023-09-06 09:48:06 +02:00
Andrey Antukh
74be76c914 Merge pull request #3600 from penpot/palba-fix-fixes
🐛 Upgrade the fixes functions to avoid corner cases
2023-09-05 16:38:10 +02:00
Pablo Alba
8cb917cf51 🐛 Upgrade the fixes functions to avoid corner cases 2023-09-05 16:16:22 +02:00
Andrey Antukh
2706d1ffd3 Merge pull request #3598 from penpot/palba-fix-duplicate-component
🐛 Fix duplicate component doesn't create a main shape
2023-09-05 12:20:34 +02:00
Pablo Alba
bd1a681e71 🐛 Fix duplicate component doesn't create a main shape 2023-09-05 12:19:57 +02:00
Andrey Antukh
36506ec360 🐛 Replace :use-for-thumbnail? with :use-for-thumbnail 2023-09-05 12:01:40 +02:00
Alejandro
a4ed9e57fb Merge pull request #3590 from penpot/niwinz-develop-experiments-2
🐛 & 
2023-09-05 11:12:55 +02:00
Andrey Antukh
0f133ca431 🐛 Fix more issues on frontend gulpfile 2023-09-05 10:50:54 +02:00
Andrey Antukh
c1117b6da9 🐛 Fix issue on frontend build process caused by deps update 2023-09-05 10:29:19 +02:00
Andrey Antukh
a01c64ea57 Don't render not visible shapes on workspace 2023-09-04 17:37:08 +02:00
Andrey Antukh
5b3e12bb9c ♻️ Refactor change builder for make it more efficient
Mainly replaces the usafe of the inneficient d/preconj helper
with a combination of conj and simple list as data structure whitch
maintains the previous ordering semantics on addition.

Also removes the d/preconj from the codebase.
2023-09-04 15:48:34 +02:00
Andrey Antukh
4e974cd2f3 🐛 Fix typo on has-point? impl 2023-09-04 15:33:04 +02:00
Alejandro
87f085da74 Merge pull request #3594 from penpot/niwinz-develop-experiments-1
🐛 Several bugfixes and other minor imprivements
2023-09-04 12:28:03 +02:00
Andrey Antukh
b68b802b6d 🐛 Fix shape radius type toggle on workspace 2023-09-04 12:04:15 +02:00
Andrey Antukh
c54deb0218 🐛 Fix proportion lock toggle callback
Add missing dependency
2023-09-04 12:04:15 +02:00
Andrey Antukh
bd734c1095 🐛 Fix log level setting on file migrations ns 2023-09-04 12:04:15 +02:00
Andrey Antukh
6a3b963a77 🐛 Add migration that fixes all frames that does not have :shapes attr 2023-09-04 12:04:15 +02:00
Andrey Antukh
a097ed29a9 Fix extensibility and naming of workspace shape fixer 2023-09-04 12:04:15 +02:00
Andrey Antukh
c7f9774524 Add more flexible call flow for db interacting methods 2023-09-04 12:04:15 +02:00
Andrey Antukh
90f7e97d5b Improve kondo analyze function for db/with-atomic
Allow pass options as third argument on params vector
2023-09-04 12:04:15 +02:00
Alejandro Alonso
07562af677 Merge remote-tracking branch 'origin/staging' into develop 2023-09-04 11:47:10 +02:00
Alejandro
1eaf7b2b44 Merge pull request #3593 from penpot/niwinz-staging-bugfixes-9
🐛 Bugfixes and logging improvements
2023-09-04 11:42:27 +02:00
Andrey Antukh
903f064e87 Decrease slightly argon2id cost for improve usability
The previous values are set too high. The current value are still
valid under current recomendation but improves a little bit the
time of password verification.
2023-09-04 11:35:31 +02:00
Andrey Antukh
a23d1908e9 Improve worker logging 2023-09-04 11:35:31 +02:00
Andrey Antukh
1e8226a3fc 🐛 Fix log level setting on file migrations ns 2023-09-04 11:35:31 +02:00
Andrey Antukh
b7459726f5 Merge pull request #3592 from penpot/superalex-remember-last-team-log-out-2
 Remember last team accross logouts and sessions and fix some weird stuff
2023-09-04 11:12:42 +02:00
Alejandro Alonso
b8179d0e35 Remember last team accross logouts and sessions and fix some auth weird stuff 2023-09-04 10:34:30 +02:00
Alejandro
53a9906736 Merge pull request #3589 from penpot/niwinz-develop-debug-import-fix
🐛 Fix clone operaton of dbg handler
2023-09-01 13:15:16 +02:00
Andrey Antukh
7aae12c732 🐛 Fix clone operaton of dbg handler 2023-09-01 13:07:49 +02:00
Alejandro
6080b778d4 Merge pull request #3570 from penpot/niwinz-develop-experiments-1
 Add performance enhancements (part 2)
2023-09-01 12:58:54 +02:00
Andrey Antukh
8a4fcc1d10 Delimit rendering of components when they are visible on workspace assets tab 2023-09-01 12:50:29 +02:00
Andrey Antukh
1e2603f1f5 Add minor improvements to use-visible hook 2023-09-01 12:50:29 +02:00
Andrey Antukh
937d3b4954 Don't perform assets filtering if term is empty 2023-09-01 12:50:29 +02:00
Andrey Antukh
8ff18a2a9e Add asset item full path to the search filtering 2023-09-01 12:50:29 +02:00
Andrey Antukh
e278d042ea Improve usability of assets tab on search
Automatically uncollapse assets groups when a total searched
results is less than a threshold of 60 (current default)
2023-09-01 12:50:29 +02:00
Andrey Antukh
9804bd88c2 Add improvements to css modules related macros 2023-09-01 12:50:29 +02:00
Andrey Antukh
62f15f9b9d Make components assets gropups collapsed by default on assets tab 2023-09-01 12:50:29 +02:00
Andrey Antukh
50a49e5fbf Show by default assets as not visible 2023-09-01 12:50:29 +02:00
Andrey Antukh
b649adf544 💄 Add cosmetic improvements to sidebar assets namespace 2023-09-01 12:50:29 +02:00
Andrey Antukh
c6e248b52f Add correct impl for is-direct-child-of-root? helper
And we restore the previously removed helper and incorrectly replaced by
the `is-direct-child-of-root?`.

In penpot exists two concepts: root and root-frame; root is the
artificially created shape that represents the ROOT, and root-frame
means a frame that is shape of frame type which is a direct children
of ROOT.
2023-09-01 12:47:18 +02:00
Andrey Antukh
1a1e55037b 🔥 Remove unused conditional on root-shape component 2023-09-01 12:47:18 +02:00
Andrey Antukh
82f1b96503 Add micro optimization to is-direct-child-of-root? helper 2023-09-01 12:47:18 +02:00
Andrey Antukh
58f788455f Add experimental equality with exceptions props checking to frames 2023-09-01 12:47:18 +02:00
Andrey Antukh
b28cad2250 Improve efficiency of equiv impl of jvm-custom-record 2023-09-01 12:47:18 +02:00
Andrey Antukh
7f91619075 Add improved text change detection on viewport text renderer 2023-09-01 12:47:18 +02:00
Andrey Antukh
f82c682421 Delimit attrs on update-shape-flags impl 2023-09-01 12:47:18 +02:00
Alejandro Alonso
69f2e7c43f Merge remote-tracking branch 'origin/staging' into develop 2023-09-01 12:40:17 +02:00
Andrey Antukh
2a6022fa18 🐛 Fix importation on debug endpoint 2023-09-01 12:01:11 +02:00
Andrey Antukh
e36b49b4f0 Merge pull request #3587 from penpot/superalex-layer-multiselection-behaviour
 Improve layers multiselection behaviour
2023-09-01 11:20:27 +02:00
Alejandro Alonso
92ff5de538 Improve layers multiselection behaviour 2023-09-01 11:20:10 +02:00
Alejandro Alonso
c83d028466 Colorpicker: remember las color mode 2023-09-01 11:18:45 +02:00
Alejandro
56a0d522dc Merge pull request #3585 from penpot/niwinz-staging-storage-gc-deleted
 Add minor improvements to logging
2023-09-01 06:40:41 +02:00
Andrey Antukh
a3495800b5 Add minor logging improvements to worker namespace 2023-08-31 21:09:18 +02:00
Andrey Antukh
750cf05784 Add minor logging related improvements to binfile namespace 2023-08-31 21:08:23 +02:00
Andrey Antukh
1384219ae7 📎 Update devenv logging file 2023-08-31 21:08:01 +02:00
Andrey Antukh
d2d9aeff25 📎 Reduce log level of worker submit operation
Start logging to as TRACE instead of DEBUG
2023-08-31 20:59:58 +02:00
Andrey Antukh
95d80c9578 Merge pull request #3582 from penpot/superalex-fix-invalid-comments-when-delete-page
🐛 Fix deleted pages comments shown in right sidebar
2023-08-31 20:02:00 +02:00
Alejandro
b523bef8ba Merge pull request #3581 from penpot/niwinz-staging-storage-gc-deleted
 Improve storage-gc-deleted task reliability
2023-08-31 15:18:15 +02:00
Alejandro Alonso
0c5c04e58a 🐛 Fix deleted pages comments shown in right sidebar 2023-08-31 15:16:55 +02:00
Andrey Antukh
a0973b9ddf Improve storage-gc-deleted task reliability 2023-08-31 14:36:31 +02:00
Andrey Antukh
f30732dc7f Merge pull request #3575 from penpot/palba-remove-innecesary-message
📎 Remove innecesary message on delete shared dialog
2023-08-31 14:15:50 +02:00
Pablo Alba
2f8cac83ae 📎 Remove innecesary message on delete shared dialog 2023-08-31 13:52:37 +02:00
Alejandro
c53b6117c0 Merge pull request #3574 from penpot/azazeln28-fix-text-shapes-rendered-with-bad-proportions
🐛 Fix text shapes rendered with bad proportions
2023-08-31 12:10:36 +02:00
Aitor
bd3ddebcc4 🐛 Fix text shapes rendered with bad proportions 2023-08-31 12:06:31 +02:00
Alejandro
0441f28880 Merge pull request #3577 from penpot/hiru-hide-messages-on-exit
🐛 Fix message popup remains open when exiting workspace
2023-08-31 11:45:41 +02:00
Andrés Moya
288030888a 🐛 Fix message popup remains open when exiting workspace 2023-08-31 11:39:46 +02:00
Alejandro
203c0ed87d Merge pull request #3579 from penpot/eva-refix-lock-title
🐛 Fix lock and hide tooltip
2023-08-31 11:38:51 +02:00
Eva
09e28076cd 🐛 Fix lock and hide tooltip 2023-08-31 11:31:58 +02:00
Alejandro
ad4e489312 Merge pull request #3578 from penpot/superalex-fix-list-view-is-discarded-on-tab-change-for-assets-sidebar-tab
🐛 Fix list view is discarded on tab change for assets sidebar
2023-08-31 11:31:13 +02:00
Alejandro Alonso
50932dea54 🐛 Fix list view is discarded on tab change for assets sidebar 2023-08-31 11:25:36 +02:00
Andrey Antukh
da3c829b1b 📎 Fix clj linter issues on backend 2023-08-31 11:24:30 +02:00
Andrey Antukh
d4b4e6be7d 🐛 Fix frontend cljs linter issues 2023-08-31 10:49:09 +02:00
Alejandro
722ad5216f Merge pull request #3576 from penpot/niwinz-develop-update-deps
⬆️ Update dependencies
2023-08-31 10:48:25 +02:00
Andrey Antukh
3a6007d385 📎 Fix clj linter issues on backend 2023-08-31 10:36:20 +02:00
Andrey Antukh
fb1bdd4ce7 🐛 Fix frontend cljs linter issues 2023-08-31 09:31:53 +02:00
Andrey Antukh
63668fb66e 📎 Fix scss linter issues 2023-08-31 09:25:40 +02:00
Andrey Antukh
eb2187daf2 ⬆️ Update dependencies 2023-08-31 09:20:22 +02:00
Andrey Antukh
2cc76a2609 Merge pull request #3573 from penpot/hiru-fix-group-creation
🐛 Correctly initialize geometry when creating a new group
2023-08-30 15:21:20 +02:00
Andrés Moya
2d0b14d483 🐛 Correctly initialize geometry when creating a new group 2023-08-30 13:47:55 +02:00
Andrey Antukh
1c769a13e2 Merge remote-tracking branch 'weblate/develop' into develop 2023-08-30 11:12:26 +02:00
Hosted Weblate
25a4a92f05 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2023-08-30 11:05:14 +02:00
Hosted Weblate
17274e9341 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2023-08-30 11:05:04 +02:00
Yaron Shahrabani
877fff1b2c 🌐 Add translations for: Hebrew.
Currently translated at 99.7% (1206 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2023-08-30 11:04:58 +02:00
AlexTECPlayz
7b5260eedd 🌐 Add translations for: Romanian.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2023-08-30 11:04:58 +02:00
Kristijan Žic
99b08402da 🌐 Add translations for: Croatian.
Currently translated at 84.9% (1027 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2023-08-30 11:04:57 +02:00
Linerly
2e899f1d9d 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2023-08-30 11:04:57 +02:00
Amine Gdoura
f39e962250 🌐 Add translations for: Arabic.
Currently translated at 61.4% (743 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2023-08-30 11:04:56 +02:00
Amerey.eu
263a4e32dc 🌐 Add translations for: Czech.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2023-08-30 11:04:56 +02:00
Linerly
7d55df10ab 🌐 Add translations for: Indonesian.
Currently translated at 97.0% (1173 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2023-08-30 11:04:55 +02:00
Stas Haas
5775129b53 🌐 Add translations for: Russian.
Currently translated at 63.1% (763 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2023-08-30 11:04:55 +02:00
Mikel Larreategi
05678f5002 🌐 Add translations for: Basque.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2023-08-30 11:04:54 +02:00
Stas Haas
853d2a9b29 🌐 Add translations for: German.
Currently translated at 98.6% (1193 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-08-30 11:04:53 +02:00
王世阳
70f7476614 🌐 Add translations for: Chinese (Simplified).
Currently translated at 99.8% (1207 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2023-08-30 11:04:53 +02:00
Ņikita K
ed0708bcbd 🌐 Add translations for: Latvian.
Currently translated at 96.5% (1167 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2023-08-30 11:04:52 +02:00
Andrey Antukh
43210e4b5a Merge branch 'staging' into develop 2023-08-30 10:48:49 +02:00
Andrey Antukh
cc5b1c950b Merge branch 'translations' into staging 2023-08-30 10:35:47 +02:00
Andrey Antukh
52851f4c6f 📎 Add dutch language 2023-08-30 10:35:33 +02:00
Andrey Antukh
9bd42be771 Merge remote-tracking branch 'weblate/develop' into translations 2023-08-30 10:26:28 +02:00
Eva Marco
0030447ea8 Merge pull request #3558 from penpot/hiru-show-assets-to-update
🎉 Show changed assets when updating libraries
2023-08-30 07:38:56 +02:00
Andrey Antukh
0d0c5ed96c Add minor performance improvement to get-viewer-frames
Reducing redundant lookups
2023-08-29 17:09:00 +02:00
Andrey Antukh
b7eb20dc44 Reduce unnecesary lookups on get-frame-by-position fn 2023-08-29 17:09:00 +02:00
Andrey Antukh
6b3fa31d68 🔥 Remove unused top-nested-frame-ids helper 2023-08-29 17:09:00 +02:00
Andrey Antukh
48881f218c 📎 Add minor improvements to schema generator helpers 2023-08-29 17:09:00 +02:00
Andrey Antukh
a82ee01d99 Add minor improvement to points->lines helper 2023-08-29 17:09:00 +02:00
Andrey Antukh
a9d2cc227b 💄 Add minor cosmetic improvements on viewport hooks ns 2023-08-29 17:09:00 +02:00
Andrey Antukh
a754d5ae3b Add throttling to over-shapes-stream on viewport 2023-08-29 17:09:00 +02:00
Andrey Antukh
ec1c1fcd2f 📎 Fix function naming
Rename `all-frames-by-position` to `get-frames-by-position`
2023-08-29 17:09:00 +02:00
Andrey Antukh
9cc7f3c600 Add minor performance optimization to all-frames-by-position 2023-08-29 17:09:00 +02:00
Andrey Antukh
80826e58ad Add missing boolean type hints 2023-08-29 17:09:00 +02:00
Andrey Antukh
ad73c449fd Replace mapv with map on get-frame-ids fn 2023-08-29 17:09:00 +02:00
Andrey Antukh
85a1443ada 💄 Add cosmetic improvements to get-frames fn 2023-08-29 17:09:00 +02:00
Andrey Antukh
ce0842ce87 🎉 Add d/unstable-sort helper 2023-08-29 17:09:00 +02:00
Andrey Antukh
59600d07c3 Add type hints to intersect-segments? fn 2023-08-29 17:09:00 +02:00
Andrey Antukh
5b73040696 Add type hints to on-segment? fn 2023-08-29 17:09:00 +02:00
Andrey Antukh
d8c1425daf Add minor perfromance improvement to is-point-inside-evenodd fn
Replace filter with filterv for avoid an other iteration on the
following count operation
2023-08-29 17:09:00 +02:00
Andrey Antukh
64accaa842 Simplify has-point? impl for non-path shapes 2023-08-29 17:09:00 +02:00
Andrés Moya
eed175dfe4 Rework usage of design components and tokens 2023-08-29 16:05:58 +02:00
Alejandro
266e1c7142 Merge pull request #3572 from penpot/eva-fix-layer-name-viewer
🐛 Fix layer name on viewer
2023-08-29 14:18:10 +02:00
Eva
befbb17ee3 🐛 Fix layer name on viewer 2023-08-29 14:11:04 +02:00
Andrey Antukh
1794ea0d9e Merge remote-tracking branch 'origin/staging' into develop 2023-08-29 13:25:51 +02:00
Andrey Antukh
5f65960d42 Merge pull request #3568 from penpot/eva-fix-tooltip-visibility
🐛 Fix tooltip on toggle visibility and toggle lock buttons
2023-08-29 13:15:54 +02:00
Eva
dc813732c3 🐛 Fix tooltip on toggle visibility and toggle lock buttons 2023-08-29 13:15:40 +02:00
Andrey Antukh
661e4a001a Merge pull request #3569 from penpot/superalex-fix-invalid-file-amount-after-moving-files
🐛 Bugfixing
2023-08-29 13:13:36 +02:00
Alejandro Alonso
53d1624f3f 🐛 Fix deleted pages comments shown in right sidebar 2023-08-29 13:13:12 +02:00
Alejandro Alonso
514ba6604b 🐛 Fix invalid file amount after moving files 2023-08-29 13:13:11 +02:00
Andrey Antukh
d8a42bf3c1 Merge pull request #3566 from penpot/superalex-fix-rulers
🐛 Bugfixing
2023-08-29 13:05:48 +02:00
Alejandro
0aa361013a Merge pull request #3551 from penpot/niwinz-bugfixes-1
🐛 Fix unexpected output on get-page when invalid object-id is pro…
2023-08-29 13:04:34 +02:00
Andrey Antukh
ddbc828342 🐛 Fix unexpected output on get-page when invalid object-id is provided 2023-08-29 13:04:23 +02:00
Alejandro
cbcaa582cd Merge pull request #3567 from penpot/eva-frontend-fixes
🐛 Fix some small frontend errors
2023-08-29 12:59:28 +02:00
Alejandro Alonso
67eb305202 🐛 Fix duplicate and copy/paste frames internal error 2023-08-29 11:52:35 +02:00
Andrey Antukh
cf2ee435c0 🐛 Fix incorrect event handling on dropdown menu
Related to react18 event handling new behavior
2023-08-29 11:11:16 +02:00
Eva
a225def708 Fix some small frontend errors 2023-08-29 07:46:19 +02:00
Sebastiaan Pasma
67cff1ed74 🌐 Add translations for: Dutch.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2023-08-28 16:57:12 +02:00
Alejandro Alonso
27534702fb 🐛 Fix viewer inspect code 2023-08-28 15:54:10 +02:00
Andrés Moya
5a312fd7b2 Use new css macros and fix link color in new style 2023-08-28 15:09:31 +02:00
Andrés Moya
d8027936b4 Small enhancements 2023-08-28 15:09:31 +02:00
Andrés Moya
ca88314524 🎉 Show changed assets when updating libraries 2023-08-28 15:09:31 +02:00
Alejandro Alonso
2b2d7bc406 🐛 Fix rulers 2023-08-28 13:29:22 +02:00
Sebastiaan Pasma
22c88a19e2 🌐 Add translations for: Dutch.
Currently translated at 83.2% (1007 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2023-08-26 15:54:37 +02:00
Andrés Moya
96a5444357 Validate frame-id 2023-08-25 13:13:00 +02:00
Andrey Antukh
159ac92021 Merge pull request #3561 from penpot/superalex-click-flow-tag-open-viewer
 Click on flow tag open viewer
2023-08-25 12:40:10 +02:00
Andrey Antukh
1a92657c7c Merge pull request #3559 from penpot/superalex-fix-alt-l-shortcuts
🐛 Fix alt+l shortcuts
2023-08-25 12:37:10 +02:00
Alejandro Alonso
8669207086 Click on flow tag open viewer 2023-08-25 11:40:56 +02:00
Alejandro Alonso
b82ce671b9 🐛 Fix alt+l shortcuts 2023-08-25 10:54:34 +02:00
Andrey Antukh
629322e505 🐛 Fix snapshot debug utils 2023-08-25 10:02:54 +02:00
Alejandro
90aab03a8f Merge pull request #3556 from penpot/niwinz-develop-enhancements-3
 Improvements on devenv and docker config
2023-08-24 15:07:17 +02:00
Andrey Antukh
cb7fbc2cc4 🐛 Fix cache issues on default nginx configuration on docker images 2023-08-24 14:49:37 +02:00
Andrey Antukh
e998ec7c2d 🐛 Fix cache issues on devevn nginx config 2023-08-24 14:49:37 +02:00
Aitor Moreno
ff14208a95 Merge pull request #3555 from penpot/superalex-navigate-up-in-layer-hierarchy-with-shift-enter-shortcut
 Navigate up in layer hierarchy with Shift+Enter shortcut
2023-08-24 13:42:12 +02:00
Aitor
8593ca1310 🐛 Fix scroll automatically to layer item 2023-08-24 13:31:47 +02:00
Andrey Antukh
b80469c040 ⬆️ Update devenv dependencies 2023-08-24 13:19:02 +02:00
Alejandro Alonso
f69e141ac1 Navigate up in layer lierarchy with Shift+Enter shortcut 2023-08-24 12:25:03 +02:00
Andrey Antukh
496afb0f25 Merge remote-tracking branch 'origin/staging' into develop 2023-08-24 12:02:40 +02:00
Alejandro
b0497f1352 Merge pull request #3554 from penpot/niwinz-staging-bugfixes-8
🐛 Prevent rollback for idle-in-transaction errors on cron tasks
2023-08-24 12:02:13 +02:00
Alejandro Alonso
aaf9c6e50b Enable access tokens by default 2023-08-24 12:00:56 +02:00
Pablo Alba
c3f73ff7aa 🐛 Fix error on press escape while renamming a component 2023-08-24 11:50:59 +02:00
Andrey Antukh
d80aa7593b 🐛 Fix unexpected exception on encoding error response 2023-08-24 11:37:59 +02:00
Andrés Moya
027ef48e66 Add tooltip to library name 2023-08-24 11:34:10 +02:00
Pablo Alba
453c576fdd 💄 Assets tab visual adjustments 2023-08-24 11:34:10 +02:00
Andrey Antukh
5275c35002 🐛 Prevent rollback for idle-in-transaction errors on cron tasks 2023-08-24 11:18:56 +02:00
Alejandro
e1507755ba Merge pull request #3550 from penpot/superalex-fix-union-operations
🐛 Fix union operations
2023-08-24 06:53:39 +02:00
Andrey Antukh
3292e7b923 🐛 Make clj/jvm record impl behave the same as cljs/js 2023-08-23 18:47:26 +02:00
Andrey Antukh
e4ec954b8c 🐛 Fix incorrect impl of without-keys for records 2023-08-23 18:47:26 +02:00
Alejandro Alonso
0782382ee1 🐛 Fix union operations 2023-08-23 18:47:26 +02:00
Alejandro Alonso
f02b5765d7 🐛 Fix safe number max values 2023-08-23 14:59:08 +02:00
Pablo Alba
a6ec73fd4c Merge pull request #3553 from penpot/niwinz-bugfixes-2
🐛 Set proper minimal shape size on draw on click operation
2023-08-23 12:47:18 +02:00
Andrey Antukh
c0422f4e13 🐛 Set proper minimal shape size on draw on click operation 2023-08-23 12:43:28 +02:00
Pablo Alba
9618bd6697 Merge pull request #3538 from penpot/hiru-validate-shapes
 Add function to validate shape referential integrity
2023-08-23 10:03:38 +02:00
Alejandro Alonso
1f31722571 📎 Update version.txt file 2023-08-23 09:38:23 +02:00
Alejandro Alonso
834c18323e Revert "📎 Update version.txt file"
This reverts commit a7f39e89f6.
2023-08-23 09:38:07 +02:00
Andrés Moya
730df04970 Add function to validate shape referential integrity 2023-08-22 17:59:28 +02:00
andy
1d2f5b6c0b 🌐 Add translations for: Norwegian Bokmål.
Currently translated at 12.5% (152 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nb_NO/
2023-08-22 11:49:11 +02:00
Pablo Alba
2ca28721f7 🐛 Fix instanciate an object set it in the top frame of a tree 2023-08-22 11:28:00 +02:00
Alejandro
ab87db099a Merge pull request #3542 from penpot/niwinz-bugfixes-1
🐛 Fix inconsistencies on handlong :file-image attr on import and file-gc task
2023-08-22 06:50:58 +02:00
Andrey Antukh
661a916a5f 🐛 Fix reference counting of file-media objects in :fill-image attr 2023-08-21 19:11:55 +02:00
Andrey Antukh
b8dee17075 🐛 Fix incorrect streams handling on thumbnail_render 2023-08-21 19:11:55 +02:00
Andrey Antukh
1709f84a14 Merge remote-tracking branch 'origin/develop' into develop 2023-08-21 17:26:46 +02:00
Andrey Antukh
e6664013ba Merge remote-tracking branch 'origin/staging' into develop 2023-08-21 17:26:21 +02:00
Pablo Alba
2ada687ecc Show a confirmation dialog when an user tries to publish an empty library 2023-08-21 16:29:53 +02:00
Pablo Alba
1642efbaa4 Merge pull request #3534 from penpot/hiru-fix-absorb-library
🐛 Fix absorb unpublished library
2023-08-21 15:34:59 +02:00
Andrey Antukh
bfff547fdf Merge pull request #3525 from penpot/niwinz-react-update
 Update to React 18
2023-08-21 14:49:34 +02:00
Pablo Alba
7336312b75 New component icon 2023-08-21 14:45:32 +02:00
Aitor
4b8ee8ef84 Update to React 18 2023-08-21 14:34:54 +02:00
Alejandro Alonso
5ea9a52e69 🐛 Fix viewer 2023-08-21 14:18:56 +02:00
Pablo Alba
0ce838fbb6 Merge pull request #3533 from penpot/hiru-update-board-grids
 Board grids are now synced with components
2023-08-21 11:45:25 +02:00
Pablo Alba
3de50986e7 🐛 Fix component context menu 2023-08-21 09:38:29 +02:00
Stas Haas
c8d5e4ef35 🌐 Add translations for: German.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-08-19 13:55:24 +02:00
Alejandro Alonso
a7f39e89f6 📎 Update version.txt file 2023-08-18 10:35:12 +02:00
Andrés Moya
8e2011c755 🐛 Fix absorb unpublished library 2023-08-17 17:50:19 +02:00
Pablo Alba
70bb34118c Merge pull request #3532 from penpot/hiru-fix-component-modified
🐛 Fix component modified date in v1
2023-08-17 17:18:48 +02:00
Andrés Moya
93a0e79167 Board grids are now synced with components 2023-08-17 16:29:47 +02:00
Andrés Moya
f409dfd3d1 🐛 Fix component modified date in v1 2023-08-17 16:05:54 +02:00
Alejandro Alonso
e1954b5dd7 🐛 Fix old files with invalid refs for texts and fills 2023-08-17 09:48:18 +02:00
Pablo Alba
c2a27bb845 🐛 Fix update main targeting remote-shape 2023-08-17 09:38:30 +02:00
Pablo Alba
c5315de91c 🐛 Reset component is now against remote main 2023-08-17 09:38:30 +02:00
Andrés Moya
f8e1a15907 Enhance dump-tree debug command and add dump-subtree 2023-08-17 09:38:30 +02:00
Andrés Moya
8b801b65f6 Enhance synchronization of nested shapes 2023-08-17 09:38:30 +02:00
Sebastiaan Pasma
196d57dd5c 🌐 Add translations for: Dutch.
Currently translated at 74.1% (897 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2023-08-15 15:50:00 +02:00
Alejandro
a1ac839b2a Merge pull request #3517 from penpot/niwinz-enhancements-push-notifications
🎉 Add the ability to send push notifications
2023-08-14 12:24:43 +02:00
Andrey Antukh
1e9a4d74eb 🐛 Add safechecks to binfile exportation 2023-08-14 12:13:31 +02:00
Andrey Antukh
7a9777419c Backport db module improvements from develop 2023-08-14 12:13:31 +02:00
Andrey Antukh
28836d82cd Add minor improvements to error report template 2023-08-14 12:13:31 +02:00
Andrey Antukh
da62a6809c Stop report oidc failed operations as exceptions 2023-08-14 12:13:31 +02:00
Andrey Antukh
5d5d238fec 💄 Add minor cosmetic improvements on dashboard ui component 2023-08-14 12:13:31 +02:00
Andrey Antukh
e5dedb1e3d 🎉 Add push notifications support 2023-08-14 12:13:31 +02:00
Alejandro
2e33575f01 Merge pull request #3524 from penpot/juan-ester-ui-review
💄 Adds styling changes to new UI
2023-08-14 08:41:58 +02:00
elhombretecla
bf0a676b83 💄 Adds new modal and toolbar styles 2023-08-14 08:33:49 +02:00
Sebastiaan Pasma
4c7cd02f56 🌐 Add translations for: Dutch.
Currently translated at 54.0% (654 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2023-08-12 14:49:53 +02:00
Alejandro Alonso
b3128bd32b 🐛 Fix overlay manual positioning 2023-08-10 11:15:42 +02:00
Alejandro
b3f62d8a82 Merge pull request #3515 from penpot/niwinz-develop-bugfixes-3
🐛 Fix incorrect position data calculation on generating thumbnails
2023-08-10 10:59:55 +02:00
Andrey Antukh
9b61aae216 🐛 Fix incorrect attributes usage on shape 2023-08-10 09:47:25 +02:00
elhombretecla
6420188675 💄 Adds new CSS polishing 2023-08-10 08:57:32 +02:00
Andrey Antukh
d02329115a 🐛 Fix incorrect position data calculation on generating thumbnails
Only one change line, but it took 4 hours of work to find it...
2023-08-09 19:20:55 +02:00
Andrey Antukh
31323703a8 Merge remote-tracking branch 'origin/staging' into develop 2023-08-09 13:36:42 +02:00
Alejandro Alonso
15a9035ed1 🐛 Fix multiple elements export 2023-08-09 12:19:27 +02:00
elhombretecla
8b9781f345 💄 Adds new components styles 2023-08-09 11:31:50 +02:00
elhombretecla
bc14f59153 💄 Fix color assets and styles 2023-08-09 11:11:51 +02:00
elhombretecla
af460536d1 💄 Fix css left-header 2023-08-09 09:08:56 +02:00
Vincas Dundzys
82e51d358b 🌐 Add translations for: Lithuanian.
Currently translated at 10.3% (125 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lt/
2023-08-09 09:04:18 +02:00
Alejandro
6ceb816362 Merge pull request #3460 from penpot/niwinz-develop-enhancements-2
 Several enhancements (performance and code style)
2023-08-09 08:30:00 +02:00
Alejandro
091d1ff5cf Merge pull request #3457 from penpot/niwinz-develop-bugfixes-2
🐛 Fix unexpected exception on viewer when page has no frame
2023-08-09 08:19:53 +02:00
Alejandro
fbcc2494b4 Merge pull request #3509 from penpot/niwinz-staging-bugfixes-7
 Add improvements to api doc
2023-08-09 08:00:02 +02:00
Andrey Antukh
4a016dce14 📎 Add minor improvements on params formating on error logger 2023-08-08 14:53:55 +02:00
Andrey Antukh
53f40043aa 📎 Fix typo on api doc main template 2023-08-08 14:52:39 +02:00
Alejandro Alonso
937dd5a857 🐛 Fix zip importer for none fills 2023-08-08 13:52:09 +02:00
Andrey Antukh
36b167956c Add improvements to api doc 2023-08-08 13:44:47 +02:00
Alejandro
695152274c Merge pull request #3506 from penpot/niwinz-staging-bugfixes-7
 Improve error report of invalid image
2023-08-08 13:17:34 +02:00
Andrey Antukh
486c638076 🐛 Fix image upload issues on safari with drag&drop 2023-08-08 12:58:39 +02:00
Andrey Antukh
81facd58c9 Improve error report of invalid image 2023-08-08 12:57:49 +02:00
Alejandro
2a0031d23c Merge pull request #3505 from penpot/niwinz-staging-bugfixes-7
💄 Add  minor cosmetic improvement on error report template
2023-08-08 11:07:02 +02:00
Andrey Antukh
63a3186e6d 💄 Add minor cosmetic improvement on error report template 2023-08-08 10:42:26 +02:00
Alejandro Alonso
fcdf33b134 🐛 Fix backend api doc generation for auth required endpoints 2023-08-08 10:39:09 +02:00
Alejandro Alonso
19d88cc1a6 🐛 Fix backend api doc generation 2023-08-08 09:55:32 +02:00
Alejandro
1f68c6164a Merge pull request #3501 from penpot/niwinz-staging-bugfixes-7
 Improve get-user-info implementation (oidc)
2023-08-07 16:36:19 +02:00
Andrey Antukh
c39702fbf7 Improve get-user-info implementation (oidc) 2023-08-07 15:55:54 +02:00
Alejandro Alonso
b3f0683d02 🐛 Fix image validation 2023-08-07 15:06:59 +02:00
Andrey Antukh
1979e6f283 Merge remote-tracking branch 'origin/staging' into develop 2023-08-07 13:00:26 +02:00
Andrey Antukh
39741f98c0 Merge remote-tracking branch 'origin/develop' into develop 2023-08-07 12:59:50 +02:00
Andrey Antukh
80bf7cc1e5 Merge remote-tracking branch 'origin/staging' into develop 2023-08-07 12:59:17 +02:00
Alejandro
211de1bb9c Merge pull request #3498 from penpot/niwinz-staging-bugfixes-6
🐛 Allow nil values on bool content params
2023-08-07 12:57:30 +02:00
Alejandro
8ad16f9644 Merge pull request #3465 from penpot/eva-structure-redesign
💄 UI structure redesign
2023-08-07 12:57:21 +02:00
Eva
28a06c99b5 💄 UI structure redesign 2023-08-07 12:52:36 +02:00
Andrey Antukh
fe80aab394 🐛 Allow nil values on bool content params 2023-08-07 11:46:19 +02:00
Alejandro
a494b89bba Merge pull request #3497 from penpot/niwinz-staging-bugfixes-6
🐛 Fix incorrect implementation on error reporting context collection
2023-08-07 11:20:00 +02:00
Andrey Antukh
6e313dff84 🐛 Add workaround for unexpected exception on fix-broken-shapes
which happens when we have a component shape tree with an ephimeral
shape with id ZERO (unused and with invalid children)
2023-08-07 11:12:27 +02:00
Alejandro Alonso
766040198a 🐛 Fix text validation 2023-08-07 09:29:04 +02:00
Andrey Antukh
7afaa9d31f 🐛 Fix incorrect implementation on error reporting context collection 2023-08-04 18:40:47 +02:00
Pablo Alba
b62a149b34 🐛 Fix when component has a long name then its icon and '3 dots' menu are not visible on Design tab 2023-08-04 17:52:51 +02:00
Alejandro Alonso
cf68a9cf1e 🐛 Fix safe number max values 2023-08-04 15:16:43 +02:00
Alejandro
c69f6da2d7 Merge pull request #3493 from penpot/niwinz-staging-bugfixes-6
🐛 Several bugfixes
2023-08-04 13:35:12 +02:00
Alejandro
d02129ef04 Merge pull request #3490 from penpot/niwinz-enhancements-srepl
 Add file snapshot related internal functions
2023-08-04 13:24:49 +02:00
Andrey Antukh
259b05db51 Add more improvements to error reporting 2023-08-04 13:10:36 +02:00
Andrey Antukh
2ba7996116 🐛 Fix unexpected viewport update on leave workspace 2023-08-04 12:58:27 +02:00
Andrey Antukh
66e877ed40 🐛 Fix stroke-width parsing on svg upload
And refactor a bit the stroke parsing function
2023-08-04 12:58:27 +02:00
Pablo Alba
53ea8a7f53 🐛 Fix texts on deleteunpublish library 2023-08-04 11:04:13 +02:00
Andrey Antukh
bc27d9aab2 🎉 Add helpers to frontend debug entry point 2023-08-04 08:28:01 +02:00
Andrey Antukh
13d68a53c0 🎉 Add rpc method for working with file snapshots 2023-08-04 08:28:01 +02:00
Andrey Antukh
d1128a6b1e 🎉 Add helpers for take file snapshots 2023-08-03 17:51:34 +02:00
Andrey Antukh
f039b904f2 Add the ability to skip some rpc methods from api doc 2023-08-03 17:51:34 +02:00
Alejandro
f3bf04e1c9 Merge pull request #3488 from penpot/niwinz-staging-bugfixes-5
 Add better error reporting on response encoding middleware
2023-08-03 16:46:05 +02:00
Andrey Antukh
1190cf837b Add an internal approach to prevent xlog gc to remove file changes 2023-08-03 16:40:42 +02:00
Alejandro Alonso
79e3aadfcf 🐛 Fix undo change for multiple shapes 2023-08-03 16:38:21 +02:00
Andrey Antukh
0527c55398 Add better exception handling on json content type handling 2023-08-03 16:31:35 +02:00
Andrey Antukh
54bb89b2bb ⬆️ Upgrade yetti to v9.16 (fixes exception unwrapping) 2023-08-03 16:31:35 +02:00
Andrey Antukh
9334f935eb Add better error reporting on response encoding middleware 2023-08-03 16:10:41 +02:00
Andrey Antukh
804addfa66 📎 Add srepl helper for process files 2023-08-03 11:49:14 +02:00
Alejandro
fed31d366f Merge pull request #3480 from penpot/azazeln28-bugfixing-1
🐛 Bug fixing
2023-08-03 07:28:28 +02:00
Aitor Moreno
55b7bba944 Merge pull request #3484 from penpot/superalex-fix-duplicate-board
🐛 Fix duplicate board
2023-08-02 18:27:18 +02:00
Alejandro Alonso
3ff13f1d8f 🐛 Fix duplicate board 2023-08-02 18:22:46 +02:00
Aitor
4b28685a6d 🐛 Fix prototype selects preventing ctrl-z 2023-08-02 16:15:08 +02:00
Alejandro
53001921d5 Merge pull request #3481 from penpot/niwinz-staging-hotfix-4
🐛 Bugfixes & Improvements
2023-08-02 16:08:12 +02:00
Andrey Antukh
046f501152 Improve error reporting context 2023-08-02 14:51:12 +02:00
Andrey Antukh
00f7c94377 Improve database error reporter 2023-08-02 13:43:53 +02:00
Andrey Antukh
eae5dfc828 🐛 Don't send empty changes on fix broken shape links 2023-08-02 13:43:53 +02:00
Andrey Antukh
88261c2ec3 Increase network timeout on exporter dockerfile 2023-08-02 13:43:53 +02:00
Andrey Antukh
1bfc28f63d Add missing index on server_error_report table 2023-08-02 13:43:53 +02:00
Alejandro Alonso
e7a82579c1 🐛 Fix paste groups without shapes attr 2023-08-02 11:17:20 +02:00
Alejandro
30c786741f Merge pull request #3478 from penpot/niwinz-staging-hotfix-4
🐛 Fix broken shape relations on workspace initialization
2023-08-02 11:13:39 +02:00
Pablo Alba
1bb3a3a084 🐛 Add script for fix files with bad shape-ref 2023-08-02 10:46:06 +02:00
Andrey Antukh
3eb2569465 Add better exception reporting on commit-changes 2023-08-02 10:45:11 +02:00
Andrey Antukh
7efeeec9b1 Add workspace initialization fix for broken shape references
Is the code that executes at workspace initialization that checks all
the shape children for broken references and proceed to emit a special
event that fixes the shape children references.
2023-08-02 10:45:11 +02:00
Aitor
67f56dd0f8 🐛 Fix color picker not working when using shortcut 2023-08-02 10:18:40 +02:00
Alejandro
2ec5a3ba6a Merge pull request #3476 from penpot/niwinz-staging-hotfix-4
 Improve ws-conn handling on session expiration
2023-08-01 14:41:15 +02:00
Andrey Antukh
958931d264 Improve ws-conn handling on session expiration 2023-08-01 13:09:51 +02:00
Alejandro Alonso
e3f69bcc98 🐛 Fix path validation 2023-08-01 12:39:33 +02:00
Alejandro
9c53a33bac Merge pull request #3472 from penpot/niwinz-staging-hotfix-3
🐛 Ensure :shapes attr on importing an svg with an empty group
2023-07-31 16:33:06 +02:00
Andrey Antukh
f72206bba3 🐛 Ensure :shapes attr on importing an svg with an empty group
This commit should not not be backported to, because the affected
code is already refactored and the issue is already fixed on develop
branch
2023-07-31 16:26:03 +02:00
Alejandro
37a19aa6b5 Merge pull request #3471 from penpot/niwinz-staging-hotfix-3
🐛 Hot Fixes
2023-07-31 16:20:47 +02:00
Andrey Antukh
17ea8300ed 🐛 Accept nil values for :fill-color-gradient attr 2023-07-31 15:58:32 +02:00
Andrey Antukh
aac044fa0a 🐛 Fix incorrect schema on bool-content 2023-07-31 15:49:42 +02:00
Alejandro
e935ccae76 Merge pull request #3469 from penpot/niwinz-staging-hotfix-2
🐛 Allow nil values for x,y,width and height on paths
2023-07-31 13:41:22 +02:00
Andrey Antukh
13312dc467 🐛 Allow nil values for x,y,width and height on paths 2023-07-31 13:36:28 +02:00
Alejandro Alonso
0ec49e5e95 🐛 Fix remove content from boolean 2023-07-31 13:02:52 +02:00
Andrey Antukh
228b09c340 Merge remote-tracking branch 'origin/staging' into develop 2023-07-31 12:33:54 +02:00
Alejandro
a49999186f Merge pull request #3466 from penpot/niwinz-staging-hotfix-1
🐛 Remove limits that can cause unexpected exceptions
2023-07-31 12:09:58 +02:00
Andrey Antukh
fc416ee4af 🐛 Make grid params type optional 2023-07-31 12:06:31 +02:00
Andrey Antukh
37bd537bfd 🐛 Remove limits that can cause unexpected exceptions 2023-07-31 11:54:29 +02:00
Andrey Antukh
a64cb47afb Merge remote-tracking branch 'origin/staging' into develop 2023-07-31 11:13:40 +02:00
Alejandro
17798dbf40 Merge pull request #3459 from penpot/niwinz-staging-bugfixes
🐛 Bugfixes & Enhancements
2023-07-31 10:20:44 +02:00
Alejandro
4e1dfcce32 Merge pull request #3453 from penpot/azazeln28-fix-thumbnail-rendering-flashing
🐛 Fix thumbnail rendering flashing
2023-07-31 09:21:55 +02:00
Aitor
c28da17515 🐛 Fix thumbnail rendering flashing 2023-07-31 09:03:33 +02:00
Alejandro
9f0e65a042 Merge pull request #3450 from penpot/azazeln28-fix-ctrl-z-select-issue
🐛 Fix CTRL+Z in workspace select
2023-07-31 08:46:51 +02:00
Aitor
f1cf5d8ba8 🐛 Fix ctrl+z in workspace select issue 2023-07-31 08:38:48 +02:00
Eva Marco
cc682a382f Merge pull request #3455 from penpot/azazeln28-fix-layers-scroll-breaking-new-css-system
Fix layers scroll breaking new css system
2023-07-31 08:03:34 +02:00
Andrey Antukh
b616a20b28 Add performance oriented refactor to the outline component 2023-07-28 16:38:28 +02:00
Andrey Antukh
c3eb90b1fa ♻️ Add minor refactor to release dialog components 2023-07-28 16:19:27 +02:00
Andrey Antukh
dcd428d3b2 ♻️ Add minor refactor to dashboard export dialog components 2023-07-28 16:18:59 +02:00
Andrey Antukh
9d2fc63780 Merge remote-tracking branch 'origin/staging' into develop 2023-07-28 16:18:37 +02:00
Pablo Alba
340fe75204 🐛 Fix copies have select color wrong 2023-07-28 13:39:16 +02:00
Andrey Antukh
1f98b168ba 🐛 Set correct modification date on projects on file move operation 2023-07-28 13:20:57 +02:00
Andrey Antukh
21430cbd7d Show project modified date consistently 2023-07-28 13:20:57 +02:00
Andrey Antukh
f174264f7f 🎉 Add flex layout playground template to the dashboard carousel 2023-07-28 13:20:57 +02:00
Andrey Antukh
51d0851846 🐛 Fix unexpected exception on viewer when page has no frame 2023-07-28 11:55:42 +02:00
Aitor Moreno
6eaa905f0c Merge pull request #3456 from penpot/niwinz-bugfixes
🐛 Bugfixes & Enhancements
2023-07-28 11:51:36 +02:00
Andrey Antukh
f76f4615cf Merge remote-tracking branch 'origin/staging' into develop 2023-07-28 11:48:50 +02:00
Andrey Antukh
1c23e4e8be 🎉 Add v1.19 release notes dialog 2023-07-28 11:27:23 +02:00
Andrey Antukh
e0ad6c0b95 🐛 Fix unexpected exception on saving boolean shapes 2023-07-28 10:43:03 +02:00
Aitor
f1d73d5662 🐛 Fix layers scroll breaking new css system 2023-07-28 10:37:17 +02:00
Pablo Alba
102e05bdf7 🐛 Fix shape-ref missing in nested components copies 2023-07-28 09:20:17 +02:00
Andrey Antukh
960ae66cbd Improve srelp.helper analyze-files usability 2023-07-27 11:49:41 +02:00
Pablo Alba
456b604937 📎 Add debug functions for shape-ref 2023-07-27 11:23:41 +02:00
Aitor Moreno
bbe3021aed Merge pull request #3448 from penpot/superalex-bugfixing-19
🐛 Bugfixing
2023-07-26 15:16:53 +02:00
Alejandro Alonso
934c6c5aae 🐛 Avoid just white spaces for old password 2023-07-26 15:12:35 +02:00
Alejandro Alonso
7036dddad1 🐛 Fix enable undo just after using pencil 2023-07-26 07:37:23 +02:00
Alejandro Alonso
92ee6320f5 🐛 Fix enable comment mode and insert image keeps on comment mode 2023-07-26 06:18:24 +02:00
Alejandro Alonso
8a3c580d0f 🐛 Fix undo layer mode preview 2023-07-26 06:18:08 +02:00
Aitor Moreno
08a11929ca Merge pull request #3442 from penpot/eva-bugfixing-11
Bugfixing
2023-07-25 17:42:05 +02:00
Alejandro
b460a8f64e Merge pull request #3447 from penpot/superalex-fix-retrieve-unread-comment-threads-extra-calls
🐛 Fix retrieve unread comment threads extra calls
2023-07-25 14:56:21 +02:00
Alejandro Alonso
1aa7960863 🐛 Fix retrieve unread comment threads extra calls 2023-07-25 14:50:42 +02:00
Pablo Alba
577c2b39dc ♻️ Rename helper root-frame? to is-direct-child-of-root? 2023-07-25 13:59:12 +02:00
Alejandro
89edcb5651 Merge pull request #3446 from penpot/niwinz-improve-connection-error-handling-on-save
 Improve connection errors handling on workspace save operation
2023-07-25 13:56:37 +02:00
Eva
653bc66b8f 🐛 Fix dropdown width 2023-07-25 13:27:07 +02:00
Andrey Antukh
bec09fb5d1 Improve connection errors handling on workspace save operation 2023-07-25 12:52:47 +02:00
Eva
9048c01308 🐛 Fix copy color information in several formats 2023-07-25 11:57:41 +02:00
Eva
959e069ea9 🐛 Fix unnecessary button 2023-07-25 11:57:39 +02:00
Eva
955bf0ef9e 🐛 Fix empty reply comments 2023-07-25 11:57:20 +02:00
Alejandro
35f931c05a Merge pull request #3436 from penpot/niwinz-enhancements
 Several enhacements
2023-07-25 10:43:07 +02:00
Eva Marco
9a60ac477f Merge pull request #3434 from penpot/superalex-bugfixing-18
🐛 Superalex bugfixing
2023-07-25 10:36:32 +02:00
Alejandro Alonso
ec131382b3 🐛 Fix error when a user different than the thread creator edits a comment 2023-07-25 10:32:11 +02:00
Alejandro Alonso
ea2e25b46d 🐛 Making old-password non required again 2023-07-25 10:32:11 +02:00
Alejandro Alonso
db7c4a9265 🐛 Fix export multiple images when only one of them has export settings 2023-07-25 10:32:11 +02:00
Alejandro Alonso
1b31a02c14 🐛 Fix when user deletes one file during import it is impossible to finish importing of second file 2023-07-25 10:32:09 +02:00
Alejandro
dcbf57d8d2 Merge pull request #3443 from penpot/palba-fix-incorrect-style-layers-tab-titles
Fix incorrect style for layers tab titles
2023-07-25 09:55:18 +02:00
Pablo Alba
6e73e7cc71 Fix incorrect style for layers tab titles 2023-07-25 09:45:52 +02:00
Aitor
44e31f1890 📚 Add missing change in CHANGES.md 2023-07-25 09:05:59 +02:00
Alejandro Alonso
fc4ed48626 Merge remote-tracking branch 'origin/staging' into develop 2023-07-25 06:57:48 +02:00
Aitor
fb4ee4a355 🐛 Fix text gradient handlers 2023-07-25 06:56:25 +02:00
Alejandro
af368d656d Merge pull request #3440 from penpot/azazeln28-fix-text-gradient-handlers
🐛 Fix text gradient handlers
2023-07-25 06:55:11 +02:00
Yaron Shahrabani
1a92bd0478 🌐 Add translations for: Hebrew.
Currently translated at 99.7% (1206 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2023-07-24 21:05:43 +02:00
Aitor
d83b8f29b6 🐛 Fix text gradient handlers 2023-07-24 16:06:45 +02:00
Pablo Alba
6c0d57ba03 🐛 Cant't delete copies 2023-07-24 14:59:17 +02:00
Andrey Antukh
08b35e19fb ♻️ Refactor editable-label component 2023-07-24 13:29:01 +02:00
Andrey Antukh
fb942a9620 ♻️ Refactor color-name component 2023-07-24 13:29:01 +02:00
Andrey Antukh
e60be6f262 ♻️ Refactor button-link component 2023-07-24 13:29:01 +02:00
Andrey Antukh
1e9c809b84 Add minor performance optimizations to code-block component 2023-07-24 13:29:01 +02:00
Andrey Antukh
a44f2c5788 ♻️ Add minor refactor to radio buttons components 2023-07-24 13:29:01 +02:00
Andrey Antukh
397ada1f78 ♻️ Refactor color-input naming 2023-07-24 13:29:01 +02:00
Andrey Antukh
5f558d6fdc ♻️ Refactor numeric-input naming 2023-07-24 13:29:00 +02:00
Andrey Antukh
02c853cf57 Prevent unexpected requests on dashboard after logout 2023-07-24 13:27:27 +02:00
Andrey Antukh
98091057f9 ♻️ Refactor fm/submit-button component 2023-07-24 13:27:27 +02:00
Andrey Antukh
9b9c5822d1 📎 Add minor improvement to events ns error logging 2023-07-24 13:27:27 +02:00
Andrey Antukh
27fb4c7ed9 Improve with-atomic macro to accept cfg 2023-07-24 13:27:27 +02:00
Andrey Antukh
d268ff2952 Merge remote-tracking branch 'origin/staging' into develop 2023-07-24 13:26:17 +02:00
Alejandro
d254184057 Merge pull request #3428 from penpot/alotor-bugfixes-6
Bugfixes
2023-07-24 07:42:03 +02:00
AlexTECPlayz
cd55adefb8 🌐 Add translations for: Romanian.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2023-07-18 15:02:57 +02:00
Andrey Antukh
7e73ac307a Merge pull request #3426 from penpot/alotor-undo-transaction-fixes
Create guard for undo transactions
2023-07-14 17:25:17 +02:00
alonso.torres
f611584bb3 🐛 Create guard for undo transactions 2023-07-14 15:37:49 +02:00
Andrey Antukh
c1013c359d 💄 Add cosmetic improvements to update-position fn 2023-07-14 15:35:33 +02:00
Andrey Antukh
e97aab4c7f 💄 Add cosmetic improvements to align-object-to-parent fn 2023-07-14 15:35:33 +02:00
Andrey Antukh
a3f347c9fd 🐛 Fix object alignment issue 2023-07-14 15:35:33 +02:00
Andrey Antukh
e78edca5a8 🐛 Increase version numbers for ensure execute migrations again 2023-07-14 15:35:33 +02:00
Alejandro Alonso
e9914d5265 Merge remote-tracking branch 'origin/staging' into develop 2023-07-14 15:27:38 +02:00
alonso.torres
e1faba2ddc 🐛 Fix absolute positioned layouts not showing flex properties 2023-07-14 15:06:50 +02:00
Alejandro Alonso
0f60f115f5 🐛 Fix focus list for texts 2023-07-14 14:59:06 +02:00
Eva Marco
13560bc866 Merge pull request #3422 from penpot/palba-fix-library-title-style
🐛 Fix incorrect style for asset libraries titles
2023-07-14 14:43:43 +02:00
alonso.torres
c670089c03 🐛 Fix problem with skew transformations 2023-07-14 14:30:26 +02:00
alonso.torres
b1f0d09501 🐛 Fix assets right click button for multiple selection 2023-07-14 14:30:26 +02:00
alonso.torres
53b4c6383b 🐛 Fix undo when updating several texts 2023-07-14 14:30:26 +02:00
Eva Marco
e9819ab063 Merge pull request #3423 from penpot/fix-invite-cursor-position
🐛 Fix position of text cursor is a bit too high in Invitations se…
2023-07-14 14:05:05 +02:00
Pablo Alba
3af019ca6f Merge pull request #3420 from penpot/hiru-fix-touched
🐛 Fix touched detecion in texts
2023-07-14 13:45:49 +02:00
Pablo Alba
9b9f2c39b9 🐛 Fix duplicate a component copy missing shape-ref 2023-07-14 12:36:13 +02:00
Pablo Alba
203b6c63a4 🐛 Fix incorrect style for asset libraries titles 2023-07-14 12:27:42 +02:00
Pablo Alba
217ca66720 🐛 Fix position of text cursor is a bit too high in Invitations section 2023-07-14 12:25:01 +02:00
Eva Marco
4ab13ed435 Merge pull request #3419 from penpot/niwinz-enhancements-css
💄 Add mainly cosmetic improvements to several components
2023-07-14 11:14:15 +02:00
Stas Haas
3006ed7966 🌐 Add translations for: German.
Currently translated at 99.8% (1207 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-07-14 10:49:25 +02:00
Alejandro Alonso
ab16bba21b Merge remote-tracking branch 'origin/staging' into develop 2023-07-14 07:34:25 +02:00
Alejandro
1106ebc377 Merge pull request #3418 from penpot/alotor-fix-safari-thumbs
🐛 Fix problem with safari thumbnails
2023-07-14 07:34:13 +02:00
Andrés Moya
de7a3bf52c 🐛 Fix touched detecion in texts 2023-07-13 17:10:03 +02:00
alonso.torres
9bcb3e9e7f 🐛 Fix problem with Safari thumbnails 2023-07-13 17:05:25 +02:00
Andrey Antukh
62fb9c3cfe Improve css handling on color-bullet-new component 2023-07-13 16:34:14 +02:00
Andrey Antukh
b5dac770d3 Improve performance of button-link component 2023-07-13 16:32:03 +02:00
Andrey Antukh
6ae58a77ed 💄 Use native destructuring support instead of unchecked-get 2023-07-13 16:31:59 +02:00
Andrey Antukh
00f4abbad9 Improve css handling performance on title-bar component 2023-07-13 15:55:46 +02:00
Andrey Antukh
6c13925930 🐛 Fix bad interaction of file migrations components-v2 and pointer-map feature 2023-07-13 15:00:28 +02:00
Alejandro Alonso
e8de8c2401 Merge remote-tracking branch 'origin/staging' into develop 2023-07-13 13:38:53 +02:00
Alejandro
39b46b3bc7 Merge pull request #3417 from penpot/azazeln28-fix-previous-thumbnail-rendered
🐛 Fix previous thumbnail rendered
2023-07-13 13:34:38 +02:00
Aitor
b0ba06eca8 Set smooth/instant autoscroll depending on distance 2023-07-13 13:32:15 +02:00
Eva
477dc6315e 🐛 Fix create empty comments 2023-07-13 13:31:31 +02:00
Eva
a1b90a8569 🐛 Fix exports menu on viewer mode 2023-07-13 13:31:31 +02:00
Eva
743397323d 🐛 Fix create typography with section closed 2023-07-13 13:31:31 +02:00
Eva
9e15a7548f 🐛 Fix onboarding modal height 2023-07-13 13:30:38 +02:00
Alejandro
529ef75058 Merge pull request #3414 from penpot/azazeln28-improve-layers-autoscroll
  Improve layers autoscroll
2023-07-13 13:14:51 +02:00
Aitor
2977709468 🐛 Fix previous thumbnail being rendered when fill is transparent 2023-07-13 13:14:41 +02:00
Alejandro
c4ca40da16 Merge pull request #3410 from penpot/eva-fix
🐛 Some frontend fixes
2023-07-13 13:13:45 +02:00
Alejandro Alonso
ffc65c3e31 Merge remote-tracking branch 'origin/staging' into develop 2023-07-13 12:59:53 +02:00
Alejandro
a6818a8a55 Merge pull request #3407 from penpot/azazeln28-fix-svg-text-thumbnail-rendering
🐛 Fix SVG text rendering on thumbnails
2023-07-13 12:59:20 +02:00
Aitor
a72e50f674 🐛 Fix SVG text rendering on thumbnails 2023-07-13 12:47:15 +02:00
Eva
965c4fe243 🐛 Fix create empty comments 2023-07-13 12:45:01 +02:00
Eva
13b1762873 🐛 Fix exports menu on viewer mode 2023-07-13 12:45:01 +02:00
Eva
ee73384993 🐛 Fix create typography with section closed 2023-07-13 12:45:01 +02:00
Eva
a940c7e912 🐛 Fix onboarding modal height 2023-07-13 12:44:59 +02:00
Andrey Antukh
875a3cf63c 🐛 Fix bad interaction of file migrations components-v2 and pointer-map feature 2023-07-13 12:19:22 +02:00
Andrey Antukh
8eb64de062 Merge remote-tracking branch 'origin/staging' into develop 2023-07-13 12:13:06 +02:00
Pablo Alba
119b3a405c 🐛 Fix duplicate page with comnponents duplicates the components 2023-07-13 11:42:31 +02:00
Pablo Alba
62cb7e21b8 Merge pull request #3413 from penpot/niwinz-develop-bugfixes
🐛 Fix selection bug on path edition
2023-07-13 11:41:23 +02:00
Alejandro Alonso
fc018b18b3 🐛 Fix rotate several elements in bulk 2023-07-13 11:28:17 +02:00
Aitor
f57ed6a763 Set smooth/instant autoscroll depending on distance 2023-07-13 10:52:49 +02:00
Andrey Antukh
ee7c3ece75 🐛 Fix selection bug on path edition 2023-07-13 10:50:39 +02:00
Eva Marco
233b9a7951 Merge pull request #3411 from penpot/niwinz-fix-css-macros
🐛 Fix CSS related macros backward compatibility
2023-07-13 07:54:03 +02:00
Andrey Antukh
52b7328ef5 💄 Fix indentation on workspace left toolbar ns 2023-07-12 15:26:12 +02:00
Andrey Antukh
b6e9ea1d60 🐛 Fix backward compatibility of css related macros 2023-07-12 15:24:48 +02:00
Aitor Moreno
8b7f791509 Merge pull request #3400 from penpot/alotor-bugfixes-4
Bugfixes
2023-07-12 13:11:16 +02:00
alonso.torres
369192a353 🐛 Locks shapes when moved inside a locked parent 2023-07-12 13:06:42 +02:00
alonso.torres
1b0a6b26ce 🐛 Fix problem with bool contents 2023-07-12 13:06:42 +02:00
alonso.torres
fc35b0b853 🐛 Fix retrieve user comments in dashboard 2023-07-12 13:06:42 +02:00
alonso.torres
872648d393 🐛 Fix new-file button on project not redirecting to the new file 2023-07-12 13:06:42 +02:00
alonso.torres
5631204567 🐛 Fix paste elements at bottom of frame 2023-07-12 13:06:42 +02:00
alonso.torres
9f121cb38b 🐛 Fix problem with comments not sticking 2023-07-12 13:06:42 +02:00
alonso.torres
5072c903c5 🐛 Fix bad frame-id for certain componentes 2023-07-12 13:06:42 +02:00
alonso.torres
66559d3ce3 🐛 Fix error screen on image upload failure 2023-07-12 13:06:42 +02:00
alonso.torres
7e0a612818 🐛 Fix problem when sliding color picker in selected-colors 2023-07-12 13:06:40 +02:00
Alejandro
e9ce327eef Merge pull request #3390 from penpot/hiru-fix-overlay
Fix several bugs related to interaction overlays
2023-07-12 10:57:18 +02:00
Andrés Moya
491251f5ce 🐛 Fix overlay position with elements fixed when scrolling 2023-07-12 09:46:46 +02:00
Andrés Moya
65598aa724 🐛 Fix overlay position when it has shadow or blur 2023-07-12 09:46:46 +02:00
Andrés Moya
e563611c05 🐛 Fix overlay close from an artboard 2023-07-12 09:46:46 +02:00
Andrés Moya
a2d1ce8120 🐛 Fix overlay position in open-overlay 2023-07-12 09:46:45 +02:00
Alejandro
9713f2859f Merge pull request #3322 from penpot/niwinz-performance-custom-rect
 Performance enhancements (part 1)
2023-07-12 07:20:43 +02:00
Andrey Antukh
42aee56c36 💄 Add indentation fixes on frontend tests 2023-07-11 17:27:36 +02:00
Andrey Antukh
dae5e71fa1 Mark new or updated files with new features
for avoid crossversion modifications
2023-07-11 17:27:36 +02:00
Andrey Antukh
dfc2ab56a9 💄 Fix code style consistency on schema declarations on file ns 2023-07-11 17:27:36 +02:00
Andrey Antukh
ab0245279f ♻️ Refactor (again) numeric input component 2023-07-11 17:27:36 +02:00
Andrey Antukh
e96d129ee8 💄 Add minor cosmetic change on workspace drawing ns 2023-07-11 17:27:36 +02:00
Andrey Antukh
42fe47e5f1 Make the frame-id and parent-id always initialized on shape 2023-07-11 17:27:36 +02:00
Andrey Antukh
f246de82f4 💄 Add cosmetic changes to measures menu component 2023-07-11 17:27:36 +02:00
Andrey Antukh
810abe6728 🐛 Fix bug related to path shape initialization 2023-07-11 17:27:35 +02:00
Andrey Antukh
2c61cfd139 Optimize content->points helper 2023-07-11 17:27:35 +02:00
Andrey Antukh
e833e29bd4 📎 Add arity-0 to make-rect function 2023-07-11 17:27:35 +02:00
Andrey Antukh
8dfebc39fe 🔥 Remove duplicate code 2023-07-11 17:27:35 +02:00
Andrey Antukh
fbf89d7f6c Add tests for record macro 2023-07-11 17:27:35 +02:00
Andrey Antukh
0b4b14af9e Add optimized version of apply-transform
using internal mutation
2023-07-11 17:27:35 +02:00
Andrey Antukh
723aab6b80 Use positional constructor for matrix 2023-07-11 17:27:35 +02:00
Andrey Antukh
3ab67e4545 Add lightweight optimization to modifiers handling
Mainly using controlled internal mutation when is possible
2023-07-11 17:27:35 +02:00
Andrey Antukh
4a4423da70 Add micro optimization to cph/root? predicate
accessing directly to the prop instead of using
the lookup operation
2023-07-11 17:27:35 +02:00
Andrey Antukh
8d46271e9d Avoid unnecesary call on math helper 2023-07-11 17:27:35 +02:00
Andrey Antukh
a15a2010b6 Add huge optimization to the transform-points-matrix
it reduces the 90% overhead of this function; in an relative
comparison the same execution is reduced from 350ms to 18ms
2023-07-11 17:27:35 +02:00
Andrey Antukh
4d3064ba6d 💄 Add minor cosmetic improvements to geom shape pixel precision code 2023-07-11 17:27:35 +02:00
Andrey Antukh
0e513f950a 💄 Add minor cosmetic changes to geom shape contraints code 2023-07-11 17:27:35 +02:00
Andrey Antukh
8723116230 Add some minor optimizations to geom shape common helpers 2023-07-11 17:27:35 +02:00
Andrey Antukh
819c7ea814 Add micro optimization to handle-area-selection event impl 2023-07-11 17:27:35 +02:00
Andrey Antukh
23d358aea7 💄 Add cosmetic changes on viewport hooks and actions 2023-07-11 17:27:35 +02:00
Andrey Antukh
ea5b153578 Use new defrecord for geom data structures 2023-07-11 17:27:35 +02:00
Andrey Antukh
3f14308908 Move fressian and transit impl for geom objects to respective nss 2023-07-11 17:27:35 +02:00
Andrey Antukh
f7801f9450 💄 Add minor cosmetic change to dm/get-prop macro impl 2023-07-11 17:27:35 +02:00
Andrey Antukh
f6e9c398b0 Improve performance of absolute-move function 2023-07-11 17:27:35 +02:00
Andrey Antukh
1ddea076e3 Reduce allocation on translate-*-frame functions 2023-07-11 17:27:35 +02:00
Andrey Antukh
121188d921 📎 Update frontend bench tools 2023-07-11 17:27:35 +02:00
Andrey Antukh
7fa24fdc2f 🐛 Fix issues on converting graphics to components 2023-07-11 17:27:35 +02:00
Andrey Antukh
ea47ce30df 💄 Add cosmetic improvements to align-objects event 2023-07-11 17:27:35 +02:00
Andrey Antukh
9b477ca0eb 🐛 Fix issue on transforms/move function related to path shapes
Where shape contains nils for x and y coords
2023-07-11 17:27:35 +02:00
Andrey Antukh
daeaf1548b Add minor performance enhancements to layers-toolbox component 2023-07-11 17:27:35 +02:00
Andrey Antukh
0bc468f434 Optimize layer-item component 2023-07-11 17:27:35 +02:00
Andrey Antukh
f3b856b2af Improve performance and usability of new css styles 2023-07-11 17:27:35 +02:00
Andrey Antukh
b65452cb73 Add performance improvements to use-search hook on layers 2023-07-11 17:27:35 +02:00
Andrey Antukh
0102ca1bcf Add performance improvements to layer-name component 2023-07-11 17:27:35 +02:00
Andrey Antukh
6a1c32bb71 Use native props destructuring on measures menu 2023-07-11 17:27:35 +02:00
Andrey Antukh
03271ce3fc 💄 Add cosmetic improvements on rect options sidebar 2023-07-11 17:27:35 +02:00
Andrey Antukh
6e7595f48c ♻️ Remove ? char from shape attrs 2023-07-11 17:27:35 +02:00
Andrey Antukh
405aa66357 🎉 Add new shape & rect data structures
Also optimizes some functions for faster shape and rect props
access (there is still a lot of work ahead optimizing the rest of
the functions)

Also normalizes shape creation and validation for ensuring
correct setup of all the mandatory properties.
2023-07-11 17:27:35 +02:00
Andrey Antukh
9f5640c1db 📎 Add kondo config for new defrecord macro 2023-07-11 17:27:35 +02:00
Andrey Antukh
c32b1860c4 🎉 Add custom defrecord macro implementation 2023-07-11 17:27:31 +02:00
Aitor Moreno
91037caa55 Merge pull request #3406 from penpot/eva-bugfixing-10
🐛 Fix several frontend errors
2023-07-11 16:21:12 +02:00
Eva
b94885a764 🐛 Fix shortcut translation 2023-07-11 13:31:59 +02:00
Eva
52545692df 🐛 Fix border radius values with decimals 2023-07-11 13:31:59 +02:00
Eva
3dcd640a99 🐛 Fix search bar width on layer tab 2023-07-11 13:31:59 +02:00
Eva
2e461b3070 🐛 Fix text menu order on design tab 2023-07-11 13:31:59 +02:00
Eva
41924246aa 🐛 Fix text decoration on button 2023-07-11 13:31:58 +02:00
Alejandro
2b37a3c613 Merge pull request #3405 from penpot/niwinz-bugfixes-2023-w26-2
🐛 Bugfixes & Enhancements
2023-07-11 12:55:27 +02:00
Alejandro
d0e407bfea Merge pull request #3399 from penpot/juan-toolbar-redesign
💄 Toolbar redesign
2023-07-11 12:48:18 +02:00
Andrey Antukh
f30ba5876e Add performance oriented changes to dashboard teams section 2023-07-11 12:00:16 +02:00
Andrey Antukh
23c8043f34 🐛 Fix incorrect message on sending invitation to a member 2023-07-11 12:00:16 +02:00
Alejandro
a6fc60a88d Merge pull request #3403 from penpot/palba-fix-library-backup-order
🐛 Fix library backup assets order
2023-07-11 11:12:39 +02:00
Alejandro Alonso
d3b5d577fd Merge remote-tracking branch 'origin/staging' into develop 2023-07-11 10:46:32 +02:00
Alejandro
3c9d3bd5af Merge pull request #3404 from penpot/superalex-fix-select-text-javascript-function
🐛 Fix select text javascript function
2023-07-11 10:42:14 +02:00
Alejandro Alonso
8e1c4238cb 🐛 Fix select text javascript function 2023-07-11 10:17:39 +02:00
Pablo Alba
2d57523e00 Merge pull request #3402 from penpot/superalex-bugfixing-16
🐛 Alex bugfixing
2023-07-11 08:10:56 +02:00
Pablo Alba
8e0c6da1d6 🐛 Fix library backup assets order 2023-07-11 08:05:56 +02:00
Eva
481c67b1f8 💄 Toolbar redesign 2023-07-11 07:56:14 +02:00
Alejandro Alonso
8007794cba 🐛 Fix dissolve interaction 2023-07-11 07:45:29 +02:00
Alejandro Alonso
8b81f700a5 Refactor select all on pending numeric input 2023-07-11 07:45:12 +02:00
Alejandro
b8dbd16b01 Merge pull request #3397 from penpot/juan-history-redesign
💄  History panel redesign
2023-07-11 06:52:40 +02:00
Alejandro
ea753da0ae Merge pull request #3401 from penpot/niwinz-bugfixes-2023-w26-2
🐛 Bugfixes
2023-07-10 15:19:49 +02:00
Alejandro
6539b7da5b Merge pull request #3396 from penpot/alotor-grid-layout
First grid layout version
2023-07-10 15:08:14 +02:00
Andrey Antukh
d1a7c58c53 Report error on something goes wrong on image processing 2023-07-10 15:07:17 +02:00
Andrey Antukh
e5a7edeaf6 Always fetch fresh library templates 2023-07-10 15:07:17 +02:00
Andrey Antukh
d0a422e8bd 💄 Add cosmetic improvement to backend main ns 2023-07-10 15:07:17 +02:00
Andrey Antukh
7ea92529f9 Make template thumbnails available offline 2023-07-10 15:07:17 +02:00
Andrey Antukh
494c585e2f Make builtin templates download ondemand if cache is not present 2023-07-10 15:07:17 +02:00
alonso.torres
da9fa31c27 Adds grid to the actibable features 2023-07-10 14:56:15 +02:00
alonso.torres
ac184a7c8f Improved codegen 2023-07-10 14:49:25 +02:00
alonso.torres
30d78554c2 Improved code generation 2023-07-10 14:49:25 +02:00
alonso.torres
cb502fc70d Improved code gen 2023-07-10 14:49:25 +02:00
alonso.torres
ecc3b29996 Fix problem with rotated layers 2023-07-10 14:49:25 +02:00
alonso.torres
a70d909a25 Show grid layout on component thumbnails and empty grids 2023-07-10 14:49:25 +02:00
alonso.torres
68c85c8fa5 Changes to transform 2023-07-10 14:49:25 +02:00
alonso.torres
61573dcef5 🐛 Fix problem with validation 2023-07-10 14:49:25 +02:00
alonso.torres
704421fa1f 🐛 Fix scroll problem 2023-07-10 14:49:25 +02:00
alonso.torres
b3482c1d6a 🐛 Fix problem with space-between and only one track 2023-07-10 14:49:25 +02:00
alonso.torres
34575b9413 Resize inspect on viewer 2023-07-10 14:49:25 +02:00
alonso.torres
3741a65276 Moved text styles to css when generating code 2023-07-10 14:49:25 +02:00
alonso.torres
a2c59acfa9 Update info panel 2023-07-10 14:49:25 +02:00
alonso.torres
c3a8c3826d Changes to edit UI 2023-07-10 14:49:25 +02:00
alonso.torres
e01af790f3 Add copy all code button 2023-07-10 14:49:25 +02:00
alonso.torres
600b1a6d8d Improved code generation 2023-07-10 14:49:25 +02:00
alonso.torres
4b8783c104 🐛 Fix problem with paste objects 2023-07-10 14:49:25 +02:00
alonso.torres
9b8ef35603 Grid layers order 2023-07-10 14:49:25 +02:00
alonso.torres
e86939b8ee Improved flex tracks behavior and auto sizing 2023-07-10 14:49:24 +02:00
alonso.torres
06ab577e41 More improvements to layout grid UI 2023-07-10 14:49:24 +02:00
alonso.torres
b13db69cf9 Grid layout polishing 2023-07-10 14:49:24 +02:00
alonso.torres
03c64303f5 Support rotated UI 2023-07-10 14:49:24 +02:00
alonso.torres
b83c35b0dd Refresh grid cells after change static/absolute item 2023-07-10 14:49:24 +02:00
alonso.torres
7b410d46ec Editing on double click 2023-07-10 14:49:24 +02:00
alonso.torres
c0342a2c75 Adds cell to shape options 2023-07-10 14:49:24 +02:00
alonso.torres
f920d4213e Fix problem with zoom 2023-07-10 14:49:24 +02:00
alonso.torres
0c1e83e4a6 Fix problem with effects 2023-07-10 14:49:24 +02:00
alonso.torres
0358eb51e8 Change behavior on empty grid creation 2023-07-10 14:49:24 +02:00
alonso.torres
cf4e2f91d1 Grid layout polishing 2023-07-10 14:49:24 +02:00
alonso.torres
0e152bb7f9 Paste on position in grid 2023-07-10 14:49:24 +02:00
alonso.torres
714b2c8805 Remove tracks update multispan cells 2023-07-10 14:49:24 +02:00
alonso.torres
b0136fef29 🐛 Fix problem with fill width/height and alignment 2023-07-10 14:49:24 +02:00
alonso.torres
b3b984d339 Add import/export svg for grid 2023-07-10 14:49:24 +02:00
alonso.torres
664825a2a6 Fix specs for grid layout 2023-07-10 14:49:24 +02:00
alonso.torres
7e7b642e20 Move objects in grid with keys 2023-07-10 14:49:24 +02:00
alonso.torres
c9b932f954 Position absolute in grid layout 2023-07-10 14:49:24 +02:00
alonso.torres
117a8d09d3 Add space-between/space-around/space evenly to grids 2023-07-10 14:49:24 +02:00
alonso.torres
2177b7ae13 Improved auto/flex size assignment 2023-07-10 14:49:24 +02:00
alonso.torres
8671e9cf8a Child element options 2023-07-10 14:49:24 +02:00
alonso.torres
1c4678ad5d Update grid on child changes 2023-07-10 14:49:24 +02:00
alonso.torres
c31dc94496 Align items in grid layout 2023-07-10 14:49:24 +02:00
alonso.torres
47e927d571 Change column/row from cell options 2023-07-10 14:49:24 +02:00
alonso.torres
f5bb6b05f3 Add grid icons to layers 2023-07-10 14:49:24 +02:00
alonso.torres
5925d2520f Changes to the editor UI 2023-07-10 14:49:24 +02:00
alonso.torres
3c8934e847 Fill size for grid children 2023-07-10 14:49:24 +02:00
alonso.torres
0195165de0 Resize tracks from editor 2023-07-10 14:49:24 +02:00
alonso.torres
4bd15b5de1 Adds child layout options to grid children 2023-07-10 14:49:24 +02:00
alonso.torres
cdebf245e3 Multispan cells auto sizing 2023-07-10 14:49:24 +02:00
alonso.torres
0eff2e8887 Support for multi-track span in cells 2023-07-10 14:49:24 +02:00
alonso.torres
43d1f676ef Move shapes in grid 2023-07-10 14:49:24 +02:00
alonso.torres
2df40ad767 Adds grid column/row sizing without spanned tracks 2023-07-10 14:49:24 +02:00
alonso.torres
4bfe81f771 Enable grid editor 2023-07-10 14:49:24 +02:00
Andrey Antukh
0268964f36 Merge remote-tracking branch 'origin/staging' into develop 2023-07-10 14:47:19 +02:00
Andrey Antukh
02b41abaf8 Improve builtin template fetching management 2023-07-10 13:58:45 +02:00
Andrey Antukh
a665339c98 ♻️ Move dashboard libraries templates to other namespace
And refactor its internal state management
2023-07-10 13:58:45 +02:00
Alejandro
9c0e594294 Merge pull request #3388 from penpot/niwinz-bugfixes-2023-w26-2
 Add backward compatibility layer for v1.20 and other fixes
2023-07-10 12:48:43 +02:00
Andrey Antukh
ad53d0b55a 🐛 Update project modified-at field after file import 2023-07-10 12:44:24 +02:00
Andrey Antukh
decaeda2fe 🐛 Set bigger maximum token length on backend validation 2023-07-10 12:44:24 +02:00
Andrey Antukh
60130d4db2 🐛 Use correct fullname after OICD registration process 2023-07-10 12:44:24 +02:00
Andrey Antukh
f85a9011ee 🐛 Fix excessive data fetching on workspace comments 2023-07-10 12:44:24 +02:00
Andrey Antukh
9dbf6ffd14 🐛 Fix focus handling on comment edition 2023-07-10 12:44:24 +02:00
Andrey Antukh
992dd04b47 💄 Add cosmetic improvements to comments ns 2023-07-10 12:44:24 +02:00
Andrey Antukh
010a3ef3a7 💄 Add minor cosmetic chanes to workspace comments ns 2023-07-10 12:44:24 +02:00
Andrey Antukh
3da0d85d8f 🐛 Set correct project modified-at on moving files between projects
Happens when you use drag and drop on dashboard for moving files between
projects, but also if you use a context menu actions
2023-07-10 12:44:22 +02:00
Andrey Antukh
7a837110f0 Add proper on-accept callback on features related restriction error
Which redirects user to the dashboard if the team-id and project-id
is available in stante; if not just flushes hard refresh
2023-07-10 12:44:07 +02:00
Andrey Antukh
09d28d8583 Add better file feature handling on file retrieval 2023-07-10 12:44:07 +02:00
Andrey Antukh
90f5b4b631 Qualify json encoding warning log messages as errors 2023-07-10 12:44:07 +02:00
Alejandro
52ad26d4e7 Merge pull request #3391 from penpot/alotor-bugfixes-3
Alotor bugfixes 3
2023-07-10 12:39:48 +02:00
Eva
a77d82883f 💄 History panel redesign 2023-07-10 12:34:12 +02:00
Andrey Antukh
5c92ad727d Merge pull request #3398 from penpot/superalex-fix-nginx-locations-with-regex
🐛 Fix nginx locations with regex
2023-07-10 12:22:22 +02:00
Alejandro Alonso
7823a3270a 🐛 Fix nginx locations with regex 2023-07-10 12:00:29 +02:00
Eva
1ff08bfe6a 💄 Make small visual changes on assets tab 2023-07-10 10:53:27 +02:00
alonso.torres
b565e20f1a 🐛 Fix problem with slashes in layers names for exporter 2023-07-10 09:56:06 +02:00
alonso.torres
735170debf 🐛 Fix problem with HSV color picker 2023-07-10 09:56:06 +02:00
alonso.torres
a2fbf93ec1 🐛 Fix problem with importation process 2023-07-07 14:15:14 +02:00
Andrey Antukh
7b887d3188 Merge pull request #3389 from penpot/superalex-bugfixing-15
🐛 bugfixing
2023-07-07 13:03:15 +02:00
Alejandro Alonso
c1dd4e5e6f 🐛 Fix popup 'Create a group' appears each time after single graphics is moving into already existed group 2023-07-07 13:01:56 +02:00
Alejandro Alonso
7d7b4074b2 🐛 Fix picking a gradient color in recent colors for a new color in the assets tab 2023-07-07 13:01:56 +02:00
Alejandro Alonso
51462ba476 🐛 Fix finalize editor state to consider existing position-data 2023-07-07 12:00:23 +02:00
Alejandro Alonso
99693f0fc2 🐛 Fix cut/delete text layer when while creating text 2023-07-07 12:00:22 +02:00
Andrey Antukh
fdbabe49df Merge pull request #3382 from penpot/alotor-bugfixes-2
Bugfixes
2023-07-07 10:54:48 +02:00
alonso.torres
996a614ed7 🐛 Fix grid not being cutted in frames 2023-07-07 10:18:28 +02:00
alonso.torres
7a499bfc90 🐛 Fix problem with images patterns repeating 2023-07-07 10:18:28 +02:00
alonso.torres
647beec1e8 🐛 Fix problem with comments when user left the team 2023-07-07 10:18:28 +02:00
alonso.torres
dd9f637f02 🐛 Fix problem with comments mode not staying 2023-07-07 10:18:28 +02:00
alonso.torres
00450565c8 🐛 Makes height priority for the rows/columns grids 2023-07-07 10:18:27 +02:00
Alejandro Alonso
43dfdbb374 Merge remote-tracking branch 'origin/staging' into develop 2023-07-07 08:49:06 +02:00
Alejandro Alonso
cf9fb7face 🐛 Fix 404 errors 2023-07-06 19:00:10 +02:00
Alejandro Alonso
bd4b4d23b1 Merge remote-tracking branch 'origin/staging' into develop 2023-07-06 18:31:49 +02:00
Alejandro
44514a0961 Merge pull request #3383 from penpot/niwinz-bugfixes-2023-w26-2
🐛 Bugfixes
2023-07-06 18:27:04 +02:00
Alejandro Alonso
bfc490bd63 🐛 Fix 404 errors 2023-07-06 15:22:55 +02:00
Andrey Antukh
1b387e9fc7 📎 Fix minor issue on CHANGES.md file 2023-07-06 13:54:08 +02:00
Andrey Antukh
4561a87450 Merge remote-tracking branch 'origin/staging' into develop 2023-07-06 13:52:23 +02:00
Andrey Antukh
0a9cad76c3 💄 Add minor cosmetic improvements to typography menu components 2023-07-06 12:46:51 +02:00
Andrey Antukh
26ef8df79c ⬆️ Update frontend dependencies (only bugfixes) 2023-07-06 12:46:51 +02:00
Andrey Antukh
cd2f50fdb4 🐛 Fix react warnings on font-selector 2023-07-06 12:46:51 +02:00
Andrey Antukh
59d02314e2 ⬆️ Update google fonts 2023-07-06 12:46:51 +02:00
Andrey Antukh
88ac27788b 🐛 Fix whitespace handling on color assets name 2023-07-06 12:46:51 +02:00
Andrey Antukh
c16de52b49 ♻️ Add minor refactor to shared-link dialog component
Fixes the issue of creating incorrect link when only non-current pages
are selected on the shared link permissions
2023-07-06 12:46:51 +02:00
Andrey Antukh
8d6d589a0c 💄 Add minor cosmetic change to viewer-page component 2023-07-06 12:29:33 +02:00
Andrey Antukh
0817c4e140 Print js trace on exceptional state error is raised 2023-07-06 12:29:33 +02:00
Andrey Antukh
aad70d9df8 💄 Add minor cosmetic improvement to viewer events ns 2023-07-06 12:29:33 +02:00
Andrey Antukh
bbcf9c00a5 🐛 Remove conditional cache handling from get-view-only-bundle rpc method
The cond/etag handling is the cause of incorrect number of shared links
returned by the endpoint. Because of incorrect cache invalidation.
2023-07-06 12:29:33 +02:00
Eva
49df4a9404 🐛 Fix several frontend validations 2023-07-06 12:28:47 +02:00
Eva
acfeae8638 🐛 Fix select all checkbox on shared link config 2023-07-06 12:28:47 +02:00
Eva
7216a514e6 🐛 Fix context menu z-index 2023-07-06 12:28:47 +02:00
Eva
48d9541d46 🐛 Fix scroll of comment list on viewer 2023-07-06 12:28:47 +02:00
Alejandro Alonso
01ec22d662 🐛 Fix finalize text editor state when blur 2023-07-05 13:22:50 +02:00
Alejandro Alonso
b43d09e5ce 🐛 Fix email change validation 2023-07-05 13:22:50 +02:00
Alejandro Alonso
009236bbe3 🐛 Fix export from shared prototype 2023-07-05 13:22:50 +02:00
Alejandro Alonso
0d87dc5680 🐛 Fix drag and drop in the dashboard generates import file error message 2023-07-05 13:22:50 +02:00
Alejandro
8b0339bbab Merge pull request #3379 from penpot/alotor-bugfixes
Alotor bugfixes
2023-07-05 10:58:48 +02:00
alonso.torres
302bfd3007 🐛 Fix problems with locked frames 2023-07-05 08:44:59 +02:00
alonso.torres
302750bd7e 🐛 Fix issue with paths line to curve and concurrent editing 2023-07-05 08:10:54 +02:00
alonso.torres
66e32e9cbd 🐛 Fix problem with selection shortcuts 2023-07-05 08:10:50 +02:00
alonso.torres
e40245e187 🐛 Fixed problem with styles inside def for svg import 2023-07-05 08:09:48 +02:00
Alejandro
16854e7e83 Merge pull request #3376 from penpot/niwinz-bugfixes-2023-w26-2
🐛 Bugfixes
2023-07-05 06:25:37 +02:00
Kristijan Žic
53ed1404e7 🌐 Add translations for: Croatian.
Currently translated at 84.9% (1027 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2023-07-05 04:17:05 +02:00
Andrey Antukh
5a8df0dfae Add better validation of profile rpc methods 2023-07-04 19:28:52 +02:00
Andrey Antukh
8f8d90abbc Revert some changes to the audit validation 2023-07-04 19:28:52 +02:00
Alejandro
bf297539ae Merge pull request #3374 from penpot/niwinz-bugfixes-2023-w26-2
🐛 Bugfixes
2023-07-04 16:27:17 +02:00
Eva
fe8f13ed57 Add new palette UI 2023-07-04 15:35:45 +02:00
Andrey Antukh
be652b909e Add stronger validationt to auth/register rpc methods 2023-07-04 14:36:31 +02:00
Andrey Antukh
068d2f13f4 Add min-max validation to word-string schema 2023-07-04 13:55:58 +02:00
Andrey Antukh
1464f5da90 Ensure that all emails are under 250chars 2023-07-04 13:55:58 +02:00
Andrey Antukh
7b0d3bdcab Add stricter validation on events endpoint 2023-07-04 13:55:58 +02:00
Alejandro
5d42631c7a Merge pull request #3370 from penpot/niwinz-improvements
 Add some improvements to the oidc module
2023-07-04 12:36:39 +02:00
Andrey Antukh
e0c0b251a9 💄 Add minor cosmetic change to CHANGES.md file 2023-07-04 11:19:19 +02:00
Andrey Antukh
a868dcf8e6 🐛 Don't allow empty strings and whitespace-only strings on media name 2023-07-04 11:19:19 +02:00
Andrey Antukh
b64a9f0cf4 🐛 Fix graphic item rename on assets pannel 2023-07-04 11:19:19 +02:00
Alejandro
45a909f5ff Merge pull request #3371 from penpot/niwinz-bugfixes-2023-w26
🐛 Don't allow empty or whitespace-only names on components
2023-07-04 06:52:05 +02:00
Andrey Antukh
dcc15e485d 🐛 Don't allow empty or whitespace-only names on components 2023-07-03 17:03:18 +02:00
Alejandro
6849a5b0e0 Merge pull request #3357 from penpot/eva-bugfixin-8
🐛 Fix some bugs
2023-07-03 14:04:42 +02:00
Eva
ef3fedee59 🐛 Fix some warnings and format some files 2023-07-03 13:58:58 +02:00
Eva
8955f87d5a 🐛 Fix z-index nillable input when static position 2023-07-03 13:58:57 +02:00
Eva
94b5c98042 🐛 Fix context menu outside screen 2023-07-03 13:58:40 +02:00
Eva
82183ec71a 🐛 Fix create and account only with spaces 2023-07-03 13:58:22 +02:00
Eva
e75b53ff8d 🐛 Fix search font visualitation 2023-07-03 13:58:01 +02:00
Eva
9a880f007c 🐛 Fix focus title on layers sidebar 2023-07-03 13:57:48 +02:00
Eva
02466d603c 🐛 Fix allow team name to be all blank 2023-07-03 13:57:47 +02:00
Eva
4d4e9703cc 🐛 Fix drag projects on dahsboard 2023-07-03 13:57:30 +02:00
Eva
a737c125d5 🐛 Fix unpublish more than one library at the same time 2023-07-03 13:57:15 +02:00
Alejandro Alonso
56bee7dd7c 📎 Update CHANGES.md file and version.txt 2023-07-03 13:33:49 +02:00
Alejandro Alonso
d809b972ec Merge remote-tracking branch 'origin/staging' into develop 2023-07-03 13:32:48 +02:00
Alejandro Alonso
e461745479 📎 Update CHANGES.md file and version.txt 2023-07-03 13:32:36 +02:00
Andrey Antukh
8cda8924df Add the ability to select user info source
using the PENPOT_OIDC_USER_INFO_SOURCE environment variable
with two possible values: token and userinfo
2023-07-03 10:46:29 +02:00
Andrey Antukh
dda67af5cc Update oidc impl with latest buddy-sign improvements 2023-07-03 10:46:25 +02:00
Andrey Antukh
cadcc1607d Increase default argon2id iterations 2023-07-03 10:43:26 +02:00
Andrey Antukh
63c8798264 ⬆️ Update backend and common dependencies 2023-07-03 10:43:26 +02:00
Alejandro Alonso
d22c47fc50 Merge remote-tracking branch 'origin/staging' into develop 2023-07-03 09:38:18 +02:00
Alejandro Alonso
74dd4f1ff8 🐛 Fix text content validation 2023-07-03 09:35:55 +02:00
Alejandro Alonso
53cee87701 🐛 Fix deleted fonts present in recent block 2023-07-03 09:35:55 +02:00
Alejandro Alonso
d939a86e75 🐛 Fix null vlaues for grid columns/rows 2023-07-03 09:28:57 +02:00
elhombretecla
38f1e9338a Update README.md 2023-07-03 08:57:53 +02:00
elhombretecla
da19544cbe Update README.md 2023-07-03 08:57:38 +02:00
elhombretecla
711d63c51e Update README.md 2023-07-03 08:55:59 +02:00
elhombretecla
844a9cfbe2 Update README.md 2023-07-03 08:55:04 +02:00
Linerly
f691f8d5b5 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2023-07-02 19:52:28 +02:00
Amine Gdoura
2c68e8309e 🌐 Add translations for: Arabic.
Currently translated at 61.4% (743 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2023-07-02 19:52:27 +02:00
Amerey.eu
dce8b5b37c 🌐 Add translations for: Czech.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2023-07-01 14:52:54 +02:00
Mikel Larreategi
6546bfc889 🌐 Add translations for: Basque.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2023-07-01 14:52:52 +02:00
Linerly
b915abb2d2 🌐 Add translations for: Indonesian.
Currently translated at 97.0% (1173 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2023-07-01 14:52:51 +02:00
Alejandro Alonso
1afdbcfbaa Merge remote-tracking branch 'origin/staging' into develop 2023-06-28 12:49:26 +02:00
Alejandro
050646506e Merge pull request #3358 from penpot/niwinz-oidc-improvements
 Add the ability to parse OIDC JWT token
2023-06-28 10:17:11 +02:00
Andrey Antukh
6339b07fba Add the ability to parse OIDC JWT token
If jwks-uri is provided or properly discovered, they will be used
for unsign JWT token and get use info data from that token instead
of making an additional call to the userinfo endpoint
2023-06-28 00:25:48 +02:00
Pablo Alba
e61aaaecf3 🐛 Fix libraries are truncated on 'Libraries' page 2023-06-27 14:47:23 +02:00
Alejandro Alonso
a3ab524a8a Merge remote-tracking branch 'origin/staging' into develop 2023-06-27 14:12:44 +02:00
Alejandro
3ea5b1a8de Merge pull request #3356 from penpot/superalex-fix-file-etag-calculation
🐛 fix file etag calculation
2023-06-27 14:10:50 +02:00
Andrey Antukh
17731db28b 🐛 Fix file etag calculation considering the profile id too 2023-06-27 13:55:55 +02:00
Aitor
5b40fdf3f0 🔧 Add VSCode settings 2023-06-27 13:54:07 +02:00
Alejandro Alonso
9ab067b6d8 🐛 Fix add group to graphics and components 2023-06-27 13:53:17 +02:00
Pablo Alba
2648dc3d27 🐛 Fix menu for create annotation appears on components that already have annotation 2023-06-27 13:31:12 +02:00
Pablo Alba
9d06a34df4 🐛 Fix Annotation is not shown on View mode 2023-06-27 13:17:40 +02:00
Pablo Alba
201f6ed96a 🐛 Fix libraries are truncated on 'Libraries' page 2023-06-27 13:16:56 +02:00
Pablo Alba
1770bb995b 🐛 Fix annotations size: adjust textarea height according to annotation content 2023-06-27 13:08:39 +02:00
Pablo Alba
85e1899f6b 🐛 Fix '(...)' is truncated for 'Typographies' section in Library view 2023-06-27 12:47:22 +02:00
Pablo Alba
0716aaeff6 🐛 Fix missing view for empty library on Libraries page 2023-06-27 12:47:22 +02:00
Alejandro Alonso
af114ee9d0 Merge branch 'astudentinearth-astudentinearth-change-radius-tooltips' into staging 2023-06-27 10:53:09 +02:00
Alejandro Alonso
2249bf9745 📎 Update CHANGES.md file 2023-06-27 10:52:56 +02:00
astudentinearth
c3c6112ade 🐛 Change independent corner radius input tooltips
Make the inputs show a tooltip for the relevant corner(e.g. "Top left") instead of "Radius"

Signed-off-by: Burak Yeniçeri <burak.yn.dev@gmail.com>
2023-06-27 10:52:56 +02:00
Alejandro
5ea80c018f Merge pull request #3352 from penpot/niwinz-bugfixes
 Improvements & bugfixes
2023-06-27 10:40:30 +02:00
Alejandro Alonso
287213cfaf Refactor select all on input text click 2023-06-27 10:32:50 +02:00
Andrey Antukh
51d829a4b3 🐛 Fix incorrect handling of SSL param on email sending subsystem
Fixes #3213
2023-06-27 09:50:05 +02:00
Andrey Antukh
f166fe1926 🐛 Add proper validation of registration domain whitelist on oidc
Fixes #3348
2023-06-26 18:14:56 +02:00
Andrey Antukh
f60d09eb8f 🎉 Add uuid->short-id helper
Mainly helps encode a safer subset of bits (96) of an uuid using
a more compact encoding (base62) which is compatible with CSS and
URL's
2023-06-26 18:03:16 +02:00
Andrey Antukh
339903f567 🐛 Fix incorrect handling of error on thumbnail renderer 2023-06-26 14:51:49 +02:00
Andrey Antukh
7f16a79af5 🐛 Fix email printing to the logging subsystem
Fixes #3239
2023-06-26 11:16:37 +02:00
Andrey Antukh
97af5f71eb Merge branch 'staging' into develop 2023-06-26 10:21:34 +02:00
Andrey Antukh
ba4ef66cdc Merge branch 'main' into staging 2023-06-26 10:19:58 +02:00
Alejandro Alonso
7191fe847c Merge remote-tracking branch 'origin/staging' into develop 2023-06-26 09:49:54 +02:00
Alejandro
dad13ed826 Merge pull request #3350 from penpot/superalex-fix-internal-error-on-team-settings
🐛 Fix internal server error occurred when user wants to open team…
2023-06-26 09:49:34 +02:00
Alejandro Alonso
6cab413a8f 🐛 Fix internal server error occurred when user wants to open team settings 2023-06-26 09:41:08 +02:00
Alejandro
a895eaf61c Merge pull request #3347 from penpot/niwinz-fonts-local-caching
🐛 Fix several bugs related to fonts and components migration
2023-06-23 16:36:39 +02:00
Andrey Antukh
7977d75e3d Reduce the dashboard thumbnail size 2023-06-23 16:28:52 +02:00
Andrey Antukh
7746649eb8 🐛 Fix minor issues with fonts caching 2023-06-23 16:28:52 +02:00
Andrey Antukh
840801ea15 🐛 Don't update modified_at field on applying components migration 2023-06-23 16:28:52 +02:00
Andrey Antukh
cacaf2bf95 ⬆️ Update devenv dockerfile 2023-06-23 16:28:52 +02:00
Alejandro
4607d9f210 Merge pull request #3342 from penpot/niwinz-fonts-local-caching
 Add several improvements to fonts loading
2023-06-23 14:07:25 +02:00
Andrey Antukh
8f0a4e8333 🎉 Add local caching of gfonts styles 2023-06-23 13:32:38 +02:00
Andrey Antukh
ef5c9babe1 Merge remote-tracking branch 'origin/staging' into develop 2023-06-23 13:22:33 +02:00
Alejandro Alonso
f75b111564 🐛 Fix impossible to add group to typographies 2023-06-23 13:21:36 +02:00
Alejandro Alonso
a8e058ada6 🐛 Fix add asset color, invalid color appears 2023-06-23 13:21:36 +02:00
Alejandro Alonso
c988d54925 🐛 Fix hide rulers option not working 2023-06-23 13:21:36 +02:00
Alejandro
921ea61e6c Merge pull request #3344 from penpot/alotor-fix-viewer-scroll
🐛 Fix problem with scroll in viewer mode
2023-06-23 13:12:23 +02:00
Alejandro
71a6ee51fa Merge pull request #3343 from penpot/niwinz-onmpremise-improvements
 Add minor improvements for onpremise users
2023-06-23 13:10:14 +02:00
Andrey Antukh
b138550c0d 🐛 Fix issue on awsns http handler 2023-06-23 13:05:48 +02:00
Andrey Antukh
81658c90d1 Add the ability to disable dashboard templates section 2023-06-23 13:05:48 +02:00
alonso.torres
ca1e6c342f 🐛 Fix problem with scroll in viewer mode 2023-06-23 12:55:49 +02:00
Andrey Antukh
7feda98eb3 Add the ability to disable the google fonts provider 2023-06-23 12:55:22 +02:00
Alejandro
33e0e6293b Merge pull request #3341 from penpot/niwinz-bugfix-thumbnails
🐛 Fix thumbnails handling on dashboard libraries
2023-06-23 12:53:18 +02:00
Andrey Antukh
2a81d8563a 🐛 Fix thumbnails handling on dashboard libraries 2023-06-23 12:24:49 +02:00
Alejandro Alonso
ae9d6b627d Merge remote-tracking branch 'origin/staging' into develop 2023-06-22 14:38:12 +02:00
Alejandro
2db5925e60 Merge pull request #3337 from penpot/superalex-fix-text-fills-with-gradient
🐛 Fix text fills with gradient
2023-06-22 14:37:19 +02:00
Alejandro Alonso
d02f3ba011 🐛 Fix text fills with gradient 2023-06-22 14:08:21 +02:00
Alejandro
74e8081574 Merge pull request #3272 from penpot/azazeln28-thumbnail-renderer
🎉 Add thumbnail renderer service
2023-06-22 13:45:07 +02:00
Pablo Alba
1817d4ce38 🐛 It is possible to create empty component annotation (2) 2023-06-22 13:34:27 +02:00
Andrey Antukh
433b1b68c3 🐛 Improve fonts loading related to thumbnals rendering 2023-06-22 13:19:48 +02:00
Pablo Alba
776159c1e8 🐛 It is possible to create empty component annotation 2023-06-22 12:44:44 +02:00
Pablo Alba
45e76bc38b 🐛 Fix delete component annotation 2023-06-22 12:44:44 +02:00
Pablo Alba
54cee6ea72 🐛 Fix annotation is not duplicated together with main component 2023-06-22 12:44:44 +02:00
Pablo Alba
0ae4988908 🐛 Fix Internal server error occurred after clicking on '3 dots' menu of copy component on Design tab 2023-06-22 10:08:26 +02:00
Andrey Antukh
a97929992e Convert to schema some specs on file-thumbnails rpc methods 2023-06-22 09:34:13 +02:00
Alejandro Alonso
a53176489a 🐛 Fix extra line framing dashboard cards 2023-06-22 09:27:46 +02:00
Andrés Moya
d8121364ad 🐛 Fix touched on adding shapes to a component copy and undo 2023-06-22 09:27:27 +02:00
Alejandro Alonso
a66a952573 Merge remote-tracking branch 'origin/staging' into develop 2023-06-22 09:08:56 +02:00
Alejandro Alonso
d4fe810813 🐛 Fix shared link broken 2023-06-22 08:01:15 +02:00
Andrey Antukh
10205e51cc 🔥 Remove atom wrapping on several config props 2023-06-21 20:10:49 +02:00
Andrey Antukh
0aefd044dc Remove atom wrapping on public-uri 2023-06-21 20:10:49 +02:00
Andrey Antukh
d11b007795 Add thumbnail renderer
And integrate the dashboard thumbnails to use that service
2023-06-21 20:10:49 +02:00
Alejandro Alonso
5af2489315 Merge remote-tracking branch 'origin/staging' into develop 2023-06-21 17:06:47 +02:00
Alejandro Alonso
64ddfa0c31 📎 Update CHANGES.md file 2023-06-21 17:06:29 +02:00
Alejandro Alonso
6242c62bcb 📎 Update CHANGES.md file 2023-06-21 17:05:09 +02:00
Andrés Moya
e8dde477a5 🐛 Fix restore remote component 2023-06-21 17:04:46 +02:00
Alejandro Alonso
69969d9815 Merge remote-tracking branch 'origin/staging' into develop 2023-06-21 17:03:54 +02:00
Alejandro Alonso
1b0848389c 📎 Update CHANGES.md file 2023-06-21 17:03:42 +02:00
Pablo Alba
4f02cc3e86 Merge pull request #3331 from penpot/hiru-restore-comp-missing-lib
🐛 Disallow restore component when the library has been detached
2023-06-21 16:46:16 +02:00
Andrés Moya
749d60be48 🐛 Disallow restore component when the library has been detached 2023-06-21 16:39:17 +02:00
Alejandro Alonso
a0535de30c 📎 Update CHANGES.md file 2023-06-21 12:53:47 +02:00
Alejandro Alonso
bb8a523208 📎 Update CHANGES.md file 2023-06-21 12:52:13 +02:00
Alejandro Alonso
4d3e7f9a75 Merge remote-tracking branch 'origin/staging' into develop 2023-06-21 12:50:49 +02:00
Alejandro Alonso
9bd658661d Merge remote-tracking branch 'origin/staging' 2023-06-21 12:50:11 +02:00
Alejandro Alonso
2edbc10851 📎 Update CHANGES.md file 2023-06-21 12:50:04 +02:00
Alejandro Alonso
5fc303a05d Merge remote-tracking branch 'origin/staging' into develop 2023-06-21 12:45:54 +02:00
Alejandro Alonso
50bdad3450 Merge remote-tracking branch 'origin/staging' 2023-06-21 12:44:56 +02:00
Alejandro Alonso
9a45ce80a6 🐛 Fix comments navigation 2023-06-21 12:41:49 +02:00
Pablo Alba
3645d1af20 Merge pull request #3327 from penpot/superalex-fix-right-click-options-over-layer-or-shape
🐛 Fix right click options over layer or shape
2023-06-21 12:22:24 +02:00
Alejandro Alonso
d2bfd98a05 🐛 Fix right click options over layer or shape 2023-06-20 13:29:07 +02:00
Andrey Antukh
ecedf46c2a 📎 Add missing changelog entries for the 1.18.5 2023-06-20 11:21:09 +02:00
Andrey Antukh
73d42c03d5 Allow override the default nginx resolver
using the PENPOT_INTERNAL_RESOLVER environment variable
2023-06-20 11:21:09 +02:00
Pablo Alba
e96bedc1c8 🎉 Create multiple componentes 2023-06-20 11:07:33 +02:00
Aitor Moreno
c5f37fadba Merge pull request #3323 from penpot/alotor-fix-reload
 Not hotreload cursors
2023-06-19 16:15:59 +02:00
Aitor
8052c5f973 📎 Add [data-test] to page-items 2023-06-19 16:13:48 +02:00
Andrés Moya
c499c8a323 🐛 Small fix 2023-06-19 16:09:16 +02:00
alonso.torres
6b9962b2b3 Not hotreload cursors 2023-06-19 14:57:51 +02:00
Eva Marco
0a81ae1ea0 Merge pull request #3313 from penpot/azazeln28-fix-cursors
🐛 Fix creation cursors not being displayed
2023-06-19 13:55:19 +02:00
Stas Haas
5cb5df63d9 🌐 Add translations for: German.
Currently translated at 99.8% (1207 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-06-19 13:49:09 +02:00
Stas Haas
74552a4989 🌐 Add translations for: Russian.
Currently translated at 63.1% (763 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2023-06-19 13:49:08 +02:00
Alejandro
c6d71ea902 Merge pull request #3321 from penpot/niwinz-bugfixes-export
Niwinz bugfixes export
2023-06-19 13:16:44 +02:00
Andrey Antukh
4d850ebe6e 🐛 Add proper features initialization on render entrypoint 2023-06-19 13:08:11 +02:00
Andrey Antukh
dac18e876f 🐛 Fix validation error on password recovery submit operation 2023-06-19 13:07:46 +02:00
Andrey Antukh
d016876710 🐛 Add missing file-id validation on get-page rpc method 2023-06-19 13:07:26 +02:00
Andrey Antukh
ddeb540df6 🐛 Fix pointer map related issues on get-page rpc method
mainly used on render.html endpoint which is used by exporter
2023-06-19 13:06:44 +02:00
Pablo Alba
7733bc4419 🐛 Fix ungroup component 2023-06-19 12:29:54 +02:00
Alejandro Alonso
128fe29619 Show interactions on click as default setting at the view mode 2023-06-19 12:00:08 +02:00
Alejandro Alonso
23e200dece 🐛 Fix user select layer mode 2023-06-19 11:05:51 +02:00
Pablo Alba
d9375c1dd1 Fix duplicate shape in a component copy maintains its ref 2023-06-19 10:33:17 +02:00
K.B.Dharun Krishna
b72b8a6d53 🌐 Add translations for: Tamil.
Currently translated at 4.2% (51 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ta/
2023-06-17 11:51:35 +02:00
Mikel Larreategi
0a74696874 🌐 Add translations for: Basque.
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2023-06-17 11:51:35 +02:00
Stas Haas
6548fe069e 🌐 Add translations for: German.
Currently translated at 99.4% (1202 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-06-17 11:51:33 +02:00
Alejandro Alonso
aeebed6ef7 Merge remote-tracking branch 'origin/staging' into develop 2023-06-16 14:13:51 +02:00
Pablo Alba
498ba257b6 Merge pull request #3290 from penpot/hiru-fix-update-notifications
🐛 Solve error in notification of library changes
2023-06-16 14:07:35 +02:00
Andrés Moya
6edba71c12 🐛 Fix calculation of component modified and remove unneeded check 2023-06-16 13:24:41 +02:00
Andrés Moya
a559e7310a 🐛 Solve error in notification of library changes
(See main.data.workspace.notifications/schema:handle-file-change)
2023-06-16 12:23:11 +02:00
Andrés Moya
ebd172ab05 🐛 Fix detection of libraries needing to update 2023-06-16 12:22:14 +02:00
Pablo Alba
cdc3367d1b Merge pull request #3286 from penpot/superalex-fix-add-flow-option-for-frames
🐛 Fix add flow option in contextual menu for frames
2023-06-16 12:17:48 +02:00
Pablo Alba
8d37d63a27 Merge pull request #3292 from penpot/hiru-fix-export-components
🐛 Fix export components for v2
2023-06-16 12:12:01 +02:00
Aitor
95f0f63276 🐛 Fix creation cursors not being displayed 2023-06-16 12:04:16 +02:00
Pablo Alba
5cab599a06 Merge pull request #3285 from penpot/hiru-fill-problems
🐛 Revert #9de962bb and solve the fill issues in a different way
2023-06-16 11:56:39 +02:00
Alejandro Alonso
24715a85e5 Deleted fonts auto match 2023-06-16 11:07:16 +02:00
Alejandro
559c03550d Merge pull request #3298 from penpot/superalex-improve-invitations-validation
 Improve invitations validation
2023-06-16 10:51:57 +02:00
Alejandro
b8137d80cc Merge pull request #3314 from penpot/superalex-fix-survey-issues-2
🐛 Fix survey select 'other' options
2023-06-16 10:49:58 +02:00
Alejandro Alonso
0d7cac28c4 🐛 Fix survey select 'other' options 2023-06-16 10:35:37 +02:00
Alejandro Alonso
ae4fe73ac9 🐛 Fix survey select default options 2023-06-16 08:40:29 +02:00
Alejandro
1c1397a5d8 Merge pull request #3307 from penpot/eva-fix-color-context
🐛 Fix number of color bullets shown on context menu
2023-06-15 12:17:30 +02:00
Eva
cbebf9a94c 🐛 Fix number of color bullets shown on context menu 2023-06-15 11:51:25 +02:00
Alejandro
119b3e7884 Merge pull request #3306 from penpot/eva-fix-shortcuts
🐛 Fix shortcuts translation error
2023-06-15 11:03:27 +02:00
Eva
13607adf86 🐛 Fix shortcuts translation error 2023-06-15 10:59:40 +02:00
Eva Marco
247c950cce Merge pull request #3304 from penpot/alotor-fix-shape-to-path
🐛 Fix problem when transforming shape to path
2023-06-15 10:36:41 +02:00
Eva Marco
1555d4abaf Merge pull request #3303 from penpot/azazeln28-cursors
 Add CSS cursor classes
2023-06-15 08:10:44 +02:00
Alejandro
77a16a6074 Merge pull request #3301 from penpot/juan-shorcuts-ui-redesign
Shorcuts UI redesign
2023-06-15 08:03:12 +02:00
Alejandro
28b1c9c6d6 Merge pull request #3302 from penpot/superalex-fix-survey-issues
🐛 Fix some onboarding survey issues
2023-06-15 07:38:47 +02:00
Alejandro Alonso
1bb1734448 🐛 Fix some onboarding survey issues 2023-06-15 07:33:11 +02:00
alonso.torres
dd472bee64 🐛 Fix problem when transforming shape to path 2023-06-14 18:07:33 +02:00
Aitor
216454f66f Add CSS cursor classes 2023-06-14 16:27:14 +02:00
elhombretecla
ca85854baf 🎉 Adds basic shortcuts structure 2023-06-14 13:12:50 +02:00
Eva Marco
0682ed101d Merge pull request #3297 from penpot/alotor-global-styles
 Fix new styles leaking for scroll
2023-06-13 11:54:58 +02:00
Alejandro Alonso
8a9a3cbf37 Improve invitations validation 2023-06-13 11:51:03 +02:00
alonso.torres
c74ccfaa8d Fix new styles leaking for scroll 2023-06-13 11:50:21 +02:00
Andrés Moya
f2fcd0f82f 🐛 Fix export components for v2 2023-06-12 17:13:10 +02:00
Andrés Moya
a43d439b31 🐛 Revert #9de962bb and solve the fill issues in a different way 2023-06-09 21:13:43 +02:00
Alejandro
b73ab97556 Merge pull request #3284 from penpot/hiru-fix-blend-mode-validation
🐛 Flix blend mode validation when importing svg
2023-06-09 11:26:02 +02:00
Alejandro Alonso
baca9a8ce5 🐛 Fix survey spanish typo 2023-06-09 10:49:03 +02:00
Alejandro Alonso
bc64fdb1bc 🐛 Fix add flow option in contextual menu for frames 2023-06-09 09:28:27 +02:00
Stas Haas
22d852fca8 🌐 Add translations for: German.
Currently translated at 98.6% (1193 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-06-09 03:52:52 +02:00
王世阳
17c2f44780 🌐 Add translations for: Chinese (Simplified).
Currently translated at 100.0% (1209 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2023-06-09 03:52:51 +02:00
Andrés Moya
1d5d5e2499 🐛 Flix blend mode validation when importing svg 2023-06-08 09:52:34 +02:00
Pablo Alba
8b29a50577 Fix paste shapes from another components should detach them 2023-06-07 16:46:52 +02:00
Pablo Alba
55a821f193 🐛 Fix copy paste can produce nested components in copies 2023-06-07 13:08:17 +02:00
Pablo Alba
291180816a 🐛 Fix go to main component on another page 2023-06-07 11:53:56 +02:00
Pablo Alba
27695f5ae1 Merge pull request #3270 from penpot/hiru-bugtixes-3
Hiru bugfixes 3
2023-06-06 16:18:51 +02:00
Pablo Alba
69d3bda01f 🐛 Remove graphics from assets filter for components v2 2023-06-06 16:11:09 +02:00
Alejandro
1632530b21 Merge pull request #3280 from penpot/superalex-fix-develop-2
🐛 Fix align.cljc lint
2023-06-06 14:11:16 +02:00
Alejandro Alonso
c89f2fc627 🐛 Fix align.cljc lint 2023-06-06 14:03:08 +02:00
Alejandro Alonso
d0c68dbc23 🎉 Updage CHANGES.md 2023-06-06 13:23:34 +02:00
Alejandro
e41c36f534 Merge pull request #3267 from dfelinto/fix-distribute
🐛 Distribute vertical spacing failing for overlapped text
2023-06-06 13:21:51 +02:00
Andrés Moya
9de962bbc9 🐛 Do not render fills block when there is no fill. 2023-06-06 13:16:28 +02:00
Sebastiaan Pasma
40286c81d4 🌐 Add translations for: Dutch.
Currently translated at 11.6% (141 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2023-06-06 10:49:44 +02:00
Aitor Moreno
4947169a7c Merge pull request #3268 from penpot/superalex-file-libraries-colors-order
 Improve file libraries colors order
2023-06-06 10:43:52 +02:00
Andrés Moya
f425a5866b 🐛 Allow empty fills in text content 2023-06-05 17:37:41 +02:00
Andrés Moya
3e30d4776a 🐛 Avoid unneeded component update, that was generating loops 2023-06-05 15:43:57 +02:00
Andrés Moya
bca90c54e9 🐛 Preserve root shape position on parent when create component 2023-06-05 15:43:57 +02:00
Andrés Moya
8c3f90fe36 🐛 Fix erroneous touched state when delete a copy and then undo 2023-06-05 15:43:57 +02:00
Andrés Moya
0b316d6828 🐛 Fix touched erroneously set after a text component sync 2023-06-05 15:43:57 +02:00
alonso.torres
8772e51bd2 🐛 Fix problem with padding input 2023-06-05 11:23:08 +02:00
Alejandro Alonso
7e8afb4228 Merge remote-tracking branch 'origin/staging' into develop 2023-06-05 10:19:43 +02:00
Alejandro
6659ab110c Merge pull request #3273 from penpot/alotor-fix-gap
🐛 Fix problem with undefined gaps
2023-06-05 10:04:26 +02:00
alonso.torres
3b8c3647fa 🐛 Fix problem with undefined gaps 2023-06-05 09:56:03 +02:00
Alejandro Alonso
4fc8ac61f1 ❤️ Add thanks for Dalai Felinto 2023-06-05 06:41:07 +02:00
Alejandro Alonso
5b475f9206 ❤️ Add thanks for Dalai Felinto 2023-06-05 06:40:30 +02:00
Alejandro
c228f2fd68 Merge pull request #3266 from dfelinto/fix-distribute-enable
🐛 Distribute fix enabled when two elements were selected
2023-06-05 06:38:33 +02:00
Stas Haas
3b262f2ae5 🌐 Add translations for: German.
Currently translated at 97.5% (1179 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-06-04 14:49:45 +02:00
Ņikita K
80dd910d58 🌐 Add translations for: Latvian.
Currently translated at 98.8% (1195 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2023-06-03 02:50:25 +02:00
王世阳
21a066ec64 🌐 Add translations for: Chinese (Simplified).
Currently translated at 99.8% (1207 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2023-06-03 02:50:24 +02:00
Dalai Felinto
395fbef19e 🐛 Distribute vertical spacing failing for overlapped text
The code was doing what it was designed to, however there is no
reason to prevent elements with a bit of overlap to also be
equally distributed.

closes #3141

Signed-off-by: Dalai Felinto <dalai@blender.org>
2023-06-02 18:45:35 +02:00
Dalai Felinto
a6155f9f83 🐛 Distribute fix enabled when two elements were selected
The distribute operations only make sense when there are at least 3
selected elements.

-----------------

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.

Signed-off-by: Dalai Felinto <dalai@blender.org>
2023-06-02 18:43:37 +02:00
Pablo Alba
a89d47b5c5 🐛 Fix 'upate main component' and 'reset overrides' shows in context menu of untouched copies 2023-06-02 17:56:33 +02:00
andy
29c091a26b 🌐 Added translation for: Dutch. 2023-06-02 17:17:44 +02:00
Alejandro
531d640d38 Merge pull request #3274 from penpot/azazeln28-fix-thankyou
📚 Fix broken THANKYOU.md links
2023-06-02 15:46:56 +02:00
Aitor Moreno
3505834014 Merge pull request #3258 from penpot/superalex-add-color-asset-from-selected-layer
🐛 Fix create color asset from selected layer
2023-06-02 15:26:23 +02:00
Aitor
cc0b981938 📚 Fix broken THANKYOU.md links 2023-06-02 15:15:34 +02:00
Pablo Alba
380b632dd0 🐛 Fix can't add fill color to a component without fill 2023-06-01 15:00:01 +02:00
Pablo Alba
fc038998d5 🐛 Fix copy paste can produce nested components 2023-06-01 13:45:37 +02:00
Alejandro Alonso
b8ef6dffb9 Improve file libraries colors order 2023-06-01 13:28:59 +02:00
Eva
33fb979b2c 🐛 Fix broken file 2023-06-01 12:51:51 +02:00
Ņikita K
b249cd1b72 🌐 Add translations for: Latvian.
Currently translated at 96.5% (1167 of 1209 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2023-06-01 08:51:33 +02:00
Andrey Antukh
b87f0bd5e8 🐛 Fix issue on features handling function 2023-06-01 06:28:00 +02:00
Pablo Alba
69069afb0a Merge pull request #3260 from penpot/hiru-component-outline-color
🐛 Detect correctly color of outlines and controls of components
2023-05-31 17:02:08 +02:00
Andrés Moya
9c79c80fd7 🐛 Detect correctly color of outlines and controls of components 2023-05-31 11:36:29 +02:00
Pablo Alba
dcb5194252 🐛 After restore a component, make the action 'go to main component' 2023-05-31 11:27:28 +02:00
Pablo Alba
4582ffb440 🐛 Fix show main component 2023-05-31 11:27:28 +02:00
Alejandro Alonso
3ca7cae6e0 Merge remote-tracking branch 'origin/staging' into develop 2023-05-31 11:15:28 +02:00
Alejandro Alonso
893c7a7d2e ⬆️ Update deps 2023-05-31 11:05:21 +02:00
Alejandro Alonso
274a201dba ❤️ Add thanks for Vaibhav Shukla 2023-05-31 10:43:59 +02:00
Alejandro Alonso
917f0d2b20 🐛 Fix create color assets opacity specs 2023-05-31 10:19:38 +02:00
Alejandro Alonso
5a733c84be Merge remote-tracking branch 'origin/staging' into develop 2023-05-31 10:14:45 +02:00
Andrés Moya
d8861bbf48 🐛 Refix commit f3754d0c55, lost in merge conflict 2023-05-30 14:41:54 +02:00
Andrés Moya
63e920828b 🐛 Fix frame components lost fill when migrated to v2 2023-05-30 10:55:12 +02:00
Andrés Moya
eeaee5fd13 🐛 Fix error first time doing a component change operation 2023-05-30 10:55:12 +02:00
Andrés Moya
fd6001090e 🐛 Detach shapes when dragged out of their component 2023-05-30 10:55:12 +02:00
Andrés Moya
968dcefc28 🐛 Maintain ids of main shapes to keep existing copies in sync 2023-05-30 10:55:12 +02:00
Pablo Alba
61cad18bcc 🐛 Use update position for align 2023-05-29 15:40:25 +02:00
Alejandro Alonso
78551cea61 🐛 Fix create color asset from selected layer 2023-05-29 15:27:21 +02:00
Alejandro Alonso
c189b5e638 🐛 Disable old urls when moving files between projects 2023-05-29 11:56:42 +02:00
Pablo Alba
2c007e7303 🐛 Remove duplicate component context menu item 2023-05-29 08:48:23 +02:00
Pablo Alba
610e34e05b Merge pull request #3245 from penpot/hiru-fix-nesting-loop
🐛 Avoid infinite loop nesting copies inside components
2023-05-26 19:00:27 +02:00
Alejandro
bd83292a85 Merge pull request #3252 from penpot/niwinz-bugfix-1
🐛 Fix incorrect impl of go-to-main-component
2023-05-26 15:55:51 +02:00
Andrey Antukh
1a420476c5 🐛 Fix incorrect impl of go-to-main-component 2023-05-26 15:51:29 +02:00
Alejandro Alonso
038d327b50 🐛 Fix project navigation from workspace 2023-05-26 15:11:35 +02:00
diacritica
4d094961b7 💄 Fixed link for penpotfest landing page
A simple github's friendly markdown fix to get link right
2023-05-26 12:31:37 +02:00
diacritica
97b5abb47b 📚 Added Penpot Fest link to README
This is a temporary change to let people know about Penpot Fest's open
registration
2023-05-26 12:24:56 +02:00
Alejandro
3106058637 Merge pull request #3248 from penpot/azazeln28-refactor-unnecessary-encode-decode
♻️ Refactor svg to data-uri code
2023-05-26 11:10:45 +02:00
Aitor
4068413f9f ♻️ Refactor svg to data-uri code 2023-05-26 10:43:12 +02:00
Andrey Antukh
ccafbec485 🔥 Remove testing keys from backend repl script 2023-05-26 10:19:15 +02:00
Alejandro
6000dc251d Merge pull request #3206 from penpot/niwinz-workspace-assets-component-performance
 Improve performance of workspace assets sidebar
2023-05-26 08:10:53 +02:00
Andrey Antukh
b85b479396 Add more improvements to workspace initialization 2023-05-26 08:04:01 +02:00
Andrey Antukh
5d892d14d5 Move sidebar ns to correct location 2023-05-26 08:04:01 +02:00
Andrey Antukh
da5209001b Hide all messages on enter workspace
move the logic from component to event
2023-05-26 08:04:01 +02:00
Andrey Antukh
a6659601f4 Make workspace readiness state more robust 2023-05-26 08:04:01 +02:00
Andrey Antukh
bd834ba840 Improve component renaming process on workspace 2023-05-26 08:04:01 +02:00
Andrey Antukh
0ea07fbe01 ♻️ Refactor selection management on workspace assets component 2023-05-26 08:04:01 +02:00
Andrey Antukh
8f72faf27d 🐛 Fix issues on penpot file import and components-v2 2023-05-26 08:04:01 +02:00
Andrey Antukh
68c0b0e8a7 Add minor perf improvement on components-v2 migration 2023-05-26 08:04:01 +02:00
Andrey Antukh
0078c0e601 🐛 Fix missing pointer persistence on file gc task 2023-05-26 08:04:01 +02:00
Andrey Antukh
1d4bd34dfc Move fressian to common module 2023-05-26 08:04:01 +02:00
Andrey Antukh
ff00043811 Improve workspace initialization flow 2023-05-26 08:04:01 +02:00
Andrey Antukh
8ca6055935 🐛 Fix backend shape validation after changes apply 2023-05-26 08:04:01 +02:00
Andrey Antukh
390f2b35fc 🐛 Ensure verify! works as expected on production builds 2023-05-26 08:04:01 +02:00
Andrey Antukh
02fbce13f0 Add minor performance improvements to workspace left toolbar 2023-05-26 08:04:01 +02:00
Andrey Antukh
5d8562e072 Fix react warnings on workspace shortcuts panel 2023-05-26 08:04:01 +02:00
Andrey Antukh
ca439cf604 Add minor performance improvements to workspace main components 2023-05-26 08:04:01 +02:00
Andrey Antukh
bdb0e24c40 Refactor state management of workspace header 2023-05-26 08:03:59 +02:00
Andrey Antukh
fcc4f4eed8 Refactor state management of workspace assets sidebar 2023-05-26 07:57:28 +02:00
Andrey Antukh
ef27301238 Add arity-1 to d/nilv that returns a transducer 2023-05-26 07:57:28 +02:00
Andrey Antukh
d1e74b0da9 Increase default stacktrace size on cljs 2023-05-26 07:57:28 +02:00
Andrey Antukh
a1819e78e4 ⬆️ Update rumext dependency 2023-05-26 07:57:28 +02:00
Andrey Antukh
a455fc015b 🐛 Fix several issues related to pointer-map and components-v2 2023-05-26 07:57:28 +02:00
Eva Marco
af2c10f2ab Merge pull request #3235 from penpot/akshay-gupta7-akshayg7-preview-blend-modes
🎉 Implement functionality to preview layer blend modes
2023-05-25 14:26:35 +02:00
Andrés Moya
82ba39f99c 🐛 Avoid infinite loops nesting a copy inside its own component 2023-05-25 10:53:01 +02:00
alonso.torres
471c9d5526 🐛 Fix problem with select method 2023-05-25 10:04:56 +02:00
Alejandro Alonso
9df6de2673 Merge branch 'akshay-gupta7-akshayg7-navigate-to-project-new-window' into develop 2023-05-25 09:58:47 +02:00
Alejandro Alonso
1c10bde4b1 🎉 Updage CHANGES.md 2023-05-25 09:58:33 +02:00
Akshay Gupta
64eba585d9 🎉 Add feature to open project name in new tab from workspace
Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
2023-05-25 09:57:49 +02:00
Alejandro Alonso
6eb5c75ad4 🐛 Fix preview layer blend modes on multiselection and avoid
persisting data while previewing
2023-05-25 08:58:52 +02:00
Andrey Antukh
23f0ee9e55 Refactor select and layer-menu components 2023-05-25 07:32:31 +02:00
Akshay Gupta
eec2fd00a2 🎉 Implement ability to preview layer blend modes
Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
2023-05-25 07:32:31 +02:00
Pablo Alba
749fc61885 🐛 Fix right button in frame title produces an internal error 2023-05-24 17:17:35 +02:00
Eva Marco
df1c56da2d Merge pull request #3236 from penpot/akshay-gupta7-akshayg7-add-shadows-reorder
🎉 Add ability to change shadows' order and place new shadows at first
2023-05-24 13:42:52 +02:00
Aitor
48b0df8e75 🐛 Fix thumbnails being rendered with previous size 2023-05-24 13:09:28 +02:00
Pablo Alba
fb3655506f 🐛 Fixes context menu action for duplicate main component 2023-05-24 12:26:27 +02:00
Pablo Alba
6929347da7 🎉 Change main shape name along with component name 2023-05-24 12:17:58 +02:00
Alejandro Alonso
1dab570907 🐛 Fix some limit situations on shadow reorder 2023-05-24 11:40:29 +02:00
Alejandro Alonso
1719f24b57 🐛 Fix develop branch after merge 2023-05-24 11:08:40 +02:00
Alejandro Alonso
2801431fab Merge remote-tracking branch 'origin/staging' into develop 2023-05-24 11:00:54 +02:00
Pablo Alba
8c915d1687 🐛 Fix paste component to another file 2023-05-23 09:10:54 +02:00
Pablo Alba
7d8a62664a Merge pull request #3223 from penpot/hiru-bugfixes
hiru bugfixing
2023-05-22 17:28:35 +02:00
Andrés Moya
9d5b59e9bb 🐛 Fix grouping of undo transactions 2023-05-22 17:26:53 +02:00
Andrés Moya
f73d7111b4 🐛 Avoid crash when renaming a page with double click 2023-05-22 17:26:53 +02:00
Andrés Moya
42a044fd22 🔥 Remove unused code 2023-05-22 17:26:53 +02:00
Andrés Moya
19ea85d9cc 🐛 Launch component sync when adding or removing shapes 2023-05-22 17:26:53 +02:00
Eva
36b016a37b Add new palette UI 2023-05-22 15:59:49 +02:00
Andrey Antukh
a09dd953ff Add incomplete performance enhancements to shadow menu
It is imposible to make this commponent efficient because of
the design limitations of numeric-input component
2023-05-22 14:15:08 +02:00
Andrey Antukh
73ed37f57a 💄 Add cosmetic changes to stoke related functions frontend 2023-05-22 14:15:08 +02:00
Andrey Antukh
98a6c63ad6 💄 Add cosmetic changes to shadow-add and reorder-shadow fns 2023-05-22 14:15:04 +02:00
Akshay Gupta
1eb6e30369 🎉 Add ability to change shadows order and place new shadows at top by default
Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
2023-05-22 12:57:01 +02:00
Andrey Antukh
68c1d9afaf Merge remote-tracking branch 'origin/staging' into develop 2023-05-22 11:01:47 +02:00
Aitor
42cd9a59b9 🐛 Fix color picker broken images 2023-05-22 10:56:46 +02:00
Andrey Antukh
b7e1e54a92 Add general performance micro optimizations 2023-05-22 10:56:46 +02:00
Andrey Antukh
78f62cc5e1 🐛 Fix incorrect level for debug and trace log messages (cljs only) 2023-05-22 10:56:46 +02:00
Aitor
48834f96d3 ♻️ Refactor thumbnail rendering on workspace 2023-05-22 10:56:46 +02:00
Pablo Alba
1d69da1ca5 🐛 Minor style tweaks for component annotations 2023-05-19 13:01:08 +02:00
Pablo Alba
2704c3f3de 🐛 Fix libraries had different sizes 2023-05-19 12:30:41 +02:00
Pablo Alba
65c695e830 🐛 Fix delete page with components 2023-05-19 10:37:10 +02:00
Alejandro Alonso
a1c09057c1 🎉 Move survey to local resources 2023-05-18 12:17:03 +02:00
Pablo Alba
b6d60773e3 Merge pull request #3220 from penpot/hiru-bugfixes
Component bugfixes
2023-05-18 11:09:34 +02:00
Andrés Moya
8636a15f4b 🐛 Fix crash in reset overrides 2023-05-17 16:26:32 +02:00
Alejandro
96782bfa8e Merge pull request #3188 from penpot/niwinz-experiments-6
♻️ Refactor validation subsystem
2023-05-17 16:11:45 +02:00
Andrey Antukh
97d2af048c 🐛 Fix srepl get-file helper (add support for pointer map) 2023-05-17 16:05:31 +02:00
Andrey Antukh
049ebdd542 🐛 Fix intermitent exception on viewport ref ns 2023-05-17 16:05:31 +02:00
Andrey Antukh
bf3888585a Add some minor performance improvements to dashboard components 2023-05-17 16:05:31 +02:00
Andrey Antukh
35969e9f26 🐛 Fix incorrect assertion on dashboard ns 2023-05-17 16:05:31 +02:00
Andrey Antukh
9cb5df31d1 🐛 Fix react warning for missing key on context-menu-a11y component 2023-05-17 16:05:31 +02:00
Andrey Antukh
cf03cb4ca4 🐛 Fix unexpected exception on thumbnails & raf 2023-05-17 16:05:31 +02:00
Andrey Antukh
63f4ef97fb 🐛 Fix pointermap issue on file export 2023-05-17 16:05:31 +02:00
Andrey Antukh
8e0abec876 💄 Add some cosmetic improvements on access-tokens components 2023-05-17 16:05:31 +02:00
Andrey Antukh
5ca3d01ea1 🎉 Add malli based validation and coersion subsystem 2023-05-17 16:05:29 +02:00
Andrey Antukh
dbc08ba80f 📎 Fix linter issues on frontend 2023-05-17 15:47:21 +02:00
Andrey Antukh
47e3279302 ⬆️ Update some frontend dependencies 2023-05-17 15:47:21 +02:00
Andrey Antukh
06f25c3950 ⬆️ Update nodejs on exporter dockerfile 2023-05-17 15:47:21 +02:00
Andrey Antukh
e96fc32cc1 ⬆️ Update devenv dockerfile 2023-05-17 15:47:21 +02:00
Andrey Antukh
444b7d5aae ⬆️ Update to JDK19 on backend dockerfile 2023-05-17 15:47:21 +02:00
Andrey Antukh
01404ba581 🎉 Add the ability to delete and search profiles to manage.py 2023-05-17 15:47:21 +02:00
Andrey Antukh
0dc7f4e07e Add test for orphaned teams deletion 2023-05-17 15:47:21 +02:00
Andrey Antukh
730c26f1e2 📎 Remove worker explicitly from test initialization 2023-05-17 15:47:21 +02:00
Andrey Antukh
e30d1a40bc Avoid vthread pinning on invitations 2023-05-17 15:47:21 +02:00
Andrey Antukh
4e7f32aa88 Improve retry mechanism and macros 2023-05-17 15:47:21 +02:00
Pablo Alba
44a3f651c2 Merge pull request #3189 from penpot/hiru-sync-notifications
 Notify library updates when really needed
2023-05-17 15:35:06 +02:00
Andrés Moya
8a42a53522 Notify library updates when really needed 2023-05-17 14:12:49 +02:00
Andrés Moya
25f7c14f97 🐛 Fix deactivation of show distances when alt-tab is used
Alt key with a shape selected activates show-distances mode.

If you press Alt+tab, in many window managers the window is switched,
and thus the alt keydown event is sent to other app and does not reach
Penpot. So, we need to deactivate the mode also on window blur.
2023-05-17 13:53:22 +02:00
Andrés Moya
568338ad68 🐛 Avoid spec failure if not path or annotations 2023-05-16 12:22:10 +02:00
Andrés Moya
30dd9c5222 🐛 Fix undo when deleting shapes inside instances (ok) 2023-05-16 11:11:47 +02:00
Pablo Alba
68367b002e Components annotations 2023-05-16 11:06:54 +02:00
Andrés Moya
cd1825d97a Revert "🐛 Fix undo when deleting shapes inside instances"
This reverts commit c421059e97.
2023-05-12 16:40:38 +02:00
Andrés Moya
c421059e97 🐛 Fix undo when deleting shapes inside instances 2023-05-12 16:20:48 +02:00
Andrés Moya
58a6f437c4 🐛 Fix display of library view 2023-05-12 13:27:45 +02:00
Andrés Moya
e032736c27 🐛 Fix crash in libraries view 2023-05-12 12:50:16 +02:00
Andrés Moya
eb0d499ddf 🐛 Fix touched detection for texts 2023-05-10 17:21:03 +02:00
Alejandro Alonso
54ab57d8f6 Merge remote-tracking branch 'origin/staging' into develop 2023-05-09 14:39:23 +02:00
Alejandro Alonso
eeb71982c8 Merge remote-tracking branch 'origin/staging' 2023-05-09 14:39:07 +02:00
Alejandro Alonso
8352c9c6fd Merge remote-tracking branch 'origin/staging' 2023-05-09 10:22:55 +02:00
Alejandro Alonso
179b23ed6a Merge remote-tracking branch 'origin/staging' into develop 2023-05-09 10:22:17 +02:00
Alejandro Alonso
d97be7043a Merge remote-tracking branch 'origin/staging' into develop 2023-05-09 09:39:08 +02:00
Alejandro
2ce676885f Merge pull request #3193 from penpot/niwinz-thumbnails-1
🎉 Allow submit thumbnails using multipart
2023-05-08 16:11:11 +02:00
Alejandro
cf0a42c6eb Merge pull request #3197 from penpot/azazeln28-fix-rules-rendering
🐛 Fix rules rendering
2023-05-08 11:56:28 +02:00
Aitor
0214cfa299 🐛 Fix rules rendering 2023-05-08 09:58:37 +02:00
Alejandro Alonso
81fff2b5e8 Merge branch 'ondrejkonec-ondrej-design-token-implementation' into develop 2023-05-08 08:27:04 +02:00
Ondřej Konečný
e5612a7373 🐛 Fix sidebar collapse icon
Signed-off-by: Ondřej Konečný <ondrej.konecny@gmail.com>
2023-05-08 08:26:49 +02:00
Alejandro Alonso
969106e2b6 📎 Update CHANGES.md file 2023-05-08 06:16:42 +02:00
Alejandro Alonso
6bad9ac629 Merge branch 'akshay-gupta7-akshayg7-focus-input-search-from-dashboard' into develop 2023-05-08 06:15:04 +02:00
Akshay Gupta
c1187dd457 🎉 Add feature to focus input on search when searching a file at projects dashboard
Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
2023-05-08 06:13:48 +02:00
Andrey Antukh
e8ffcbae69 🎉 Add support for multipart upload of thumbnails
and improve the thumbnails storage to offloading it
to the storage subsystem
2023-05-05 17:00:35 +02:00
Andrey Antukh
c2b6b40554 💄 Add cosmetic changes (and comments) to toggle-file-thumbnail-selected function 2023-05-05 17:00:35 +02:00
Andrey Antukh
541a372f01 💄 Add cosmetic changes to duplicate-page function 2023-05-05 17:00:35 +02:00
Andrey Antukh
64cef9bb7d 📎 Add missing access-token middleware tests 2023-05-05 17:00:35 +02:00
Alejandro Alonso
70be668c1a Merge branch 'ondrejkonec-ondrej-suggestions-for-improvement' into develop 2023-05-05 11:20:25 +02:00
Ondřej Konečný
3ac8bf363a removed sizing variables from radius
Signed-off-by: Ondřej Konečný <ondrej.konecny@gmail.com>
2023-05-05 11:20:00 +02:00
Alejandro
9e66231218 Merge pull request #3187 from penpot/azazeln28-rules-performance
 better rules performance
2023-05-05 11:07:24 +02:00
Alejandro Alonso
e55cf2bdf9 Merge branch 'ryanbreen-patch-1' into develop 2023-05-05 10:59:45 +02:00
Ryan Breen
0a5263be35 🐛 rect filter bounds math fix
get-rect-filter-bounds was incorrectly applying delta-blur to x1 twice and to y1 never

Signed-off-by: Ryan Breen
2023-05-05 10:59:15 +02:00
Alejandro
5dd1fa0f98 Merge pull request #3171 from penpot/niwinz-enhancements-3
 Improve file-gc task
2023-05-05 10:55:14 +02:00
Alejandro Alonso
82b2f920c1 Merge branch 'akshay-gupta7-akshayg7-click-to-select-full-values-design-sidebar' into develop 2023-05-05 10:49:49 +02:00
Akshay Gupta
1c0e1237c2 🎉 Add feature to select full values on click at the design sidebar 2023-05-05 10:49:25 +02:00
Andrey Antukh
ceeed73dea Merge remote-tracking branch 'origin/staging' into develop 2023-05-04 22:15:18 +02:00
Alejandro Alonso
890583a13a Add mvp access-token support 2023-05-04 22:14:55 +02:00
Aitor
19727a648d better rules performance 2023-05-04 12:46:37 +02:00
Alejandro Alonso
b90aef4e1d Merge branch 'akshay-gupta7-akshayg7-set-line-height-to-auto' into develop 2023-05-04 12:34:28 +02:00
Akshay Gupta
412ffe4b46 🎉 Add feature to set line-height to auto as 1.2 2023-05-04 12:34:10 +02:00
Akshay Gupta
45356ae1fc 🎉 Add feature to focus input on search when searching a file at projects dashboard 2023-05-03 14:15:59 +02:00
Eva
86b0e95458 :sparkles:Add new layers panel UI design 2023-04-27 12:26:26 +02:00
Pablo Alba
90fb619dfc Fix restore main component when it was inside a group 2023-04-26 13:30:23 +02:00
Andrey Antukh
5e89aa2726 Improve file-gc task
make it more aware of fragments referenced on changes snapshots
2023-04-26 13:28:32 +02:00
Alejandro Alonso
82dad3217b 🐛 Fix translations typo 2023-04-26 12:38:02 +02:00
Alejandro Alonso
47cb228e30 Merge branch 'akshay-gupta7-akshayg7-empty-state-for-color-typographies' into develop 2023-04-26 12:36:39 +02:00
Akshay Gupta
35c0b94e0d 🎉 Add message for empty state for color and typography palettes 2023-04-26 12:36:31 +02:00
Pablo Alba
a7015f2517 Fix restore and instanciate (in copy and paste) components with parent 2023-04-26 11:34:26 +02:00
Pablo Alba
4f471f39da Merge pull request #3166 from penpot/hiru-frame-titles
 Hide frame titles for component copies
2023-04-25 21:33:56 +02:00
Pablo Alba
f14641396f Merge pull request #3165 from penpot/hiru-board-selection
 Give frames that are components more priority on selection
2023-04-25 21:31:01 +02:00
Alejandro
d97bbdf140 Merge pull request #3169 from penpot/niwinz-enhancements-2
 Add the abiltiy to forward command params as query-string
2023-04-25 16:33:03 +02:00
Andrey Antukh
f1c42a698d 📎 Increase http socket backlog 2023-04-25 16:25:49 +02:00
Andrey Antukh
8fb62628d2 Add the abiltiy to forward command params as query-string 2023-04-25 16:25:30 +02:00
Andrey Antukh
5026bfa6c1 📎 Fix linter issues introduced in previous merge 2023-04-25 13:35:26 +02:00
Andrey Antukh
b37a92aaf7 Merge remote-tracking branch 'origin/staging' into develop 2023-04-25 13:34:28 +02:00
Andrés Moya
b45bdb52b2 Hide frame titles for component copies 2023-04-25 11:55:03 +02:00
Andrés Moya
7c612d8bcf Give frames that are components more priority on selection 2023-04-25 11:21:24 +02:00
Pablo Alba
4ddd3811b2 🐛 Fix copy and paste components between files 2023-04-25 10:27:06 +02:00
Alejandro
da54557aab Merge pull request #3163 from penpot/niwinz-bugfixes-9
🐛 🔥 Fix merge bugs and remove deprecated code
2023-04-25 10:26:47 +02:00
Aitor
52763ceaf7 Merge pull request #3138 from penpot/fix-bad-undo-group-association-in-alt-copy
🐛 Fix bad undo group associations
2023-04-25 09:42:22 +02:00
Andrey Antukh
c0ccbaebaf 🔥 Remove deprecated queries and mutations 2023-04-24 20:18:14 +02:00
Andrey Antukh
36953eef1a 🐛 Use proper commands (instead of queries) on render frontend namespace 2023-04-24 19:47:28 +02:00
Andrey Antukh
84c8a6eced 🐛 Use correct parameters on password update on login 2023-04-24 19:46:42 +02:00
Andrey Antukh
1f023eebeb 🔥 Remove unused code 2023-04-24 18:21:48 +02:00
Hosted Weblate
e2a0a40704 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2023-04-24 16:55:45 +02:00
Andrey Antukh
6af783ea91 Merge remote-tracking branch 'origin/staging' into develop 2023-04-24 16:55:18 +02:00
Alejandro Alonso
d657f5df49 Merge remote-tracking branch 'origin/staging' 2023-04-24 09:16:52 +02:00
Alejandro Alonso
e89378453a Merge remote-tracking branch 'origin/staging' into develop 2023-04-24 09:15:22 +02:00
Alejandro Alonso
b7d1488aa3 Merge branch 'akshay-gupta7-akshayg7-duplicate-with-drag-and-alt' into develop 2023-04-24 08:22:28 +02:00
Akshay Gupta
d586f82da1 🎉 Implement functionality to duplicate objects via drag + alt 2023-04-24 08:22:14 +02:00
Alejandro Alonso
a658493ac5 Merge branch 'akshay-gupta7-akshayg7-typography-palette-order' into develop 2023-04-24 07:44:06 +02:00
Akshay Gupta
eaaeef2335 🎉 Update Typography palette order 2023-04-24 07:43:47 +02:00
Alejandro Alonso
bef9bbaa6a Merge branch 'abstractalgo-patch-1' into develop 2023-04-24 06:39:22 +02:00
Dragan Okanovic
32810f2ecd 🐛 Fix broken link in README 2023-04-24 06:38:17 +02:00
Alejandro Alonso
ed164ce69b Merge remote-tracking branch 'origin/staging' into develop 2023-04-14 13:28:26 +02:00
Alejandro Alonso
974bbd5ff4 Merge remote-tracking branch 'origin/staging' 2023-04-14 13:27:34 +02:00
Pablo Alba
33656f8eb4 Merge pull request #3115 from penpot/hiru-components-boards
🎉 Now all component roots are frames
2023-04-14 12:40:32 +02:00
Andrés Moya
bbd561a772 🔧 Fix test cases 2023-04-14 12:31:04 +02:00
Andrés Moya
2790111405 🎉 Now all component roots are frames 2023-04-14 12:31:03 +02:00
Alejandro Alonso
47b791e938 Board as ruler origin 2023-04-14 09:22:43 +02:00
Pablo Alba
47b432e307 🐛 Fix bad undo group associations 2023-04-13 18:44:40 +02:00
Alejandro Alonso
ce341a05e1 Merge remote-tracking branch 'origin/staging' into develop 2023-04-13 16:34:22 +02:00
Alejandro Alonso
b992c876e9 Merge remote-tracking branch 'origin/staging' 2023-04-13 16:33:27 +02:00
Alejandro
724b8990be Merge pull request #3136 from penpot/alotor-hotfix-1.18.3
Alotor hotfix 1.18.3
2023-04-13 16:28:58 +02:00
alonso.torres
452dcb5eec 🐛 Fix problem when "show in view mode" flag 2023-04-13 14:16:03 +02:00
alonso.torres
ae3de34033 🐛 Fix problem with rulers not placing correctly 2023-04-13 14:15:49 +02:00
Alejandro Alonso
45fc55dee9 Merge remote-tracking branch 'origin/staging' into develop 2023-04-13 12:24:52 +02:00
Alejandro Alonso
c3a4dbb871 Merge remote-tracking branch 'origin/staging' 2023-04-13 12:24:39 +02:00
Alejandro
067b76ebd8 Merge pull request #3134 from penpot/niwinz-bugfixes-7
🐛 Fix upload-file-media-object rpc method
2023-04-13 11:07:15 +02:00
Andrey Antukh
cb02b07395 🐛 Fix upload-file-media-object rpc method 2023-04-13 10:55:15 +02:00
Alejandro Alonso
81d718570d 🐛 Fix backend import 2023-04-13 09:21:57 +02:00
Alejandro Alonso
ee1b9e861e Merge remote-tracking branch 'origin/staging' into develop 2023-04-13 09:17:06 +02:00
Alejandro Alonso
3905ba4ce2 Merge remote-tracking branch 'origin/staging' 2023-04-13 09:16:52 +02:00
Andrés Moya
271b83de2e 🐛 Fix features activation by devtools console 2023-04-12 16:14:36 +02:00
Alejandro Alonso
aaca901fd9 🎉 Create typography style from a selected text layer 2023-04-12 09:30:41 +02:00
Pablo Alba
ccaac2a5c7 Merge pull request #3120 from penpot/superalex-default-naming-of-text-layers
🎉 Default naming of text layers
2023-04-11 17:50:53 +02:00
Pablo Alba
147beb3963 Merge pull request #3100 from penpot/hiru-detach-top-level-only
🎉 Detach component now only affects top instance, not subinstances
2023-04-11 15:19:20 +02:00
Pablo Alba
e481f1cc99 Merge pull request #3081 from penpot/hiru-cancel-remove-graphics
 Allow to cancel and resume later remove graphics
2023-04-11 15:16:05 +02:00
Pablo Alba
c1ed5a5b33 Merge pull request #3097 from penpot/hiru-fix-features-detect
♻️ Enhance features loading to avoid race conditions
2023-04-11 09:55:11 +02:00
Alejandro Alonso
4d8f471eca Merge remote-tracking branch 'origin/staging' into develop 2023-04-11 06:52:35 +02:00
Alejandro Alonso
0dcb3e94ce Merge remote-tracking branch 'origin/staging' 2023-04-11 06:51:09 +02:00
Alejandro Alonso
5993b9855e 🎉 Default naming of text layers 2023-04-10 13:16:26 +02:00
Andrey Antukh
6abca96da1 📎 Add improved docstring for penpot_secret_key 2023-04-07 08:57:08 +02:00
Alejandro Alonso
08c6ebe10c 🐛 Fix metrics and doc endpoints 2023-04-05 20:08:20 +02:00
Alejandro Alonso
408de63ea3 Merge remote-tracking branch 'origin/staging' into develop 2023-04-05 07:35:36 +02:00
Andrés Moya
e66f9597a9 Update component copy icon 2023-04-04 15:04:40 +02:00
Alejandro Alonso
f7e37924e5 🐛 Fix backend update-profile-password! call 2023-04-03 12:53:04 +02:00
Alejandro Alonso
68b26d5f41 Merge remote-tracking branch 'origin/staging' into develop 2023-04-03 12:21:12 +02:00
Alejandro Alonso
4926c826af Merge remote-tracking branch 'origin/staging' 2023-04-03 12:09:48 +02:00
Andrés Moya
a27fa8b317 🎉 Detach component now only affects top instance, not subinstances 2023-04-03 11:52:26 +02:00
Andrés Moya
18efa4ff2c ♻️ Enhance features loading to avoid race conditions 2023-03-31 16:13:11 +02:00
Alejandro
04b7d8e1e2 Merge pull request #3094 from penpot/hotfix-1.17
🐛 Fix problem with invalid geometry
2023-03-31 14:10:36 +02:00
Pablo Alba
b33e469501 🎉 Copy paste components, even to another page 2023-03-31 14:04:06 +02:00
alonso.torres
745cf1c79d 🐛 Fix problem with invalid geometry 2023-03-31 12:05:59 +02:00
Andrés Moya
e8d49fae13 Allow to cancel and resume later remove graphics 2023-03-29 12:57:21 +02:00
Pablo Alba
b73ce14560 Merge pull request #2967 from penpot/hiru-refactor-instances
🔧 Read component shapes from pages
2023-03-28 12:00:10 +02:00
Pablo Alba
6a1115ddda 🐛 Fix usiong padding/marging value on updating with shift 2023-03-27 11:55:23 +02:00
Pablo Alba
d3ae53e3ef 🐛 Fix padding/gap/margin remain glowing when the shape is deselected and selected again 2023-03-27 11:55:23 +02:00
Pablo Alba
4774cc4859 🐛 Fix rotate board breaks paddings 2023-03-27 11:55:23 +02:00
Pablo Alba
bc07dad4ae 🐛 Fix during scale paddings glow 2023-03-27 11:55:23 +02:00
Pablo Alba
0f9ad0907e 🐛 Fix padding prediction does not work with one shape 2023-03-27 11:55:23 +02:00
Pablo Alba
300ad15f5a 🐛 Bad padding gui on nil sizing 2023-03-27 11:55:23 +02:00
Andrés Moya
ad786ab95f 🎉 Group component sync changes in a single undo 2023-03-27 10:39:35 +02:00
Andrés Moya
fe898315c3 🐛 Fix absorb libraries 2023-03-27 10:39:35 +02:00
Andrés Moya
96540af2b1 🎉 Instantiate component with duplicate 2023-03-27 10:39:35 +02:00
Pablo Alba
6889440014 🐛 Fix wrong shape-ref on duplicate component 2023-03-27 10:39:35 +02:00
Pablo Alba
e59d106315 🐛 Fix duplicate component in assets generates wrong main copy 2023-03-27 10:39:35 +02:00
Andrés Moya
7391a4086a 🔧 Refactor delete/restore components 2023-03-27 10:39:35 +02:00
Andrés Moya
b91f1959b4 🎉 Update tests 2023-03-27 10:39:35 +02:00
Andrés Moya
0711fa700b 🔧 Read component shapes from pages 2023-03-27 10:39:33 +02:00
Alejandro Alonso
a4dd5fccff 🐛 Fix develop branch after merge 2023-03-24 13:06:16 +01:00
Alejandro Alonso
4fad2ab619 Merge remote-tracking branch 'origin/staging' into develop 2023-03-24 12:33:14 +01:00
Andrés Moya
ce3e30ea02 🐛 Fix linter issues 2023-03-21 17:12:54 +01:00
Andrés Moya
1d026ab085 🎉 Added 'go to main component' to components context menu 2023-03-21 17:12:54 +01:00
Andrey Antukh
60d629a0c6 Merge branch 'connecting-line-height-values-to-variables' into develop 2023-03-19 18:37:56 +01:00
Ondřej Konečný
d337dbfa5d ♻️ Connect line-heigh values to variables and set scale
Signed-off-by: Ondřej Konečný <ondrej.konecny@gmail.com>
2023-03-19 18:37:40 +01:00
Andrey Antukh
582ec187f8 Merge remote-tracking branch 'origin/staging' into develop 2023-03-17 10:19:04 +01:00
Alejandro
40ca804d93 Merge pull request #3051 from penpot/niwinz-experiments-2
🐛 Fix many issues related to the concurrency refactor PR
2023-03-17 08:25:31 +01:00
Andrey Antukh
2818666a1a 📎 Fix minnor cosmetic issue on instant and duration pretty printing 2023-03-16 22:33:35 +01:00
Andrey Antukh
9143639357 🐛 Fix incorrect webhook url validation 2023-03-16 22:33:35 +01:00
Andrey Antukh
f18d2ea629 🐛 Add missing fragment persistence on creating file
Related with storage/pointer-map feature.
2023-03-16 22:33:35 +01:00
Andrey Antukh
938890c04c 🐛 Fix vthread pining on get-file-data-for-thumbnail rpc method 2023-03-16 22:33:35 +01:00
Andrey Antukh
9173c73eca 🐛 Forward var bindings on climit submit operation 2023-03-16 22:33:35 +01:00
Andrey Antukh
69c8a89dd2 🎉 Add the ability to specify the output format from query string 2023-03-16 22:33:35 +01:00
Andrey Antukh
b462ac019a 🐛 Fix typo on error type 2023-03-16 22:33:35 +01:00
Andrey Antukh
3011d24905 📎 Enable storage features on start-dev and repl scripts 2023-03-16 22:33:35 +01:00
Alejandro
afb09919ed Merge pull request #3001 from penpot/niwinz-experiments-2
♻️ Refactor concurrency model (start using JDK19 virtual threads on RPC and WebSockets)
2023-03-15 11:34:25 +01:00
Alejandro Alonso
d685888720 Merge remote-tracking branch 'origin/staging' into develop 2023-03-15 09:44:44 +01:00
Pablo Alba
8ae1148ef9 🎉 Go to main component from context menu or with double click on the asset 2023-03-14 17:15:53 +01:00
Andrey Antukh
c9ec5234d3 ♻️ Refactor local in-memory cache api 2023-03-14 12:30:27 +01:00
Andrey Antukh
76b931108e Increase strenght of password hashing algorithm
And enable password update mechanism on login
2023-03-14 12:30:27 +01:00
Andrey Antukh
84dc3c8fd9 🔥 Remove debugging prn 2023-03-14 12:30:27 +01:00
Andrey Antukh
2cddc49463 Remove several reflection calls 2023-03-14 12:30:27 +01:00
Andrey Antukh
91b5a0afdd Add missing type hints on matrix type functions 2023-03-14 12:30:27 +01:00
Andrey Antukh
dfdc9c9fa5 ♻️ Refactor storage internal concurrency model 2023-03-14 12:30:27 +01:00
Andrey Antukh
aafbf6bc15 ♻️ Refactor cocurrency model on backend
Mainly the followin changes:

- Pass majority of code to the old and plain synchronous style
  and start using virtual threads for the RPC (and partially some
  HTTP server middlewares).
- Make some improvements on how CLIMIT is handled, simplifying code
- Improve considerably performance reducing the reflection and
  unnecesary funcion calls on the whole stack-trace of an RPC call.
- Improve efficiency reducing considerably the total threads number.
2023-03-14 12:30:27 +01:00
Andrey Antukh
2e717882f1 ♻️ Refactor websockets impl to use virtual threads
Removing the use of core.async code and implement code using
plain old and familiar synchronous code
2023-03-14 12:30:27 +01:00
Andrey Antukh
14b53a4d5e Don't log duplicate traceback 2023-03-14 12:30:27 +01:00
Andrey Antukh
04b321caae Add several improvements to internal worker impl
Mainly for make the cron jobs do not block the scheduled executor
and offload all work to a separate threads
2023-03-14 12:30:27 +01:00
Andrey Antukh
cad1851e95 🔥 Replace own scheduled executor with the one defined in promesa lib 2023-03-14 12:30:27 +01:00
Andrey Antukh
012ead65b5 🎉 Add missing ::us/atom global spec 2023-03-14 12:30:27 +01:00
Andrey Antukh
d549fcb2ae 🐛 Pass a valid executor instance to yetti http server 2023-03-14 12:30:27 +01:00
Andrey Antukh
4c85e55176 📎 Improve tests performance making all tables as unlogged 2023-03-14 12:30:27 +01:00
Andrey Antukh
1eb593703f 📎 Update clj-kondo config 2023-03-14 12:30:27 +01:00
Andrey Antukh
771fc1788c 📎 Update backend repl script 2023-03-14 12:30:27 +01:00
Andrey Antukh
ae9886080e 📎 Add better database configuration for devenv 2023-03-14 12:30:27 +01:00
Andrey Antukh
d76baa3266 ⬆️ Update promesa dependency
And adapt all code for breaking changes
2023-03-14 12:30:27 +01:00
Eva
adffdb31f3 Add css variables and theme switch 2023-03-14 11:48:31 +01:00
Alejandro Alonso
b77f85b697 📎 Prepare new development cycle 2023-03-13 10:39:58 +01:00
993 changed files with 124244 additions and 40425 deletions

View File

@@ -34,7 +34,7 @@ jobs:
working_directory: "./frontend"
command: |
yarn install
yarn run lint-scss
yarn run lint:scss
- run:
name: common lint

View File

@@ -2,10 +2,10 @@
{promesa.core/let clojure.core/let
promesa.core/->> clojure.core/->>
promesa.core/-> clojure.core/->
promesa.exec.csp/go-loop clojure.core/loop
rumext.v2/defc clojure.core/defn
rumext.v2/fnc clojure.core/fn
app.common.data/export clojure.core/def
app.db/with-atomic clojure.core/with-open
app.common.data.macros/get-in clojure.core/get-in
app.common.data.macros/with-open clojure.core/with-open
app.common.data.macros/select-keys clojure.core/select-keys
@@ -16,6 +16,9 @@
{app.common.data.macros/export hooks.export/export
potok.core/reify hooks.export/potok-reify
app.util.services/defmethod hooks.export/service-defmethod
app.common.record/defrecord hooks.export/penpot-defrecord
app.db/with-atomic hooks.export/penpot-with-atomic
rumext.v2/fnc hooks.export/rumext-fnc
}}
:output

View File

@@ -39,6 +39,60 @@
other))]
{:node result})))
(defn penpot-with-atomic
[{:keys [node]}]
(let [[params & body] (rest (:children node))]
(if (api/vector-node? params)
(let [[sym val opts] (:children params)]
(when-not (and sym val)
(throw (ex-info "No sym and val provided" {})))
{:node (api/list-node
(list*
(api/token-node 'let)
(api/vector-node [sym val])
opts
body))})
{:node (api/list-node
(into [(api/token-node 'let)
(api/vector-node [params params])]
body))})))
(defn rumext-fnc
[{:keys [node]}]
(let [[cname mdata params & body] (rest (:children node))
[params body] (if (api/vector-node? mdata)
[mdata (cons params body)]
[params body])]
(let [result (api/list-node
(into [(api/token-node 'fn)
params]
(cons mdata body)))]
{:node result})))
(defn penpot-defrecord
[{:keys [:node]}]
(let [[rnode rtype rparams & other] (:children node)
nodes [(api/token-node (symbol "do"))
(api/list-node
(into [(api/token-node (symbol (name (:value rnode)))) rtype rparams] other))
(api/list-node
[(api/token-node (symbol "defn"))
(api/token-node (symbol (str "pos->" (:string-value rtype))))
(api/vector-node
(->> (:children rparams)
(mapv (fn [t]
(api/token-node (symbol (str "_" (:string-value t))))))))
(api/token-node nil)])]
result (api/list-node nodes)]
;; (prn "=====>" (into {} rparams))
;; (prn (api/sexpr result))
{:node result}))
(defn clojure-specify
[{:keys [:node]}]
(let [[rnode rtype & other] (:children node)
@@ -48,7 +102,6 @@
other))]
{:node result}))
(defn service-defmethod
[{:keys [:node]}]
(let [[rnode rtype ?meta & other] (:children node)

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
root = true
[*.{cljs,cljc,clj,js,css,scss,html,yml,yaml,json,mustache}]
charset = utf-8
indent_size = 2
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
*.jar
*.orig
*.penpot
*.css.json
.calva
.clj-kondo
.cpcache

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"files.exclude": {
"**/.clj-kondo": true,
"**/.cpcache": true,
"**/.lsp": true,
"**/.shadow-cljs": true,
"**/node_modules": true
}
}

View File

@@ -1,5 +1,210 @@
# CHANGELOG
## 1.20.0
### :boom: Breaking changes & Deprecations
### :sparkles: New features
- Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484)
### :bug: Bugs fixed
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.19.4
### :sparkles: New features
- Improve selected colors [Taiga #5805]( https://tree.taiga.io/project/penpot/us/5805)
### :bug: Bugs fixed
- Fix problem with z-index field in non-absolute items
## 1.19.3
### :sparkles: New features
- Remember last color mode in colorpicker [Taiga #5508](https://tree.taiga.io/project/penpot/issue/5508)
- Improve layers multiselection behaviour [Github #5741](https://github.com/penpot/penpot/issues/5741)
- Remember last active team across logouts / sessions [Github #3325](https://github.com/penpot/penpot/issues/3325)
### :bug: Bugs fixed
- List view is discarded on tab change on Workspace Assets Sidebar tab [Github #3547](https://github.com/penpot/penpot/issues/3547)
- Fix message popup remains open when exiting workspace with browser back button [Taiga #5747](https://tree.taiga.io/project/penpot/issue/5747)
- When editing text if font is changed, the proportions of the rendered shape are wrong [Taiga #5786](https://tree.taiga.io/project/penpot/issue/5786)
## 1.19.2
### :sparkles: New features
- Navigate up in layer hierarchy with Shift+Enter shortcut [Taiga #5734](https://tree.taiga.io/project/penpot/us/5734)
- Click on the flow tags open viewer with the selected frame [Taiga #5044](https://tree.taiga.io/project/penpot/us/5044)
- Add Dutch language & update translation files with weblate
### :bug: Bugs fixed
- Fix unexpected output on get-page rpc method when invalid object-id is provided [Github #3546](https://github.com/penpot/penpot/issues/3546)
- Fix Invalid files amount after moving file from Project to Drafts [Taiga #5638](https://tree.taiga.io/project/penpot/us/5638)
- Fix deleted pages comments shown in right sidebar [Taiga #5648](https://tree.taiga.io/project/penpot/us/5648)
- Fix tooltip on toggle visibility and toggle lock buttons [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
## 1.19.1
### :bug: Bugs fixed
- Fix components not registered as updated [Taiga #5725](https://tree.taiga.io/project/penpot/issue/5725)
## 1.19.0
### :boom: Breaking changes & Deprecations
### :sparkles: New features
- Default naming of text layers [Taiga #2836](https://tree.taiga.io/project/penpot/us/2836)
- Create typography style from a selected text layer [Taiga #3041](https://tree.taiga.io/project/penpot/us/3041)
- Board as ruler origin [Taiga #4833](https://tree.taiga.io/project/penpot/us/4833)
- Access tokens support [Taiga #4460](https://tree.taiga.io/project/penpot/us/4460)
- Show interactions setting at the view mode [Taiga #1330](https://tree.taiga.io/project/penpot/issue/1330)
- Improve dashboard performance related to thumbnails; now the thumbnails are
rendered as bitmap images.
- Add the ability to disable google fonts provider with the `disable-google-fonts-provider` flag
- Add the ability to disable dashboard templates section with the `disable-dashboard-templates-section` flag
- Add the ability to use the registration whitelist with OICD [Github #3348](https://github.com/penpot/penpot/issues/3348)
- Add support for local caching of google fonts (this avoids exposing the final user IP to
goolge and reduces the amount of request sent to google)
- Set smooth/instant autoscroll depending on distance [GitHub #3377](https://github.com/penpot/penpot/issues/3377)
- New component icon [Taiga #5290](https://tree.taiga.io/project/penpot/us/5290)
- Show a confirmation dialog when an user tries to publish an empty library [Taiga #5294](https://tree.taiga.io/project/penpot/us/5294)
### :bug: Bugs fixed
- Fix files can be opened from multiple urls [Taiga #5310](https://tree.taiga.io/project/penpot/issue/5310)
- Fix asset color item was created from the selected layer [Taiga #5180](https://tree.taiga.io/project/penpot/issue/5180)
- Fix unpublish more than one library at the same time [Taiga #5532](https://tree.taiga.io/project/penpot/issue/5532)
- Fix drag projects on dahsboard [Taiga #5531](https://tree.taiga.io/project/penpot/issue/5531)
- Fix allow team name to be all blank [Taiga #5527](https://tree.taiga.io/project/penpot/issue/5527)
- Fix search font visualitation [Taiga #5523](https://tree.taiga.io/project/penpot/issue/5523)
- Fix create and account only with spaces [Taiga #5518](https://tree.taiga.io/project/penpot/issue/5518)
- Fix context menu outside screen [Taiga #5524](https://tree.taiga.io/project/penpot/issue/5524)
- Fix graphic item rename on assets pannel [Taiga #5556](https://tree.taiga.io/project/penpot/issue/5556)
- Fix component and media name validation on assets panel [Taiga #5555](https://tree.taiga.io/project/penpot/issue/5555)
- Fix problem with selection shortcuts [Taiga #5492](https://tree.taiga.io/project/penpot/issue/5492)
- Fix issue with paths line to curve and concurrent editing [Taiga #5191](https://tree.taiga.io/project/penpot/issue/5191)
- Fix problems with locked layers [Taiga #5139](https://tree.taiga.io/project/penpot/issue/5139)
- Fix export from shared prototype [Taiga #5565](https://tree.taiga.io/project/penpot/issue/5565)
- Fix email change: validation error displaying even after both fields are identical [Taiga #5514](https://tree.taiga.io/project/penpot/issue/5514)
- Fix scroll on viewer comment list [Taiga #5563](https://tree.taiga.io/project/penpot/issue/5563)
- Fix context menu z-index [Taiga #5561](https://tree.taiga.io/project/penpot/issue/5561)
- Fix select all checkbox on shared link config [Taiga #5566](https://tree.taiga.io/project/penpot/issue/5566)
- Fix validation on full name input on account creation [Taiga #5516](https://tree.taiga.io/project/penpot/issue/5516)
- Fix validation on team name input [Taiga #5510](https://tree.taiga.io/project/penpot/issue/5510)
- Fix incorrect uri generation issues on share-link modal [Taiga #5564](https://tree.taiga.io/project/penpot/issue/5564)
- Fix cache issues with share-links [Taiga #5559](https://tree.taiga.io/project/penpot/issue/5559)
- Makes height priority for the rows/columns grids [#2774](https://github.com/penpot/penpot/issues/2774)
- Fix problem with comments mode not staying [#3363](https://github.com/penpot/penpot/issues/3363)
- Fix problem with comments when user left the team [Taiga #5562](https://tree.taiga.io/project/penpot/issue/5562)
- Fix problem with images patterns repeating [#3372](https://github.com/penpot/penpot/issues/3372)
- Fix grid not being clipped in frames [#3365](https://github.com/penpot/penpot/issues/3365)
- Fix cut/delete text layer when while creating text [Taiga #5602](https://tree.taiga.io/project/penpot/issue/5602)
- Fix picking a gradient color in recent colors for a new color in the assets tab [Taiga #5601](https://tree.taiga.io/project/penpot/issue/5601)
- Fix problem with importation process [Taiga #5597](https://tree.taiga.io/project/penpot/issue/5597)
- Fix problem with HSV color picker [#3317](https://github.com/penpot/penpot/issues/3317)
- Fix problem with slashes in layers names for exporter [#3276](https://github.com/penpot/penpot/issues/3276)
- Fix incorrect modified data on moving files on dashboard [Taiga #5530](https://tree.taiga.io/project/penpot/issue/5530)
- Fix focus handling on comments edition [Taiga #5560](https://tree.taiga.io/project/penpot/issue/5560)
- Fix incorrect fullname use on registring user after OIDC authentication [Taiga #5517](https://tree.taiga.io/project/penpot/issue/5517)
- Fix incorrect modified-at on project after import file [Taiga #5268](https://tree.taiga.io/project/penpot/issue/5268)
- Fix incorrect message after sending invitation to already member [Taiga 5599](https://tree.taiga.io/project/penpot/issue/5599)
- Fix text decoration on button [Taiga #5301](https://tree.taiga.io/project/penpot/issue/5301)
- Fix menu order on design tab [Taiga #5195](https://tree.taiga.io/project/penpot/issue/5195)
- Fix search bar width on layer tab [Taiga #5445](https://tree.taiga.io/project/penpot/issue/5445)
- Fix border radius values with decimals [Taiga #5283](https://tree.taiga.io/project/penpot/issue/5283)
- Fix shortcuts translations not homogenized [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
- Fix overlay manual position in nested boards [Taiga #5135](https://tree.taiga.io/project/penpot/issue/5135)
- Fix close overlay from a nested board [Taiga #5587](https://tree.taiga.io/project/penpot/issue/5587)
- Fix overlay position when it has shadow or blur [Taiga #4752](https://tree.taiga.io/project/penpot/issue/4752)
- Fix overlay position when there are elements fixed when scrolling [Taiga #4383](https://tree.taiga.io/project/penpot/issue/4383)
- Fix problem when sliding color picker in selected-colors [#3150](https://github.com/penpot/penpot/issues/3150)
- Fix error screen on upload image error [Taiga #5608](https://tree.taiga.io/project/penpot/issue/5608)
- Fix bad frame-id for certain componentes [#3205](https://github.com/penpot/penpot/issues/3205)
- Fix paste elements at bottom of frame [Taig #5253](https://tree.taiga.io/project/penpot/issue/5253)
- Fix new-file button on project not redirecting to the new file [Taiga #5610](https://tree.taiga.io/project/penpot/issue/5610)
- Fix retrieve user comments in dashboard [Taiga #5607](https://tree.taiga.io/project/penpot/issue/5607)
- Locks shapes when moved inside a locked parent [Taiga #5252](https://tree.taiga.io/project/penpot/issue/5252)
- Fix rotate several elements in bulk [Taiga #5165](https://tree.taiga.io/project/penpot/issue/5165)
- Fix onboarding slides height [Taiga #5373](https://tree.taiga.io/project/penpot/issue/5373)
- Fix create typography with section closed [Taiga #5574](https://tree.taiga.io/project/penpot/issue/5574)
- Fix exports menu on viewer mode [Taiga #5568](https://tree.taiga.io/project/penpot/issue/5568)
- Fix create empty comments [Taiga #5536](https://tree.taiga.io/project/penpot/issue/5536)
- Fix text changes not propagated to copy [Taiga #5364](https://tree.taiga.io/project/penpot/issue/5364)
- Fix position of text cursor is a bit too high in Invitations section [Taiga #5511](https://tree.taiga.io/project/penpot/issue/5511)
- Fix undo when updating several texts [Taiga #5197](https://tree.taiga.io/project/penpot/issue/5197)
- Fix assets right click button for multiple selection [Taiga #5545](https://tree.taiga.io/project/penpot/issue/5545)
- Fix problem with precision in resizes [Taiga #5623](https://tree.taiga.io/project/penpot/issue/5623)
- Fix absolute positioned layouts not showing flex properties [Taiga #5630](https://tree.taiga.io/project/penpot/issue/5630)
- Fix text gradient handlers [Taiga #4047](https://tree.taiga.io/project/penpot/issue/4047)
- Fix when user deletes one file during import it is impossible to finish importing of second file [Taiga #5656](https://tree.taiga.io/project/penpot/issue/5656)
- Fix export multiple images when only one of them has export settings [Taiga #5649](https://tree.taiga.io/project/penpot/issue/5649)
- Fix error when a user different than the thread creator edits a comment [Taiga #5647](https://tree.taiga.io/project/penpot/issue/5647)
- Fix unnecessary button [Taiga #3312](https://tree.taiga.io/project/penpot/issue/3312)
- Fix copy color information in several formats [Taiga #4723](https://tree.taiga.io/project/penpot/issue/4723)
- Fix dropdown width [Taiga #5541](https://tree.taiga.io/project/penpot/issue/5541)
- Fix enable comment mode and insert image keeps on comment mode [Taiga #5678](https://tree.taiga.io/project/penpot/issue/5678)
- Fix enable undo just after using pencil [Taiga #5674](https://tree.taiga.io/project/penpot/issue/5674)
- Fix 400 error when user changes password [Taiga #5643](https://tree.taiga.io/project/penpot/issue/5643)
- Fix cannot undo layer styles [Taiga #5676](https://tree.taiga.io/project/penpot/issue/5676)
- Fix unexpected exception on boolean shapes [Taiga #5685](https://tree.taiga.io/project/penpot/issue/5685)
- Fix ctrl+z on select not working [Taiga #5677](https://tree.taiga.io/project/penpot/issue/5677)
- Fix thubmnail rendering flashing [Taiga #5675](https://tree.taiga.io/project/penpot/issue/5675)
### :arrow_up: Deps updates
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)
### :heart: Community contributions by (Thank you!)
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
- Palettes (color, typographies) empty state (by @akshay-gupta7) [Github #3160](https://github.com/penpot/penpot/pull/3160)
- Duplicate objects via drag + alt (by @akshay-gupta7) [Github #3147](https://github.com/penpot/penpot/pull/3147)
- Set line-height to auto as 1.2 (by @akshay-gupta7) [Github #3185](https://github.com/penpot/penpot/pull/3185)
- Click to select full values at the design sidebar (by @akshay-gupta7) [Github #3179](https://github.com/penpot/penpot/pull/3179)
- Fix rect filter bounds math (by @ryanbreen) [Github #3180](https://github.com/penpot/penpot/pull/3180)
- Removed sizing variables from radius (by @ondrejkonec) [Github #3184](https://github.com/penpot/penpot/pull/3184)
- Dashboard search, set focus after shortcut (by @akshay-gupta7) [Github #3196](https://github.com/penpot/penpot/pull/3196)
- Library name dropdown arrow is overlapped by library name (by @ondrejkonec) [Taiga #5200](https://tree.taiga.io/project/penpot/issue/5200)
- Reorder shadows (by @akshay-gupta7) [Github #3236](https://github.com/penpot/penpot/pull/3236)
- Open project in new tab from workspace (by @akshay-gupta7) [Github #3246](https://github.com/penpot/penpot/pull/3246)
- Distribute fix enabled when two elements were selected (by @dfelinto) [Github #3266](https://github.com/penpot/penpot/pull/3266)
- Distribute vertical spacing failing for overlapped text (by @dfelinto) [Github #3267](https://github.com/penpot/penpot/pull/3267)
- bug Change independent corner radius input tooltips #3332 (by @astudentinearth) [Github #3332](https://github.com/penpot/penpot/pull/3332)
## 1.18.6
### :bug: Bugs fixed
- Fix comments navigation from workspace [Taiga #5504](https://tree.taiga.io/project/penpot/issue/5504)
### :sparkles: Enhancements
- Add the ability to overwrite internal resolver with `PENPOT_INTERNAL_RESOLVER` environment
variable [GH #3310](https://github.com/penpot/penpot/issues/3310)
## 1.18.5
### :bug: Bugs fixed
- Fix add flow option in contextual menu for frames
- Fix issues related with invitations
- Fix problem with undefined gaps
- Add deleted fonts auto match mechanism
## 1.18.4
### :bug: Bugs fixed
@@ -8,6 +213,7 @@
- Fix problem with layout not reflowing on shape deletion [Taiga #5289](https://tree.taiga.io/project/penpot/issue/5289)
- Fix extra long typography names on assets and palette [Taiga #5199](https://tree.taiga.io/project/penpot/issue/5199)
- Fix background-color property on inspect code [Taiga #5300](https://tree.taiga.io/project/penpot/issue/5300)
- Preview layer blend modes (by @akshay-gupta7) [Github #3235](https://github.com/penpot/penpot/pull/3235)
## 1.18.3
@@ -38,6 +244,7 @@
## 1.18.0
### :sparkles: New features
- Adds more accessibility improvements in dashboard [Taiga #4577](https://tree.taiga.io/project/penpot/us/4577)
- Adds paddings and gaps prediction on layout creation [Taiga #4838](https://tree.taiga.io/project/penpot/task/4838)
- Add visual feedback when proportionally scaling text elements with **K** [Taiga #3415](https://tree.taiga.io/project/penpot/us/3415)

View File

@@ -26,6 +26,8 @@
![feature-readme](https://user-images.githubusercontent.com/1045247/189871786-0b44f7cf-3a0a-4445-a87b-9919ec398bf7.gif)
🎇 **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 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.
## Table of contents ##
@@ -124,7 +126,7 @@ You can ask and answer questions, have open-ended conversations, and follow alon
✏️ [Tutorials](https://www.youtube.com/playlist?list=PLgcCPfOv5v54WpXhHmNO7T-YC7AE-SRsr)
🏘️ [Architecture](https://help.penpot.app/technical-guide/architecture/)
🏘️ [Architecture](https://help.penpot.app/technical-guide/developer/architecture/)
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)

View File

@@ -5,24 +5,25 @@ We want to thank to the amazing people that help us! Thank you! You're the best!
## Security
* Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD)
* [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/)
* Vaibhav Shukla
## Internationalization
* [00ff88](https://hosted.weblate.org/user/00ff88)
* [AhmadHB](https://hosted.weblate.org/user/AhmadHB)
* [Aimee](https://hosted.weblate.org/user/Aimee)
* [alejandro.alonso](alejandro.https://hosted.weblate.org/user/alonso)
* [alejandro.alonso](https://hosted.weblate.org/user/alejandro.alonso)
* [alexpawlak](https://hosted.weblate.org/user/alexpawlak)
* [allytiago](https://hosted.weblate.org/user/allytiago)
* [alonso.torres](alonso.https://hosted.weblate.org/user/torres)
* [andres.moya](andres.https://hosted.weblate.org/user/moya)
* [alonso.torres](https://hosted.weblate.org/user/alonso.torres)
* [andres.moya](https://hosted.weblate.org/user/andres.moya)
* [antoniofsm](https://hosted.weblate.org/user/antoniofsm)
* [ascarida](https://hosted.weblate.org/user/ascarida)
* [Bechii](https://hosted.weblate.org/user/Bechii)
* [Beeby](https://hosted.weblate.org/user/Beeby)
* [bingling-sama](bingling-https://hosted.weblate.org/user/sama)
* [bingling-sama](https://hosted.weblate.org/user/bingling-sama)
* [devadarta](https://hosted.weblate.org/user/devadarta)
* [diacritica](https://hosted.weblate.org/user/diacritica)
* [dundzys.vincas](dundzys.https://hosted.weblate.org/user/vincas)
* [dundzys.vincas](https://hosted.weblate.org/user/dundzys.vincas)
* [Eranot](https://hosted.weblate.org/user/Eranot)
* [erral](https://hosted.weblate.org/user/erral)
* [ersen](https://hosted.weblate.org/user/ersen)

View File

@@ -1,10 +1,10 @@
{:deps
{:mvn/repos
{"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/core.async {:mvn/version "1.6.673"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.5-5"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -15,28 +15,30 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.2.2.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.2.6.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti
{:git/tag "v9.12"
:git/sha "51646d8"
{:git/tag "v9.16"
:git/sha "7df3e08"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
metosin/reitit-core {:mvn/version "0.5.18"}
org.postgresql/postgresql {:mvn/version "42.5.2"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.883"}
metosin/reitit-core {:mvn/version "0.6.0"}
org.postgresql/postgresql {:mvn/version "42.6.0"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "1.8.158"}
buddy/buddy-sign {:mvn/version "3.4.333"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.5.351"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.2"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.8"}
org.jsoup/jsoup {:mvn/version "1.15.3"}
org.jsoup/jsoup {:mvn/version "1.16.1"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
@@ -45,14 +47,14 @@
org.lz4/lz4-java {:mvn/version "1.8.0"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
integrant/integrant {:mvn/version "0.8.0"}
integrant/integrant {:mvn/version "0.8.1"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.4"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.19.29"}
software.amazon.awssdk/s3 {:mvn/version "2.20.138"}
}
:paths ["src" "resources" "target/classes"]
@@ -69,7 +71,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"}}
{io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
:ns-default build}
:test

View File

@@ -8,19 +8,24 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.fressian :as fres]
[app.common.geom.matrix :as gmt]
[app.common.logging :as l]
[app.common.perf :as perf]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as smdj]
[app.common.schema.desc-native :as smdn]
[app.common.schema.generators :as sg]
[app.common.spec :as us]
[app.common.transit :as t]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main :as main]
[app.srepl.helpers]
[app.srepl.helpers :as srepl.helpers]
[app.srepl.main :as srepl]
[app.util.blob :as blob]
[app.util.fressian :as fres]
[app.util.json :as json]
[app.util.time :as dt]
[clj-async-profiler.core :as prof]
@@ -31,13 +36,21 @@
[clojure.spec.alpha :as s]
[clojure.stacktrace :as trace]
[clojure.test :as test]
[clojure.test.check.generators :as gen]
[clojure.test.check.generators :as tgen]
[clojure.tools.namespace.repl :as repl]
[clojure.walk :refer [macroexpand-all]]
[criterium.core :as crit]
[cuerdas.core :as str]
[datoteka.core]
[integrant.core :as ig]))
[integrant.core :as ig]
[malli.core :as m]
[malli.dev.pretty :as mdp]
[malli.error :as me]
[malli.generator :as mg]
[malli.registry :as mr]
[malli.transform :as mt]
[malli.util :as mu]
[promesa.exec :as px]))
(repl/disable-reload! (find-ns 'integrant.core))
(set! *warn-on-reflection* true)
@@ -130,3 +143,38 @@
(add-tap #(locking debug-tap
(prn "tap debug:" %)))
1))
(sm/def! ::test
[:map {:title "Foo"}
[:x :int]
[:y {:min 0} :double]
[:bar
[:map {:title "Bar"}
[:z :string]
[:v ::sm/uuid]]]
[:items
[:vector ::dt/instant]]])
(sm/def! ::test2
[:multi {:title "Foo" :dispatch :type}
[:x
[:map {:title "FooX"}
[:type [:= :x]]
[:x :int]]]
[:y
[:map
[:type [:= :x]]
[:y [::sm/one-of #{:a :b :c}]]]]
[:z
[:map {:title "FooZ"}
[:z
[:multi {:title "Bar" :dispatch :type}
[:a
[:map
[:type [:= :a]]
[:a :int]]]
[:b
[:map
[:type [:= :b]]
[:b :int]]]]]]]])

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +1,30 @@
[{:id "material-design-3"
:name "Material Design 3"
:thumbnail-uri "https://penpot.app/images/libraries/cover-md3.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Material%20Design%203.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:thumbnail-uri "https://penpot.app/images/libraries/tutorial-for-beginners.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
{:id "penpot-design-system"
:name "Penpot Design System"
:thumbnail-uri "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
{:id "flex-layout-playground"
:name "Flex Layout Playground"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"}
{:id "wireframing-kit"
:name "Wireframing Kit"
:thumbnail-uri "https://penpot.app/images/libraries/cover-wireframes.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
{:id "ant-design"
:name "Ant Design UI Kit (lite)"
:thumbnail-uri "https://penpot.app/images/libraries/cover-ant-design.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Ant-Design-UI-Kit-Lite.penpot"}
{:id "cocomaterial"
:name "Cocomaterial"
:thumbnail-uri "https://penpot.app/images/libraries/cover-cocomaterial.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Cocomaterial.penpot"}
{:id "circum-icons"
:name "Circum Icons pack"
:thumbnail-uri "https://penpot.app/images/libraries/cover-circum.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/CircumIcons.penpot"}
{:id "coreui"
:name "CoreUI"
:thumbnail-uri "https://penpot.app/images/libraries/cover-coreui.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/main/CoreUI%20DesignSystem%20(DEMO).penpot"}
{:id "whiteboarding-kit"
:name "Whiteboarding Kit"
:thumbnail-uri "https://penpot.app/images/libraries/cover-whiteboards.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}]

View File

@@ -1,6 +1,5 @@
<li class="rpc-item">
<div class="rpc-row-info">
{# <div class="type">{{item.type}}</div> #}
<div class="module">{{item.module}}:</div>
<div class="name">{{item.name}}</div>
<div class="tags">
@@ -15,19 +14,23 @@
<span>AUTH</span>
</span>
{% endif %}
{% if item.webhook %}
<span class="tag">
<span>WEBHOOK</span>
</span>
{% endif %}
{% if item.params-schema-js %}
<span class="tag">
<span>SCHEMA</span>
</span>
{% endif %}
</div>
</div>
<div class="rpc-row-detail hidden">
<h3>DOCSTRING:</h3>
<h4>DOCSTRING:</h4>
<section class="padded-section">
{% if item.added %}
<p class="small"><strong>Added:</strong> on v{{item.added}}</p>
{% endif %}
@@ -36,13 +39,18 @@
<p class="small"><strong>Deprecated:</strong> since v{{item.deprecated}}</p>
{% endif %}
{% if item.entrypoint %}
<p class="small"><strong>URI:</strong> <a href="{{item.entrypoint}}">{{item.entrypoint}}</a></p>
{% endif %}
{% if item.docs %}
<p class="docstring"> {{item.docs}}</p>
{% endif %}
</section>
{% if item.changes %}
<h3>CHANGES:</h3>
<h4>CHANGES:</h4>
<section class="padded-section">
<ul class="changes">
@@ -53,9 +61,55 @@
</section>
{% endif %}
<h3>SPEC EXPLAIN:</h3>
<section class="padded-section">
<pre class="spec-explain">{{item.spec}}</pre>
</section>
{% if item.spec %}
<h4>PARAMS (SPEC):</h4>
<section class="padded-section">
<pre class="spec-explain">{{item.spec}}</pre>
</section>
{% endif %}
{% if param-style = "js" %}
{% if item.params-schema-js %}
<h4>PARAMS:</h4>
<section class="padded-section">
<pre class="params-schema">{{item.params-schema-js}}</pre>
</section>
{% endif %}
{% if item.result-schema-js %}
<h4>RESPONSE:</h4>
<section class="padded-section">
<pre class="result">{{item.result-schema-js}}</pre>
</section>
{% endif %}
{% if item.webhook-schema-js %}
<h4>WEBHOOK PAYLOAD:</h4>
<section class="padded-section">
<pre class="webhook">{{item.webhook-schema-js}}</pre>
</section>
{% endif %}
{% else %}
{% if item.params-schema-clj %}
<h4>PARAMS:</h4>
<section class="padded-section">
<pre class="params-schema">{{item.params-schema-clj}}</pre>
</section>
{% endif %}
{% if item.result-schema-clj %}
<h4>RESPONSE:</h4>
<section class="padded-section">
<pre class="result">{{item.result-schema-clj}}</pre>
</section>
{% endif %}
{% if item.webhook-schema-clj %}
<h4>WEBHOOK PAYLOAD:</h4>
<section class="padded-section">
<pre class="webhook">{{item.webhook-schema-clj}}</pre>
</section>
{% endif %}
{% endif %}
</div>
</li>

View File

@@ -27,12 +27,78 @@ main {
header {
border-bottom: 1px solid #c0c0c0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.rpc-doc-content {
header .menu {
display: flex;
align-items: center;
margin-top: 5px;
margin-bottom: 10px;
}
header .menu nav {
list-style: none;
padding: 0px;
margin: 0px;
display: flex;
width: 45px;
justify-content: space-between;
}
header .menu nav > a {
list-style: none;
padding: 0px;
margin: 0px;
cursor: pointer;
}
header .menu nav > a.selected {
font-weight: 600;
}
b {
font-weight: 500;
}
h2 {
margin-top: 30px;
}
h3 {
font-weight: 400;
font-size: 11px;
margin-top: 20px;
text-decoration: underline;
}
h4 {
font-weight: 300;
font-size: 11px;
}
.doc-content {
margin-top: 20px;
width: 100%;
display: flex;
flex-direction: column;
/* border: 1px solid red; */
padding: 5px;
}
.doc-content p {
line-height: 22px;
margin-bottom: 0px;
}
.doc-content h3 {
margin-bottom: 0px;
}
.rpc-doc-content {
width: 100%;
display: flex;
flex-direction: column;
@@ -65,7 +131,7 @@ header {
.rpc-row-info {
cursor: pointer;
display: flex;
background-color: #eeeeee;
background-color: #e5e5e5;
padding: 5px 10px;
}
@@ -90,7 +156,7 @@ header {
}
.rpc-row-info > .module {
width: 120px;
width: 150px;
font-weight: bold;
border-right: 1px dotted #777;
text-align: right;
@@ -108,6 +174,8 @@ header {
.rpc-row-detail {
padding: 5px 10px;
padding-bottom: 20px;
border-left: 2px solid #e5e5e5;
border-right: 2px solid #e5e5e5;
}
.rpc-row-detail p {
@@ -143,3 +211,7 @@ header {
p.small strong {
font-size: 10px;
}
p.small a {
font-size: 10px;
}

View File

@@ -20,26 +20,70 @@
<main>
<header>
<h1>Penpot API Documentation (v{{version}})</h1>
<small class="menu">
[
<nav>
<a href="?type=js" {% if param-style = "js" %}class="selected"{% endif %}>JS</a>
<a href="?type=clj" {% if param-style = "cljs" %}class="selected"{% endif %}>CLJ</a>
</nav>
]
</small>
</header>
<section class="doc-content">
<h2>INTRODUCTION</h2>
<p>This documentation is intended to be a general overview of the penpot RPC API.
If you prefer, you can use <a href="/api/openapi.json">OpenAPI</a>
and/or <a href="/api/openapi">SwaggerUI</a> as alternative.</p>
<h2>GENERAL NOTES</h2>
<h3>Authentication</h3>
<p>The penpot backend right now offers two way for authenticate the request:
<b>cookies</b> (the same mechanism that we use ourselves on accessing the API from the
web application) and <b>access tokens</b>.</p>
<p>The cookie can be obtained using the <b>`login-with-password`</b> rpc method,
on successful login it sets the <b>`auth-token`</b> cookie with the session
token.</p>
<p>The access token can be obtained on the appropriate section on profile settings
and it should be provided using <b>`Authorization`</b> header with <b>`Token
&lt;token-string&gt;`</b> value.</p>
<h3>Content Negotiation</h3>
<p>The penpot API by default operates indistinctly with: <b>`application/json`</b>
and <b>`application/transit+json`</b> content types. You should specify the
desired content-type on the <b>`Accept`</b> header, the transit encoding is used
by default.</p>
<h3>Limits</h3>
<p>The rate limit work per user basis (this means that different api keys share
the same rate limit). For now the limits are not documented because we are
studying and analyzing the data. As a general rule, it should not be abused, if an
abusive use is detected, we will proceed to block the user's access to the
API.</p>
<h3>Webhooks</h3>
<p>All methods that emit webhook events are marked with flag <b>WEBHOOK</b>, the
data structure defined on each method represents the <i>payload</i> of the
event.</p>
<p>The webhook event structure has this aspect:</p>
<br/>
<pre>
{
"id": "db601c95-045f-808b-8002-362f08fcb621",
"name": "rename-file",
"props": &lt;payload&gt;,
"profileId": "db601c95-045f-808b-8002-361312e63531"
}
</pre>
</section>
<section class="rpc-doc-content">
<h2>RPC COMMAND METHODS:</h2>
<h2>RPC METHODS REFERENCE:</h2>
<ul class="rpc-items">
{% for item in command-methods %}
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
</ul>
<h2>RPC QUERY METHODS:</h2>
<ul class="rpc-items">
{% for item in query-methods %}
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
</ul>
<h2>RPC MUTATION METHODS:</h2>
<ul class="rpc-items">
{% for item in mutation-methods %}
{% for item in methods %}
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
</ul>

View File

@@ -6,13 +6,19 @@ penpot - error list
{% block content %}
<nav>
<h1>Latest error reports:</h1>
<div class="title">
<h1>Error reports (last 200)</h1>
</div>
</nav>
<main class="horizontal-list">
<ul>
{% for item in items %}
<li><a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
<span class="title">{{item.hint|abbreviate:150}}</span></li>
<li>
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
<a class="hint" href="/dbg/error/{{item.id}}">
<span class="title">{{item.hint|abbreviate:150}}</span>
</a>
</li>
{% endfor %}
</ul>
</main>

View File

@@ -0,0 +1,101 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<div>[<a href="#props">props</a>]</div>
<div>[<a href="#context">context</a>]</div>
{% if params %}
<div>[<a href="#params">params</a>]</div>
{% endif %}
{% if data %}
<div>[<a href="#edata">data</a>]</div>
{% endif %}
{% if explain %}
<div>[<a href="#explain">explain</a>]</div>
{% endif %}
{% if value %}
<div>[<a href="#value">value</a>]</div>
{% endif %}
{% if trace %}
<div>[<a href="#trace">trace</a>]</div>
{% endif %}
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="head" class="table-key">HEAD</div>
<div class="table-val">
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
</div>
</div>
<div class="table-row multiline">
<div id="props" class="table-key">LOG PROPS: </div>
<div class="table-val">
<pre>{{props}}</pre>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
{% if params %}
<div class="table-row multiline">
<div id="params" class="table-key">PARAMS: </div>
<div class="table-val">
<pre>{{params}}</pre>
</div>
</div>
{% endif %}
{% if data %}
<div class="table-row multiline">
<div id="edata" class="table-key">DATA: </div>
<div class="table-val">
<pre>{{data}}</pre>
</div>
</div>
{% endif %}
{% if value %}
<div class="table-row multiline">
<div id="value" class="table-key">VALUE: </div>
<div class="table-val">
<pre>{{value}}</pre>
</div>
</div>
{% endif %}
{% if explain %}
<div class="table-row multiline">
<div id="explain" class="table-key">EXPLAIN: </div>
<div class="table-val">
<pre>{{explain}}</pre>
</div>
</div>
{% endif %}
{% if trace %}
<div class="table-row multiline">
<div id="trace" class="table-key">TRACE:</div>
<div class="table-val">
<pre>{{trace}}</pre>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="SwaggerUI"
/>
<title>PENPOT Swagger UI</title>
<style>{{swagger-css|safe}}</style>
</head>
<body>
<div id="swagger-ui"></div>
<script>{{swagger-js|safe}}</script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: '{{public-uri}}/api/openapi.json',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
],
});
};
</script>
</body>
</html>

View File

@@ -36,6 +36,11 @@ small {
color: #888;
}
.not-important {
color: #888;
font-weight: 200;
}
small > strong {
font-size: 9px;
}
@@ -50,7 +55,13 @@ nav {
background: #e3e3e3;
}
nav > h1 {
nav > .title {
display: flex;
justify-content: center;
width: 100%;
}
nav > .title > h1 {
padding: 0px;
margin: 0px;
font-size: 11px;
@@ -151,7 +162,6 @@ nav > div:not(:last-child) {
line-height: 18px;
min-width: 210px;
margin: 0px 20px;
cursor: pointer;
display: flex;
border-radius: 3px;
}

View File

@@ -1,8 +1,14 @@
;; Required: concurrency
;; Optional: queue-size, ommited means Integer/MAX_VALUE
{:update-file {:concurrency 1 :queue-size 3}
:auth {:concurrency 128}
:process-font {:concurrency 4 :queue-size 32}
:process-image {:concurrency 8 :queue-size 32}
:push-audit-events
{:concurrency 1 :queue-size 3}}
;; Example climit.edn file
;; Required: permits
;; Optional: queue, ommited means Integer/MAX_VALUE
;; Optional: timeout, ommited means no timeout
;; Note: queue and timeout are excluding
{:update-file-by-id {:permits 1 :queue 3}
:update-file {:permits 20}
:derive-password {:permits 8}
:process-font {:permits 4 :queue 32}
:process-image {:permits 8 :queue 32}
:submit-audit-events-by-profile
{:permits 1 :queue 3}}

View File

@@ -3,12 +3,12 @@
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
alwaysWriteExceptions="false" />
alwaysWriteExceptions="true" />
</Console>
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
alwaysWriteExceptions="false" />
alwaysWriteExceptions="true" />
<Policies>
<SizeBasedTriggeringPolicy size="50M"/>
</Policies>
@@ -22,8 +22,8 @@
<Logger name="org.postgresql" level="error" />
<Logger name="app.rpc.commands.binfile" level="debug" />
<Logger name="app.storage.tmp" level="debug" />
<Logger name="app.worker" level="info" />
<Logger name="app.storage.tmp" level="info" />
<Logger name="app.worker" level="trace" />
<Logger name="app.msgbus" level="info" />
<Logger name="app.http.websocket" level="info" />
<Logger name="app.util.websocket" level="info" />
@@ -31,6 +31,7 @@
<Logger name="app.rpc.rlimit" level="info" />
<Logger name="app.rpc.climit" level="info" />
<Logger name="app.rpc.mutations.files" level="info" />
<Logger name="app.common.files.migrations" level="debug" />
<Logger name="app.loggers" level="debug" additivity="false">
<AppenderRef ref="main" level="debug" />

View File

@@ -3,8 +3,9 @@
{:default
[[:default :window "200000/h"]]
#{:command/get-teams}
[[:burst :bucket "5/1/5s"]]
;; #{:command/get-teams}
;; [[:burst :bucket "5/5/5s"]]
#{:command/get-profile}
[[:burst :bucket "60/60/1m"]]}
;; #{:command/get-profile}
;; [[:burst :bucket "60/60/1m"]]
}

View File

@@ -18,6 +18,8 @@ cp scripts/manage.py target/dist/manage.py
chmod +x target/dist/run.sh;
chmod +x target/dist/manage.py
# Prefetch
# Prefetch templates
rm -rf builtin-templates;
mkdir builtin-templates;
bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/
cp -r builtin-templates target/dist/

View File

@@ -11,6 +11,7 @@ import json
import socket
import sys
from tabulate import tabulate
from getpass import getpass
from urllib.parse import urlparse
@@ -58,13 +59,17 @@ def print_error(res):
break
def run_cmd(params):
expr = "(app.srepl.ext/run-json-cmd {})".format(encode(params))
res, failed = send_eval(expr)
if failed:
print_error(res)
sys.exit(-1)
try:
expr = "(app.srepl.ext/run-json-cmd {})".format(encode(params))
res, failed = send_eval(expr)
if failed:
print_error(res)
sys.exit(-1)
return res
return res
except Exception as cause:
print("EXC:", str(cause))
sys.exit(-2)
def create_profile(fullname, email, password):
params = {
@@ -96,6 +101,34 @@ def update_profile(email, fullname, password, is_active):
else:
print(f"No profile found with email {email}")
def delete_profile(email, soft):
params = {
"cmd": "delete-profile",
"params": {
"email": email,
"soft": soft
}
}
res = run_cmd(params)
if res is True:
print(f"Deleted")
else:
print(f"No profile found with email {email}")
def search_profile(email):
params = {
"cmd": "search-profile",
"params": {
"email": email,
}
}
res = run_cmd(params)
if isinstance(res, list):
print(tabulate(res, headers="keys"))
def derive_password(password):
params = {
"cmd": "derive-password",
@@ -107,11 +140,13 @@ def derive_password(password):
res = run_cmd(params)
print(f"Derived password: \"{res}\"")
available_commands = [
available_commands = (
"create-profile",
"update-profile",
"derive-password"
]
"delete-profile",
"search-profile",
"derive-password",
)
parser = argparse.ArgumentParser(
description=(
@@ -121,10 +156,11 @@ parser = argparse.ArgumentParser(
parser.add_argument("-V", "--version", action="version", version="Penpot CLI %%develop%%")
parser.add_argument("action", action="store", choices=available_commands)
parser.add_argument("-n", "--fullname", help="Fullname", action="store")
parser.add_argument("-e", "--email", help="Email", action="store")
parser.add_argument("-p", "--password", help="Password", action="store")
parser.add_argument("-c", "--connect", help="Connect to PREPL", action="store", default="tcp://localhost:6063")
parser.add_argument("-f", "--force", help="force operation", action="store_true")
parser.add_argument("-n", "--fullname", help="fullname", action="store")
parser.add_argument("-e", "--email", help="email", action="store")
parser.add_argument("-p", "--password", help="password", action="store")
parser.add_argument("-c", "--connect", help="connect to PREPL", action="store", default="tcp://localhost:6063")
args = parser.parse_args()
@@ -165,3 +201,19 @@ elif args.action == "derive-password":
password = getpass("Password: ")
derive_password(password)
elif args.action == "delete-profile":
email = args.email
soft = not args.force
if email is None:
email = input("Email: ")
delete_profile(email, soft)
elif args.action == "search-profile":
email = args.email
if email is None:
email = input("Email: ")
search_profile(email)

View File

@@ -4,7 +4,15 @@ export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-registration
enable-login-with-password
enable-login-with-oidc \
enable-login-with-google \
enable-login-with-github \
enable-login-with-gitlab \
enable-backend-asserts \
enable-fdata-storage-pointer-map \
enable-fdata-storage-objets-map \
enable-audit-log \
enable-transit-readable-response \
enable-demo-users \
@@ -33,7 +41,7 @@ export PENPOT_FLAGS="\
# Initialize MINIO config
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
mc admin user add penpot-s3 penpot-devenv penpot-devenv
mc admin policy set penpot-s3 readwrite user=penpot-devenv
mc admin policy attach penpot-s3 readwrite --user=penpot-devenv
mc mb penpot-s3/penpot -p
export AWS_ACCESS_KEY_ID=penpot-devenv
@@ -42,19 +50,39 @@ export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
#-J-Djdk.virtualThreadScheduler.parallelism=16
export OPTIONS="
-A:jmx-remote -A:dev \
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-J-Djdk.attach.allowAttachSelf \
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
-J-Xms50m \
-J-Xmx1024m \
-J-XX:+UseZGC \
-J-XX:-OmitStackTraceInFastThrow \
-J-XX:+UnlockDiagnosticVMOptions \
-J-XX:+DebugNonSafepoints";
-J-XX:+DebugNonSafepoints \
-J-Djdk.tracePinnedThreads=full \
-J--enable-preview";
# Uncomment for use the ImageMagick v7.x
# Setup HEAP
export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m"
# export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch"
# Increase virtual thread pool size
# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16"
# Disable C2 Compiler
# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1"
# Disable all compilers
# export OPTIONS="$OPTIONS -J-Xint"
# Setup GC
export OPTIONS="$OPTIONS -J-XX:+UseG1GC"
# Setup GC
# export OPTIONS="$OPTIONS -J-XX:+UseZGC"
# Enable ImageMagick v7.x support
# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
export OPTIONS_EVAL="nil"

View File

@@ -18,7 +18,7 @@ if [ -f ./environ ]; then
source ./environ
fi
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow $JVM_OPTS"
export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --enable-preview $JVM_OPTS"
set -x
exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main

View File

@@ -2,7 +2,20 @@
export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp enable-webhooks"
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-prepl-server \
enable-urepl-server \
enable-webhooks \
enable-backend-asserts \
enable-audit-log \
enable-transit-readable-response \
enable-demo-users \
enable-fdata-storage-pointer-map \
enable-fdata-storage-objets-map \
disable-secure-session-cookies \
enable-smtp \
enable-access-tokens";
set -ex

View File

@@ -6,15 +6,19 @@
(ns app.auth
(:require
[buddy.hashers :as hashers]))
[app.config :as cf]
[buddy.hashers :as hashers]
[cuerdas.core :as str]))
(def default-params
{:alg :argon2id
:memory 32768 ;; 32 MiB
:iterations 3
:parallelism 2})
(defn derive-password
[password]
(hashers/derive password
{:alg :argon2id
:memory 16384
:iterations 20
:parallelism 2}))
(hashers/derive password default-params))
(defn verify-password
[attempt password]
@@ -24,3 +28,16 @@
{: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,6 +7,7 @@
(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,7 +18,6 @@
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.http.middleware :as hmw]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
@@ -25,14 +25,13 @@
[app.tokens :as tokens]
[app.util.json :as json]
[app.util.time :as dt]
[app.worker :as wrk]
[buddy.sign.jwk :as jwk]
[buddy.sign.jwt :as jwt]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.response :as yrs]))
[yetti.response :as-alias yrs]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
@@ -51,36 +50,29 @@
(defn- discover-oidc-config
[cfg {:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (ex/try! (http/req! cfg
{:method :get :uri (str discovery-uri)}
{:sync? true}))]
(cond
(ex/exception? response)
(do
(l/warn :hint "unable to discover oidc configuration"
:discover-uri (str discovery-uri)
:cause response)
nil)
(= 200 (:status response))
(let [data (json/decode (:body response))
(let [uri (dm/str (u/join base-uri ".well-known/openid-configuration"))
rsp (http/req! cfg {:method :get :uri uri} {:sync? true})]
(if (= 200 (:status rsp))
(let [data (-> rsp :body json/decode)
token-uri (get data :token_endpoint)
auth-uri (get data :authorization_endpoint)
user-uri (get data :userinfo_endpoint)]
user-uri (get data :userinfo_endpoint)
jwks-uri (get data :jwks_uri)]
(l/debug :hint "oidc uris discovered"
:token-uri token-uri
:auth-uri auth-uri
:user-uri user-uri)
:user-uri user-uri
:jwks-uri jwks-uri)
{:token-uri token-uri
:auth-uri auth-uri
:user-uri user-uri})
:else
:user-uri user-uri
:jwks-uri jwks-uri})
(do
(l/warn :hint "unable to discover OIDC configuration"
:uri (str discovery-uri)
:response-status-code (:status response))
:discover-uri uri
:http-status (:status rsp))
nil))))
(defn- prepare-oidc-opts
@@ -91,6 +83,7 @@
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:jwks-uri (cf/get :oidc-jwks-uri)
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
@@ -105,8 +98,42 @@
(string? (:user-uri opts))
(string? (:auth-uri opts)))
opts
(some-> (discover-oidc-config cfg opts)
(merge opts {:discover? true}))))))
(try
(-> (discover-oidc-config cfg opts)
(merge opts {:discover? true}))
(catch Throwable cause
(l/warn :hint "unable to discover OIDC configuration"
:cause cause)))))))
(defn- process-oidc-jwks
[keys]
(reduce (fn [result {:keys [kid] :as kdata}]
(let [pkey (ex/try! (jwk/public-key kdata))]
(if (ex/exception? pkey)
(do
(l/warn :hint "unable to create public key"
:kid (:kid kdata)
:cause pkey)
result)
(assoc result kid pkey))))
{}
keys))
(defn- fetch-oidc-jwks
[cfg {:keys [jwks-uri]}]
(when jwks-uri
(try
(let [{:keys [status body]} (http/req! cfg {:method :get :uri jwks-uri} {:sync? true})]
(if (= 200 status)
(-> 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)
nil)))
(catch Throwable cause
(l/warn :hint "unable to retrieve JWKs (unexpected exception)"
:cause cause)))))
(defmethod ig/pre-init-spec ::providers/generic [_]
(s/keys :req [::http/client]))
@@ -115,7 +142,7 @@
[_ cfg]
(when (contains? cf/flags :login-with-oidc)
(if-let [opts (prepare-oidc-opts cfg)]
(do
(let [jwks (fetch-oidc-jwks cfg opts)]
(l/info :hint "provider initialized"
:provider "oidc"
:method (if (:discover? opts) "discover" "manual")
@@ -126,8 +153,9 @@
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts))
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")
nil))))
@@ -166,20 +194,22 @@
(defn- retrieve-github-email
[cfg tdata props]
(or (some-> props :github/email p/resolved)
(->> (http/req! cfg
{:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get})
(p/map (fn [{:keys [status body] :as response}]
(when-not (s/int-in-range? 200 300 status)
(ex/raise :type :internal
:code :unable-to-retrieve-github-emails
:hint "unable to retrieve github emails"
:http-status status
:http-body body))
(->> response :body json/decode (filter :primary) first :email))))))
(or (some-> props :github/email)
(let [params {:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:token/type tdata) " " (:token/access tdata))}
:timeout 6000
:method :get}
{:keys [status body]} (http/req! cfg params {:sync? true})]
(when-not (s/int-in-range? 200 300 status)
(ex/raise :type :internal
:code :unable-to-retrieve-github-emails
:hint "unable to retrieve github emails"
:http-status status
:http-body body))
(->> body json/decode (filter :primary) first :email))))
(defmethod ig/pre-init-spec ::providers/github [_]
(s/keys :req [::http/client]))
@@ -275,7 +305,7 @@
{}
props))
(defn retrieve-access-token
(defn fetch-access-token
[{:keys [provider] :as cfg} code]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
@@ -295,85 +325,83 @@
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(->> (http/req! cfg req)
(p/map (fn [{:keys [status body] :as res}]
(l/trace :hint "access token response"
:status status
:body body)
(if (= status 200)
(let [data (json/decode body)]
{:token (get data :access_token)
:type (get data :token_type)})
(ex/raise :type :internal
:code :unable-to-retrieve-token
:http-status status
:http-body body)))))))
(let [{:keys [status body]} (http/req! cfg req {:sync? true})]
(l/trace :hint "access token response" :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)})
(defn- retrieve-user-info
[{:keys [provider] :as cfg} tdata]
(letfn [(retrieve []
(l/trace :hint "request user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token tdata))
:token-type (:type tdata))
(http/req! cfg
{:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}))
(ex/raise :type :internal
:code :unable-to-retrieve-token
:hint "unable to retrieve token"
:http-status status
:http-body body)))))
(validate-response [response]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
response)
(get-email [props]
(defn- process-user-info
[provider tdata info]
(letfn [(get-email [props]
;; Allow providers hook into this for custom email
;; retrieval method.
(if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata props)
(let [attr-kw (cf/get :oidc-email-attr "email")
attr-ph (parse-attr-path provider attr-kw)]
(p/resolved (get-in props attr-ph)))))
(get-in props attr-ph))))
(get-name [info]
(get-name [props]
(let [attr-kw (cf/get :oidc-name-attr "name")
attr-ph (parse-attr-path provider attr-kw)]
(get-in info attr-ph)))
(get-in props attr-ph)))
]
(process-response [response]
(p/let [info (-> response :body json/decode)
props (qualify-props provider info)
email (get-email props)]
(let [props (qualify-props provider info)
email (get-email props)]
{:backend (:name provider)
:fullname (or (get-name props) email)
:email email
:props props})))
{:backend (:name provider)
:fullname (or (get-name props) email)
:email email
: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)))
(validate-info [info]
(l/trace :hint "authentication info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
:info (pr-str info))
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
info)]
(let [params {:uri (:user-uri provider)
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
:timeout 6000
:method :get}
response (http/req! cfg params {:sync? true})]
(->> (retrieve)
(p/fmap validate-response)
(p/mcat process-response)
(p/fmap validate-info))))
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
(-> response :body json/decode)))
(defn- get-user-info
[{:keys [provider]} tdata]
(try
(when (:token/id tdata)
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
(when-let [key (if (str/starts-with? (name alg) "hs")
(:client-secret provider)
(get-in provider [:jwks kid]))]
(let [claims (jwt/unsign (:token/id tdata) key {:alg alg})]
(dissoc claims :exp :iss :iat :sid :aud :sub)))))
(catch Throwable cause
(l/warn :hint "unable to get user info from JWT token (unexpected exception)"
:cause cause))))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
@@ -386,69 +414,93 @@
::props]))
(defn get-info
[{:keys [provider] :as cfg} {:keys [params] :as request}]
(letfn [(validate-oidc [{:keys [props] :as info}]
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
(when (and (= "oidc" (:name provider))
(seq (:roles provider)))
(let [expected-roles (into #{} (:roles provider))
current-roles (let [roles-kw (cf/get :oidc-roles-attr "roles")
roles-ph (parse-attr-path provider roles-kw)
roles (get-in props roles-ph)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
[{:keys [provider ::main/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)))
;; check if profile has a configured set of roles
(when-not (set/subset? expected-roles current-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enough permissions"))))
info)
(let [state (get params :state)
code (get params :code)
state (tokens/verify props {:token state :iss :oauth})
tdata (fetch-access-token cfg code)
info (case (cf/get :oidc-user-info-source)
:token (get-user-info cfg tdata)
:userinfo (fetch-user-info cfg tdata)
(or (get-user-info cfg tdata)
(fetch-user-info cfg tdata)))
(post-process [state info]
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state))
info (process-user-info provider tdata info)]
;; 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))))]
(l/trace :hint "user info" :info info)
(when-let [error (get params :error)]
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
(ex/raise :type :internal
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
(let [state (get params :state)
code (get params :code)
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})]
(-> (p/resolved code)
(p/then #(retrieve-access-token cfg %))
(p/then #(retrieve-user-info cfg %))
(p/then' validate-oidc)
(p/then' (partial post-process state))))))
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
(when (and (= "oidc" (:name provider))
(seq (:roles provider)))
(let [expected-roles (into #{} (:roles provider))
current-roles (let [roles-kw (cf/get :oidc-roles-attr "roles")
roles-ph (parse-attr-path provider roles-kw)
roles (get-in (:props info) roles-ph)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
;; check if profile has a configured set of roles
(when-not (set/subset? expected-roles current-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enough permissions"))))
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token 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 ::wrk/executor] :as cfg} info]
(px/with-dispatch executor
(with-open [conn (db/open pool)]
(some->> (:email info)
(profile/get-profile-by-email conn)))))
[{:keys [::db/pool] :as cfg} info]
(dm/with-open [conn (db/open pool)]
(some->> (:email info)
(profile/get-profile-by-email conn))))
(defn- redirect-response
[uri]
(yrs/response :status 302 :headers {"location" (str uri)}))
{::yrs/status 302
::yrs/headers {"location" (str uri)}})
(defn- generate-error-redirect
[_ error]
(let [uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
[_ 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)))
params {:error "unable-to-auth"
:hint hint
:type type
:code code}
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string params)))]
(redirect-response uri)))
(defn- generate-redirect
@@ -469,27 +521,32 @@
(ex/raise :type :restriction
:code :profile-blocked))
(audit/submit! cfg {:type "command"
:name "login-with-password"
:profile-id (:id profile)
:ip-addr (audit/parse-client-ip request)
:props (audit/profile->props profile)})
(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)})
(->> (redirect-response uri)
(sxf request)))
(let [info (assoc info
:iss :prepared-register
:is-active true
:exp (dt/in-future {:hours 48}))
token (tokens/generate (::main/props cfg) info)
params (d/without-nils
{:token token
:fullname (:fullname info)})
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))))
(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 (::main/props cfg) info)
params (d/without-nils
{:token token
:fullname (:fullname info)})
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))
(generate-error-redirect cfg "email-domain-not-allowed"))))
(defn- auth-handler
[cfg {:keys [params] :as request}]
@@ -500,27 +557,24 @@
:props props
:exp (dt/in-future "4h")})
uri (build-auth-uri cfg state)]
(yrs/response 200 {:redirect-uri uri})))
{::yrs/status 200
::yrs/body {:redirect-uri uri}}))
(defn- callback-handler
[cfg request]
(letfn [(process-request []
(p/let [info (get-info cfg request)
profile (get-profile cfg info)]
(generate-redirect cfg request info profile)))
(handle-error [cause]
(l/error :hint "error on oauth process" :cause cause)
(generate-error-redirect cfg cause))]
(-> (process-request)
(p/catch handle-error))))
(try
(let [info (get-info cfg request)
profile (get-profile cfg info)]
(generate-redirect cfg request info profile))
(catch Throwable cause
(l/warn :hint "error on oauth process" :cause cause)
(generate-error-redirect cfg cause))))
(def provider-lookup
{:compile
(fn [& _]
(fn [handler]
(fn [{:keys [::providers] :as cfg} request]
(fn [handler {:keys [::providers] :as cfg}]
(fn [request]
(let [provider (some-> request :path-params :provider keyword)]
(if-let [provider (get providers provider)]
(handler (assoc cfg :provider provider) request)
@@ -564,18 +618,15 @@
[_]
(s/keys :req [::session/manager
::http/client
::wrk/executor
::main/props
::db/pool
::providers]))
(defmethod ig/init-key ::routes
[_ {:keys [::wrk/executor] :as cfg}]
[_ cfg]
(let [cfg (update cfg :provider d/without-nils)]
["" {:middleware [[session/authz cfg]
[hmw/with-dispatch executor]
[hmw/with-config cfg]
[provider-lookup]]}
[provider-lookup cfg]]}
["/auth/oauth"
["/:provider"
{:handler auth-handler

View File

@@ -1,169 +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.cli.manage
"A manage cli api."
(:require
[app.common.logging :as l]
[app.db :as db]
[app.main :as main]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[clojure.string :as str]
[clojure.tools.cli :refer [parse-opts]]
[integrant.core :as ig])
(:import
java.io.Console))
;; --- IMPL
(defn init-system
[]
(let [data (-> main/system-config
(select-keys [:app.db/pool :app.metrics/metrics])
(assoc :app.migrations/all {}))]
(-> data ig/prep ig/init)))
(defn- read-from-console
[{:keys [label type] :or {type :text}}]
(let [^Console console (System/console)]
(when-not console
(l/error :hint "no console found, can proceed")
(System/exit 1))
(binding [*out* (.writer console)]
(print label " ")
(.flush *out*))
(case type
:text (.readLine console)
:password (String. (.readPassword console)))))
(defn create-profile
[options]
(let [system (init-system)
email (or (:email options)
(read-from-console {:label "Email:"}))
fullname (or (:fullname options)
(read-from-console {:label "Full Name:"}))
password (or (:password options)
(read-from-console {:label "Password:"
:type :password}))]
(try
(db/with-atomic [conn (:app.db/pool system)]
(->> (auth/create-profile! conn
{:fullname fullname
:email email
:password password
:is-active true
:is-demo false})
(auth/create-profile-rels! conn)))
(when (pos? (:verbosity options))
(println "User created successfully."))
(System/exit 0)
(catch Exception _e
(when (pos? (:verbosity options))
(println "Unable to create user, already exists."))
(System/exit 1)))))
(defn reset-password
[options]
(let [system (init-system)]
(try
(db/with-atomic [conn (:app.db/pool system)]
(let [email (or (:email options)
(read-from-console {:label "Email:"}))
profile (profile/get-profile-by-email conn email)]
(when-not profile
(when (pos? (:verbosity options))
(println "Profile does not exists."))
(System/exit 1))
(let [password (or (:password options)
(read-from-console {:label "Password:"
:type :password}))]
(profile/update-profile-password! conn (assoc profile :password password))
(when (pos? (:verbosity options))
(println "Password changed successfully.")))))
(System/exit 0)
(catch Exception e
(when (pos? (:verbosity options))
(println "Unable to change password."))
(when (= 2 (:verbosity options))
(.printStackTrace e))
(System/exit 1)))))
;; --- CLI PARSE
(def cli-options
;; An option with a required argument
[["-u" "--email EMAIL" "Email Address"]
["-p" "--password PASSWORD" "Password"]
["-n" "--name FULLNAME" "Full Name"
:id :fullname]
["-v" nil "Verbosity level"
:id :verbosity
:default 1
:update-fn inc]
["-q" nil "Don't print to console"
:id :verbosity
:update-fn (constantly 0)]
["-h" "--help"]])
(defn usage
[options-summary]
(->> ["Penpot CLI management."
""
"Usage: manage [options] action"
""
"Options:"
options-summary
""
"Actions:"
" create-profile Create new profile."
" reset-password Reset profile password."
""]
(str/join \newline)))
(defn error-msg [errors]
(str "The following errors occurred while parsing your command:\n\n"
(str/join \newline errors)))
(defn validate-args
"Validate command line arguments. Either return a map indicating the program
should exit (with a error message, and optional ok status), or a map
indicating the action the program should take and the options provided."
[args]
(let [{:keys [options arguments errors summary] :as opts} (parse-opts args cli-options)]
(cond
(:help options) ; help => exit OK with usage summary
{:exit-message (usage summary) :ok? true}
errors ; errors => exit with description of errors
{:exit-message (error-msg errors)}
;; custom validation on arguments
:else
(let [action (first arguments)]
(if (#{"create-profile" "reset-password"} action)
{:action (first arguments) :options options}
{:exit-message (usage summary)})))))
(defn exit [status msg]
(println msg)
(System/exit status))
(defn -main
[& args]
(let [{:keys [action options exit-message ok?]} (validate-args args)]
(if exit-message
(exit (if ok? 0 1) exit-message)
(case action
"create-profile" (create-profile options)
"reset-password" (reset-password options)))))

View File

@@ -146,11 +146,13 @@
(s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string)
(s/def ::oidc-client-id ::us/string)
(s/def ::oidc-user-info-source ::us/keyword)
(s/def ::oidc-client-secret ::us/string)
(s/def ::oidc-base-uri ::us/string)
(s/def ::oidc-token-uri ::us/string)
(s/def ::oidc-auth-uri ::us/string)
(s/def ::oidc-user-uri ::us/string)
(s/def ::oidc-jwks-uri ::us/string)
(s/def ::oidc-scopes ::us/set-of-strings)
(s/def ::oidc-roles ::us/set-of-strings)
(s/def ::oidc-roles-attr ::us/string)
@@ -241,10 +243,12 @@
::google-client-secret
::oidc-client-id
::oidc-client-secret
::oidc-user-info-source
::oidc-base-uri
::oidc-token-uri
::oidc-auth-uri
::oidc-user-uri
::oidc-jwks-uri
::oidc-scopes
::oidc-roles-attr
::oidc-email-attr
@@ -323,6 +327,7 @@
(def default-flags
[:enable-backend-api-doc
:enable-backend-openapi-doc
:enable-backend-worker
:enable-secure-session-cookies
:enable-email-verification])

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.db
(:refer-clojure :exclude [get])
(:refer-clojure :exclude [get run!])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
@@ -145,6 +145,10 @@
[v]
(instance? javax.sql.DataSource v))
(defn connection?
[conn]
(instance? Connection conn))
(s/def ::conn some?)
(s/def ::nilable-pool (s/nilable ::pool))
(s/def ::pool pool?)
@@ -218,52 +222,71 @@
(defmacro with-atomic
[& args]
`(jdbc/with-transaction ~@args))
(if (symbol? (first args))
(let [cfgs (first args)
body (rest args)]
`(jdbc/with-transaction [conn# (::pool ~cfgs)]
(let [~cfgs (assoc ~cfgs ::conn conn#)]
~@body)))
`(jdbc/with-transaction ~@args)))
(defn open
[pool]
(jdbc/get-connection pool))
(defn- resolve-connectable
[o]
(if (connection? o)
o
(if (pool? o)
o
(or (::conn o) (::pool o)))))
(def ^:private default-opts
{:builder-fn sql/as-kebab-maps})
(defn exec!
([ds sv]
(jdbc/execute! ds sv default-opts))
(-> (resolve-connectable ds)
(jdbc/execute! sv default-opts)))
([ds sv opts]
(jdbc/execute! ds sv (merge default-opts opts))))
(-> (resolve-connectable ds)
(jdbc/execute! sv (merge default-opts opts)))))
(defn exec-one!
([ds sv]
(jdbc/execute-one! ds sv default-opts))
(-> (resolve-connectable ds)
(jdbc/execute-one! sv default-opts)))
([ds sv opts]
(jdbc/execute-one! ds sv
(-> (merge default-opts opts)
(assoc :return-keys (::return-keys? opts false))))))
(-> (resolve-connectable ds)
(jdbc/execute-one! sv
(-> (merge default-opts opts)
(assoc :return-keys (::return-keys? opts false)))))))
(defn insert!
[ds table params & {:as opts}]
(exec-one! ds
(sql/insert table params opts)
(merge {::return-keys? true} opts)))
(-> (resolve-connectable ds)
(exec-one! (sql/insert table params opts)
(merge {::return-keys? true} opts))))
(defn insert-multi!
[ds table cols rows & {:as opts}]
(exec! ds
(sql/insert-multi table cols rows opts)
(merge {::return-keys? true} opts)))
(-> (resolve-connectable ds)
(exec! (sql/insert-multi table cols rows opts)
(merge {::return-keys? true} opts))))
(defn update!
[ds table params where & {:as opts}]
(exec-one! ds
(sql/update table params where opts)
(merge {::return-keys? true} opts)))
(-> (resolve-connectable ds)
(exec-one! (sql/update table params where opts)
(merge {::return-keys? true} opts))))
(defn delete!
[ds table params & {:as opts}]
(exec-one! ds
(sql/delete table params opts)
(merge {::return-keys? true} opts)))
(-> (resolve-connectable ds)
(exec-one! (sql/delete table params opts)
(merge {::return-keys? true} opts))))
(defn is-row-deleted?
[{:keys [deleted-at]}]
@@ -293,6 +316,11 @@
:hint "database object not found"))
row))
(defn plan
[ds sql]
(-> (resolve-connectable ds)
(jdbc/plan sql sql/default-opts)))
(defn get-by-id
[ds table id & {:as opts}]
(get ds table {:id id} opts))
@@ -367,12 +395,62 @@
([^Connection conn label]
(.setSavepoint conn (name label))))
(defn release!
[^Connection conn ^Savepoint sp ]
(.releaseSavepoint conn sp))
(defn rollback!
([^Connection conn]
(.rollback conn))
([^Connection conn ^Savepoint sp]
(.rollback conn sp)))
(defn tx-run!
[cfg f]
(cond
(connection? cfg)
(tx-run! {::conn cfg} f)
(pool? cfg)
(tx-run! {::pool cfg} f)
(::conn cfg)
(let [conn (::conn cfg)
sp (savepoint conn)]
(try
(let [result (f cfg)]
(release! conn sp)
result)
(catch Throwable cause
(rollback! sp)
(throw cause))))
(::pool cfg)
(with-atomic [conn (::pool cfg)]
(f (assoc cfg ::conn conn)))
:else
(throw (IllegalArgumentException. "invalid arguments"))))
(defn run!
[cfg f]
(cond
(connection? cfg)
(run! {::conn cfg} f)
(pool? cfg)
(run! {::pool cfg} f)
(::conn cfg)
(f cfg)
(::pool cfg)
(with-open [^Connection conn (open (::pool cfg))]
(f (assoc cfg ::conn conn)))
:else
(throw (IllegalArgumentException. "invalid arguments"))))
(defn interval
[o]
(cond

View File

@@ -37,6 +37,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- parse-address
^"[Ljakarta.mail.internet.InternetAddress;"
[v]
(InternetAddress/parse ^String v))
@@ -149,6 +150,7 @@
"mail.smtp.connectiontimeout" timeout}))
(defn- create-smtp-session
^Session
[cfg]
(let [props (opts->props cfg)]
(Session/getInstance props)))
@@ -303,7 +305,7 @@
(fn [params]
(when (contains? cf/flags :smtp)
(let [session (create-smtp-session cfg)]
(with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))]
(with-open [transport (.getTransport session (if (::ssl cfg) "smtps" "smtp"))]
(.connect ^Transport transport
^String (::username cfg)
^String (::password cfg))
@@ -339,7 +341,7 @@
(map :content)
first)))
(println "******** end email" (:id email) "**********"))]
(l/info ::l/raw out)))
(l/raw! :info out)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EMAIL FACTORIES

View File

@@ -19,19 +19,21 @@
[app.http.middleware :as mw]
[app.http.session :as session]
[app.http.websocket :as-alias ws]
[app.main :as-alias main]
[app.metrics :as mtx]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]
[reitit.core :as r]
[reitit.middleware :as rr]
[yetti.adapter :as yt]
[yetti.request :as yrq]
[yetti.response :as yrs]))
[yetti.response :as-alias yrs]))
(declare wrap-router)
(declare router-handler)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP SERVER
@@ -71,13 +73,17 @@
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/io-threads (::io-threads cfg)
:xnio/dispatch (::wrk/executor cfg)
:xnio/io-threads (or (::io-threads cfg)
(max 3 (px/get-available-processors)))
:xnio/worker-threads (or (::worker-threads cfg)
(max 6 (px/get-available-processors)))
:xnio/dispatch true
:socket/backlog 4069
:ring/async true}
handler (cond
(some? router)
(wrap-router router)
(router-handler router)
(some? handler)
handler
@@ -97,32 +103,35 @@
(defn- not-found-handler
[_ respond _]
(respond (yrs/response 404)))
(respond {::yrs/status 404}))
(defn- wrap-router
(defn- router-handler
[router]
(letfn [(handler [request respond raise]
(letfn [(resolve-handler [request]
(if-let [match (r/match-by-path router (yrq/path request))]
(let [params (:path-params match)
result (:result match)
handler (or (:handler result) not-found-handler)
request (assoc request :path-params params)]
(handler request respond raise))
(not-found-handler request respond raise)))
(partial handler request))
(partial not-found-handler request)))
(on-error [cause request respond]
(on-error [cause request]
(let [{:keys [body] :as response} (errors/handle cause request)]
(respond
(cond-> response
(map? body)
(-> (update :headers assoc "content-type" "application/transit+json")
(assoc :body (t/encode-str body {:type :json-verbose})))))))]
(cond-> response
(map? body)
(-> (update ::yrs/headers assoc "content-type" "application/transit+json")
(assoc ::yrs/body (t/encode-str body {:type :json-verbose}))))))]
(fn [request respond _]
(try
(handler request respond #(on-error % request respond))
(catch Throwable cause
(on-error cause request respond))))))
(let [handler (resolve-handler request)
exchange (yrq/exchange request)]
(handler
(fn [response]
(yt/dispatch! exchange (partial respond response)))
(fn [cause]
(let [response (on-error cause request)]
(yt/dispatch! exchange (partial respond response)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP ROUTER
@@ -130,11 +139,11 @@
(defmethod ig/pre-init-spec ::router [_]
(s/keys :req [::session/manager
::actoken/manager
::ws/routes
::rpc/routes
::rpc.doc/routes
::oidc/routes
::main/props
::assets/routes
::debug/routes
::db/pool
@@ -145,13 +154,14 @@
[_ cfg]
(rr/router
[["" {:middleware [[mw/server-timing]
[mw/format-response]
[mw/params]
[mw/format-response]
[mw/parse-request]
[session/soft-auth cfg]
[actoken/soft-auth cfg]
[mw/errors errors/handle]
[mw/restrict-methods]]}
[mw/restrict-methods]
[mw/with-dispatch :vthread]]}
(::mtx/routes cfg)
(::assets/routes cfg)

View File

@@ -7,26 +7,12 @@
(ns app.http.access-token
(:require
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.main :as-alias main]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]))
(s/def ::manager
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
(defmethod ig/pre-init-spec ::manager [_] ::manager)
(defmethod ig/init-key ::manager [_ cfg] cfg)
(defmethod ig/halt-key! ::manager [_ _])
(def header-re #"^Token\s+(.*)")
(defn- get-token
@@ -40,48 +26,50 @@
(when token
(tokens/verify props {:token token :iss "access-token"})))
(defn- get-token-perms
(def sql:get-token-data
"SELECT perms, profile_id, expires_at
FROM access_token
WHERE id = ?
AND (expires_at IS NULL
OR (expires_at > now()));")
(defn- get-token-data
[pool token-id]
(when-not (db/read-only? pool)
(when-let [token (db/get* pool :access-token {:id token-id} {:columns [:perms]})]
(some-> (:perms token)
(db/decode-pgarray #{})))))
(some-> (db/exec-one! pool [sql:get-token-data token-id])
(update :perms db/decode-pgarray #{}))))
(defn- wrap-soft-auth
[handler {:keys [::manager]}]
(us/assert! ::manager manager)
"Soft Authentication, will be executed synchronously on the undertow
worker thread."
[handler {:keys [::main/props]}]
(letfn [(handle-request [request]
(try
(let [token (get-token request)
claims (decode-token props token)]
(cond-> request
(map? claims)
(assoc ::id (:tid claims))))
(catch Throwable cause
(l/trace :hint "exception on decoding malformed token" :cause cause)
request)))]
(let [{:keys [::wrk/executor ::main/props]} manager]
(fn [request respond raise]
(let [token (get-token request)]
(->> (px/submit! executor (partial decode-token props token))
(p/fnly (fn [claims cause]
(when cause
(l/trace :hint "exception on decoding malformed token" :cause cause))
(let [request (cond-> request
(map? claims)
(assoc ::id (:tid claims)))]
(handler request respond raise)))))))))
(let [request (handle-request request)]
(handler request respond raise)))))
(defn- wrap-authz
[handler {:keys [::manager]}]
(us/assert! ::manager manager)
(let [{:keys [::wrk/executor ::db/pool]} manager]
(fn [request respond raise]
(if-let [token-id (::id request)]
(->> (px/submit! executor (partial get-token-perms pool token-id))
(p/fnly (fn [perms cause]
(cond
(some? cause)
(raise cause)
(nil? perms)
(handler request respond raise)
:else
(let [request (assoc request ::perms perms)]
(handler request respond raise))))))
(handler request respond raise)))))
"Authorization middleware, will be executed synchronously on vthread."
[handler {:keys [::db/pool]}]
(fn [request]
(let [{:keys [perms profile-id expires-at]} (some->> (::id request) (get-token-data pool))]
(handler (cond-> request
(some? perms)
(assoc ::perms perms)
(some? profile-id)
(assoc ::profile-id profile-id)
(some? expires-at)
(assoc ::expires-at expires-at))))))
(def soft-auth
{:name ::soft-auth

View File

@@ -14,11 +14,9 @@
[app.db :as db]
[app.storage :as sto]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[yetti.response :as yrs]))
[yetti.response :as-alias yrs]))
(def ^:private cache-max-age
(dt/duration {:hours 24}))
@@ -28,10 +26,9 @@
(defn get-id
[{:keys [path-params]}]
(if-let [id (some-> path-params :id d/parse-uuid)]
(p/resolved id)
(p/rejected (ex/error :type :not-found
:hunt "object not found"))))
(or (some-> path-params :id d/parse-uuid)
(ex/raise :type :not-found
:hunt "object not found")))
(defn- get-file-media-object
[pool id]
@@ -39,16 +36,12 @@
(defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj]
(let [mdata (meta obj)]
(->> (sto/get-object-url storage obj {:max-age signature-max-age})
(p/fmap (fn [{:keys [host port] :as url}]
(let [headers {"location" (str url)
"x-host" (cond-> host port (str ":" port))
"x-mtype" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
(yrs/response
:status 307
:headers headers)))))))
(let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
{::yrs/status 307
::yrs/headers {"location" (str url)
"x-host" (cond-> host port (str ":" port))
"x-mtype" (-> obj meta :content-type)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}}))
(defn- serve-object-from-fs
[{:keys [::path]} obj]
@@ -58,8 +51,8 @@
headers {"x-accel-redirect" (:path purl)
"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
(p/resolved
(yrs/response :status 204 :headers headers))))
{::yrs/status 204
::yrs/headers headers}))
(defn- serve-object
"Helper function that returns the appropriate response depending on
@@ -72,42 +65,34 @@
(defn objects-handler
"Handler that servers storage objects by id."
[{:keys [::sto/storage ::wrk/executor] :as cfg} request respond raise]
(->> (get-id request)
(p/mcat executor (fn [id] (sto/get-object storage id)))
(p/mcat executor (fn [obj]
(if (some? obj)
(serve-object cfg obj)
(p/resolved (yrs/response 404)))))
(p/fnly executor (fn [result cause]
(if cause (raise cause) (respond result))))))
[{:keys [::sto/storage] :as cfg} request]
(let [id (get-id request)
obj (sto/get-object storage id)]
(if obj
(serve-object cfg obj)
{::yrs/status 404})))
(defn- generic-handler
"A generic handler helper/common code for file-media based handlers."
[{:keys [::sto/storage ::wrk/executor] :as cfg} request kf]
(let [pool (::db/pool storage)]
(->> (get-id request)
(p/fmap executor (fn [id] (get-file-media-object pool id)))
(p/mcat executor (fn [mobj] (sto/get-object storage (kf mobj))))
(p/mcat executor (fn [sobj]
(if sobj
(serve-object cfg sobj)
(p/resolved (yrs/response 404))))))))
[{:keys [::sto/storage] :as cfg} request kf]
(let [pool (::db/pool storage)
id (get-id request)
mobj (get-file-media-object pool id)
sobj (sto/get-object storage (kf mobj))]
(if sobj
(serve-object cfg sobj)
{::yrs/status 404})))
(defn file-objects-handler
"Handler that serves storage objects by file media id."
[cfg request respond raise]
(->> (generic-handler cfg request :media-id)
(p/fnly (fn [result cause]
(if cause (raise cause) (respond result))))))
[cfg request]
(generic-handler cfg request :media-id))
(defn file-thumbnails-handler
"Handler that serves storage objects by thumbnail-id and quick
fallback to file-media-id if no thumbnail is available."
[cfg request respond raise]
(->> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
(p/fnly (fn [result cause]
(if cause (raise cause) (respond result))))))
[cfg request]
(generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))))
;; --- Initialization
@@ -115,7 +100,7 @@
(s/def ::routes vector?)
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::sto/storage ::wrk/executor ::path]))
(s/keys :req [::sto/storage ::path]))
(defmethod ig/init-key ::routes
[_ cfg]

View File

@@ -21,7 +21,7 @@
[jsonista.core :as j]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
[yetti.response :as-alias yrs]))
(declare parse-json)
(declare handle-request)
@@ -36,10 +36,10 @@
(defmethod ig/init-key ::routes
[_ {:keys [::wrk/executor] :as cfg}]
(letfn [(handler [request respond _]
(letfn [(handler [request]
(let [data (-> request yrq/body slurp)]
(px/run! executor #(handle-request cfg data)))
(respond (yrs/response 200)))]
{::yrs/status 200})]
["/sns" {:handler handler
:allowed-methods #{:post}}]))

View File

@@ -40,12 +40,25 @@
(catch Throwable cause
(p/rejected cause))))))
(defn- resolve-client
[params]
(cond
(instance? HttpClient params)
params
(map? params)
(resolve-client (::client params))
:else
(throw (UnsupportedOperationException. "invalid arguments"))))
(defn req!
"A convencience toplevel function for gradual migration to a new API
convention."
([{:keys [::client]} request]
(us/assert! ::client client)
(send! client request {}))
([{:keys [::client]} request options]
(us/assert! ::client client)
(send! client request options)))
([cfg-or-client request]
(let [client (resolve-client cfg-or-client)]
(send! client request {})))
([cfg-or-client request options]
(let [client (resolve-client cfg-or-client)]
(send! client request options))))

View File

@@ -13,7 +13,6 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.middleware :as mw]
[app.http.session :as session]
[app.rpc.commands.binfile :as binf]
[app.rpc.commands.files-create :refer [create-file]]
@@ -22,7 +21,6 @@
[app.util.blob :as blob]
[app.util.template :as tmpl]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.io :as io]
@@ -49,13 +47,17 @@
(defn prepare-response
[body]
(let [headers {"content-type" "application/transit+json"}]
(yrs/response :status 200 :body body :headers headers)))
{::yrs/status 200
::yrs/body body
::yrs/headers headers}))
(defn prepare-download-response
[body filename]
(let [headers {"content-disposition" (str "attachment; filename=" filename)
"content-type" "application/octet-stream"}]
(yrs/response :status 200 :body body :headers headers)))
{::yrs/status 200
::yrs/body body
::yrs/headers headers}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INDEX
@@ -66,10 +68,10 @@
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(yrs/response :status 200
:headers {"content-type" "text/html"}
:body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {}))))
{::yrs/status 200
::yrs/headers {"content-type" "text/html"}
::yrs/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {}))})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES
@@ -109,14 +111,18 @@
(contains? params :clone)
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)
data (blob/decode data)]
(create-file pool {:id (uuid/next)
:name (str "Cloned file: " filename)
:project-id project-id
:profile-id profile-id
:data data})
(yrs/response 201 "OK CREATED"))
project-id (:default-project-id profile)]
(db/run! pool (fn [{:keys [::db/conn]}]
(create-file conn {:id file-id
:name (str "Cloned file: " filename)
:project-id project-id
:profile-id profile-id})
(db/update! conn :file
{:data data}
{:id file-id})
{::yrs/status 201
::yrs/body "OK CREATED"})))
:else
(prepare-response (blob/decode data))))))
@@ -130,31 +136,37 @@
[{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}]
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)
data (some-> params :file :path io/read-as-bytes blob/decode)]
data (some-> params :file :path io/read-as-bytes)]
(if (and data project-id)
(let [fname (str "Imported file *: " (dt/now))
overwrite? (contains? params :overwrite?)
file-id (or (and overwrite? (ex/ignoring (-> params :file :filename parse-uuid)))
(uuid/next))]
(let [fname (str "Imported file *: " (dt/now))
reuse-id? (contains? params :reuseid)
file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid)))
(uuid/next))]
(if (and overwrite? file-id
(if (and reuse-id? file-id
(is-file-exists? pool file-id))
(do
(db/update! pool :file
{:data (blob/encode data)}
{:data data
:deleted-at nil}
{:id file-id})
(yrs/response 200 "OK UPDATED"))
{::yrs/status 200
::yrs/body "OK UPDATED"})
(do
(create-file pool {:id file-id
:name fname
:project-id project-id
:profile-id profile-id
:data data})
(yrs/response 201 "OK CREATED"))))
(db/run! pool (fn [{:keys [::db/conn]}]
(create-file conn {:id file-id
:name fname
:project-id project-id
:profile-id profile-id})
(db/update! conn :file
{:data data}
{:id file-id})
{::yrs/status 201
::yrs/body "OK CREATED"}))))
(yrs/response 500 "ERROR"))))
{::yrs/status 500
::yrs/body "ERROR"})))
(defn file-data-handler
[cfg request]
@@ -232,6 +244,11 @@
(-> (io/resource "app/templates/error-report.v2.tmpl")
(tmpl/render report)))
(render-template-v3 [{:keys [content id created-at]}]
(-> (io/resource "app/templates/error-report.v3.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :created-at (dt/format-instant created-at :rfc1123))))))
]
(when-not (authorized? pool request)
@@ -239,21 +256,23 @@
:code :only-admins-allowed))
(if-let [report (get-report request)]
(let [result (if (= 1 (:version report))
(render-template-v1 report)
(render-template-v2 report))]
(yrs/response :status 200
:body result
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}))
(yrs/response 404 "not found"))))
(let [result (case (:version report)
1 (render-template-v1 report)
2 (render-template-v2 report)
3 (render-template-v3 report))]
{::yrs/status 200
::yrs/body result
::yrs/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}})
{::yrs/status 404
::yrs/body "not found"})))
(def sql:error-reports
"SELECT id, created_at,
content->>'~:hint' AS hint
FROM server_error_report
ORDER BY created_at DESC
LIMIT 100")
LIMIT 200")
(defn error-list-handler
[{:keys [::db/pool]} request]
@@ -262,11 +281,11 @@
:code :only-admins-allowed))
(let [items (->> (db/exec! pool [sql:error-reports])
(map #(update % :created-at dt/format-instant :rfc1123)))]
(yrs/response :status 200
:body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items}))
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})))
{::yrs/status 200
::yrs/body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items}))
::yrs/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXPORT/IMPORT
@@ -302,16 +321,15 @@
::binf/profile-id profile-id
::binf/project-id project-id))
(yrs/response
:status 200
:headers {"content-type" "text/plain"}
:body "OK CLONED"))
{::yrs/status 200
::yrs/headers {"content-type" "text/plain"}
::yrs/body "OK CLONED"})
{::yrs/status 200
::yrs/body (io/input-stream path)
::yrs/headers {"content-type" "application/octet-stream"
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}}))))
(yrs/response
:status 200
:headers {"content-type" "application/octet-stream"
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}
:body (io/input-stream path))))))
(defn import-handler
@@ -341,10 +359,9 @@
::binf/profile-id profile-id
::binf/project-id project-id))
(yrs/response
:status 200
:headers {"content-type" "text/plain"}
:body "OK")))
{::yrs/status 200
::yrs/headers {"content-type" "text/plain"}
::yrs/body "OK"}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS
@@ -355,11 +372,13 @@
[{:keys [::db/pool]} _]
(try
(db/exec-one! pool ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK")
{::yrs/status 200
::yrs/body "OK"}
(catch Throwable cause
(l/warn :hint "unable to execute query on health handler"
:cause cause)
(yrs/response 503 "KO"))))
{::yrs/status 503
::yrs/body "KO"})))
(defn changelog-handler
[_ _]
@@ -368,10 +387,11 @@
(md->html [text]
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
(if-let [clog (io/resource "changelog.md")]
(yrs/response :status 200
:headers {"content-type" "text/html; charset=utf-8"}
:body (-> clog slurp md->html))
(yrs/response :status 404 :body "NOT FOUND"))))
{::yrs/status 200
::yrs/headers {"content-type" "text/html; charset=utf-8"}
::yrs/body (-> clog slurp md->html)}
{::yrs/status 404
::yrs/body "NOT FOUND"})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INIT
@@ -381,34 +401,26 @@
{:compile
(fn [& _]
(fn [handler pool]
(fn [request respond raise]
(fn [request]
(if (authorized? pool request)
(handler request respond raise)
(raise (ex/error :type :authentication
:code :only-admins-allowed))))))})
(handler request)
(ex/raise :type :authentication
:code :only-admins-allowed)))))})
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::db/pool
::wrk/executor
::sto/storage
::session/manager]))
(s/keys :req [::db/pool ::session/manager]))
(defmethod ig/init-key ::routes
[_ {:keys [::db/pool ::wrk/executor] :as cfg}]
[["/readyz" {:middleware [[mw/with-dispatch executor]
[mw/with-config cfg]]
:handler health-handler}]
[_ {:keys [::db/pool] :as cfg}]
[["/readyz" {:handler (partial health-handler cfg)}]
["/dbg" {:middleware [[session/authz cfg]
[with-authorization pool]
[mw/with-dispatch executor]
[mw/with-config cfg]]}
["" {:handler index-handler}]
["/health" {:handler health-handler}]
["/changelog" {:handler changelog-handler}]
;; ["/error-by-id/:id" {:handler error-handler}]
["/error/:id" {:handler error-handler}]
["/error" {:handler error-list-handler}]
["/file/export" {:handler export-handler}]
["/file/import" {:handler import-handler}]
["/file/data" {:handler file-data-handler}]
["/file/changes" {:handler file-changes-handler}]]])
[with-authorization pool]]}
["" {:handler (partial index-handler cfg)}]
["/health" {:handler (partial health-handler cfg)}]
["/changelog" {:handler (partial changelog-handler cfg)}]
["/error/:id" {:handler (partial error-handler cfg)}]
["/error" {:handler (partial error-list-handler cfg)}]
["/file/export" {:handler (partial export-handler cfg)}]
["/file/import" {:handler (partial import-handler cfg)}]
["/file/data" {:handler (partial file-data-handler cfg)}]
["/file/changes" {:handler (partial file-changes-handler cfg)}]]])

View File

@@ -9,6 +9,8 @@
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.session :as-alias session]
@@ -29,14 +31,14 @@
(let [claims (-> {}
(into (::session/token-claims request))
(into (::actoken/token-claims request)))]
{:path (:path request)
:method (:method request)
:params (:params request)
:ip-addr (parse-client-ip request)
:user-agent (yrq/get-header request "user-agent")
:profile-id (:uid claims)
:version (or (yrq/get-header request "x-frontend-version")
"unknown")}))
{:request/path (:path request)
:request/method (:method request)
:request/params (:params request)
:request/user-agent (yrq/get-header request "user-agent")
:request/ip-addr (parse-client-ip request)
:request/profile-id (:uid claims)
:version/frontend (or (yrq/get-header request "x-frontend-version") "unknown")
:version/backend (:full cf/version)}))
(defmulti handle-exception
(fn [err & _rest]
@@ -46,69 +48,110 @@
(defmethod handle-exception :authentication
[err _]
(yrs/response 401 (ex-data err)))
{::yrs/status 401
::yrs/body (ex-data err)})
(defmethod handle-exception :authorization
[err _]
(yrs/response 403 (ex-data err)))
{::yrs/status 403
::yrs/body (ex-data err)})
(defmethod handle-exception :restriction
[err _]
(yrs/response 400 (ex-data err)))
{::yrs/status 400
::yrs/body (ex-data err)})
(defmethod handle-exception :rate-limit
[err _]
(let [headers (-> err ex-data ::http/headers)]
(yrs/response :status 429 :body "" :headers headers)))
{::yrs/status 429
::yrs/headers headers}))
(defmethod handle-exception :concurrency-limit
[err _]
(let [headers (-> err ex-data ::http/headers)]
{::yrs/status 429
::yrs/headers headers}))
(defmethod handle-exception :validation
[err _]
[err request]
(let [{:keys [code] :as data} (ex-data err)]
(cond
(= code :spec-validation)
(let [explain (ex/explain data)]
(yrs/response :status 400
:body (-> data
(dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain)))))
{::yrs/status 400
::yrs/body (-> data
(dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))})
(= code :params-validation)
(let [explain (::sm/explain data)
payload (sm/humanize-data explain)]
{::yrs/status 400
::yrs/body (-> data
(dissoc ::sm/explain)
(assoc :data payload))})
(= code :request-body-too-large)
(yrs/response :status 413 :body data)
{::yrs/status 413 ::yrs/body data}
(= code :invalid-image)
(binding [l/*context* (request->context request)]
(l/error :hint "unexpected error on processing image" :cause err)
{::yrs/status 400 ::yrs/body data})
:else
(yrs/response :status 400 :body data))))
{::yrs/status 400 ::yrs/body data})))
(defmethod handle-exception :assertion
[error request]
(let [edata (ex-data error)
explain (ex/explain edata)]
(binding [l/*context* (request->context request)]
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
(yrs/response :status 500
:body {:type :server-error
:code :assertion
:data (-> edata
(dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))}))))
(binding [l/*context* (request->context request)]
(let [{:keys [code] :as data} (ex-data error)]
(cond
(= code :data-validation)
(let [explain (::sm/explain data)
payload (sm/humanize-data explain)]
(l/error :hint "Data assertion error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :assertion
:data (-> data
(dissoc ::sm/explain)
(assoc :data payload))}})
(= code :spec-validation)
(let [explain (ex/explain data)]
(l/error :hint "Spec assertion error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :assertion
:data (-> data
(dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))}})
:else
(do
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :assertion
:data data}})))))
(defmethod handle-exception :not-found
[err _]
(yrs/response 404 (ex-data err)))
{::yrs/status 404
::yrs/body (ex-data err)})
(defmethod handle-exception :internal
[error request]
(let [{:keys [code] :as edata} (ex-data error)]
(cond
(= :concurrency-limit-reached code)
(yrs/response 429)
:else
(binding [l/*context* (request->context request)]
(l/error :hint "Internal error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
(binding [l/*context* (request->context request)]
(l/error :hint "Internal error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :unhandled
:hint (ex-message error)
:data (ex-data error)}}))
(defmethod handle-exception org.postgresql.util.PSQLException
[error request]
@@ -117,20 +160,23 @@
(l/error :hint "PSQL error" :message (ex-message error) :cause error)
(cond
(= state "57014")
(yrs/response 504 {:type :server-error
:code :statement-timeout
:hint (ex-message error)})
{::yrs/status 504
::yrs/body {:type :server-error
:code :statement-timeout
:hint (ex-message error)}}
(= state "25P03")
(yrs/response 504 {:type :server-error
:code :idle-in-transaction-timeout
:hint (ex-message error)})
{::yrs/status 504
::yrs/body {:type :server-error
:code :idle-in-transaction-timeout
:hint (ex-message error)}}
:else
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)
:state state})))))
{::yrs/status 500
::yrs/body {:type :server-error
:code :unexpected
:hint (ex-message error)
:state state}}))))
(defmethod handle-exception :default
[error request]
@@ -140,9 +186,10 @@
(nil? edata)
(binding [l/*context* (request->context request)]
(l/error :hint "Unexpected error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)}))
{::yrs/status 500
::yrs/body {:type :server-error
:code :unexpected
:hint (ex-message error)}})
;; This is a special case for the idle-in-transaction error;
;; when it happens, the connection is automatically closed and
@@ -156,10 +203,11 @@
:else
(binding [l/*context* (request->context request)]
(l/error :hint "Unhandled error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
{::yrs/status 500
::yrs/body {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata}}))))
(defn handle
[cause request]

View File

@@ -14,6 +14,7 @@
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]
[promesa.util :as pu]
[yetti.adapter :as yt]
[yetti.middleware :as ymw]
[yetti.request :as yrq]
@@ -21,9 +22,13 @@
(: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))
(set! *warn-on-reflection* true)
(def server-timing
{:name ::server-timing
:compile (constantly ymw/wrap-server-timing)})
@@ -44,14 +49,14 @@
(let [header (yrq/get-header request "content-type")]
(cond
(str/starts-with? header "application/transit+json")
(with-open [is (yrq/body request)]
(with-open [^InputStream is (yrq/body request)]
(let [params (t/read! (t/reader is))]
(-> request
(assoc :body-params params)
(update :params merge params))))
(str/starts-with? header "application/json")
(with-open [is (yrq/body request)]
(with-open [^InputStream is (yrq/body request)]
(let [params (json/decode is json-mapper)]
(-> request
(assoc :body-params params)
@@ -62,6 +67,11 @@
(handle-error [raise cause]
(cond
(instance? RuntimeException cause)
(if-let [cause (ex-cause cause)]
(handle-error raise cause)
(raise cause))
(instance? RequestTooBigException cause)
(raise (ex/error :type :validation
:code :request-body-too-large
@@ -69,21 +79,23 @@
(or (instance? JsonEOFException cause)
(instance? JsonParseException cause))
(instance? JsonParseException cause)
(instance? MismatchedInputException cause))
(raise (ex/error :type :validation
:code :malformed-json
:hint (ex-message cause)
:cause cause))
:else
(raise cause)))]
(fn [request respond raise]
(let [request (ex/try! (process-request request))]
(if (ex/exception? request)
(if (ex/runtime-exception? request)
(handle-error raise (or (ex-cause request) request))
(handle-error raise request))
(handler request respond raise))))))
(if (= (yrq/method request) :post)
(let [request (ex/try! (process-request request))]
(if (ex/exception? request)
(handle-error raise request)
(handler request respond raise)))
(handler request respond raise)))))
(def parse-request
{:name ::parse-request
@@ -94,12 +106,7 @@
needed because transit-java calls flush very aggresivelly on each
object write."
[^java.io.OutputStream os ^long chunk-size]
(proxy [java.io.BufferedOutputStream] [os (int chunk-size)]
;; Explicitly do not forward flush
(flush [])
(close []
(proxy-super flush)
(proxy-super close))))
(yetti.util.BufferedOutputStream. os (int chunk-size)))
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
@@ -109,16 +116,14 @@
(reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream]
(try
(with-open [bos (buffered-output-stream output-stream buffer-size)]
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
(let [tw (t/writer bos opts)]
(t/write! tw data)))
(catch java.io.IOException _cause
;; Do nothing, EOF means client closes connection abruptly
nil)
(catch java.io.IOException _)
(catch Throwable cause
(l/warn :hint "unexpected error on encoding response"
:cause cause))
(binding [l/*context* {:value data}]
(l/error :hint "unexpected error on encoding response"
:cause cause)))
(finally
(.close ^OutputStream output-stream))))))
@@ -126,29 +131,27 @@
(reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream]
(try
(with-open [bos (buffered-output-stream output-stream buffer-size)]
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
(json/write! bos data json-mapper))
(catch java.io.IOException _cause
;; Do nothing, EOF means client closes connection abruptly
nil)
(catch java.io.IOException _)
(catch Throwable cause
(l/warn :hint "unexpected error on encoding response"
:cause cause))
(binding [l/*context* {:value data}]
(l/error :hint "unexpected error on encoding response"
:cause cause)))
(finally
(.close ^OutputStream output-stream))))))
(format-response-with-json [response _]
(let [body (yrs/body response)]
(let [body (::yrs/body response)]
(if (or (boolean? body) (coll? body))
(-> response
(update :headers assoc "content-type" "application/json")
(assoc :body (json-streamable-body body)))
(update ::yrs/headers assoc "content-type" "application/json")
(assoc ::yrs/body (json-streamable-body body)))
response)))
(format-response-with-transit [response request]
(let [body (yrs/body response)]
(let [body (::yrs/body response)]
(if (or (boolean? body) (coll? body))
(let [qs (yrq/query request)
opts (if (or (contains? cf/flags :transit-readable-response)
@@ -156,12 +159,17 @@
{:type :json-verbose}
{:type :json})]
(-> response
(update :headers assoc "content-type" "application/transit+json")
(assoc :body (transit-streamable-body body opts))))
(update ::yrs/headers assoc "content-type" "application/transit+json")
(assoc ::yrs/body (transit-streamable-body body opts))))
response)))
(format-from-params [{:keys [query-params] :as request}]
(and (= "json" (get query-params :_fmt))
"application/json"))
(format-response [response request]
(let [accept (yrq/get-header request "accept")]
(let [accept (or (format-from-params request)
(yrq/get-header request "accept"))]
(cond
(or (= accept "application/transit+json")
(str/includes? accept "application/transit+json"))
@@ -181,8 +189,7 @@
(fn [request respond raise]
(handler request
(fn [response]
(let [response (process-response response request)]
(respond response)))
(respond (process-response response request)))
raise))))
(def format-response
@@ -191,74 +198,59 @@
(defn wrap-errors
[handler on-error]
(fn [request respond _]
(fn [request respond raise]
(handler request respond (fn [cause]
(-> cause (on-error request) respond)))))
(try
(respond (on-error cause request))
(catch Throwable cause
(raise cause)))))))
(def errors
{:name ::errors
:compile (constantly wrap-errors)})
(defn- with-cors-headers
[headers origin]
(-> headers
(assoc "access-control-allow-origin" origin)
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width")))
(defn wrap-cors
[handler]
(if-not (contains? cf/flags :cors)
handler
(letfn [(add-headers [headers request]
(let [origin (yrq/get-header request "origin")]
(-> headers
(assoc "access-control-allow-origin" origin)
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))
(update-response [response request]
(update response :headers add-headers request))]
(fn [request respond raise]
(if (= (yrq/method request) :options)
(-> (yrs/response 200)
(update-response request)
(respond))
(handler request
(fn [response]
(respond (update-response response request)))
raise))))))
(fn [request]
(let [response (if (= (yrq/method request) :options)
{::yrs/status 200}
(handler request))
origin (yrq/get-header request "origin")]
(update response ::yrs/headers with-cors-headers origin))))
(def cors
{:name ::cors
:compile (constantly wrap-cors)})
(defn compile-restrict-methods
[data _]
(when-let [allowed (:allowed-methods data)]
(fn [handler]
(fn [request respond raise]
(let [method (yrq/method request)]
(if (contains? allowed method)
(handler request respond raise)
(respond (yrs/response 405))))))))
:compile (fn [& _]
(when (contains? cf/flags :cors)
wrap-cors))})
(def restrict-methods
{:name ::restrict-methods
:compile compile-restrict-methods})
:compile
(fn [data _]
(when-let [allowed (:allowed-methods data)]
(fn [handler]
(fn [request respond raise]
(let [method (yrq/method request)]
(if (contains? allowed method)
(handler request respond raise)
(respond {::yrs/status 405})))))))})
(def with-dispatch
{:name ::with-dispatch
:compile
(fn [& _]
(fn [handler executor]
(fn [request respond raise]
(-> (px/submit! executor #(handler request))
(p/bind p/wrap)
(p/then respond)
(p/catch raise)))))})
(def with-config
{:name ::with-config
:compile
(fn [& _]
(fn [handler config]
(fn
([request] (handler config request))
([request respond raise] (handler config request respond raise)))))})
(let [executor (px/resolve-executor executor)]
(fn [request respond raise]
(->> (px/submit! executor (partial handler request))
(p/fnly (pu/handler respond raise)))))))})

View File

@@ -8,7 +8,6 @@
(:refer-clojure :exclude [read])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
@@ -18,12 +17,9 @@
[app.main :as-alias main]
[app.tokens :as tokens]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -76,69 +72,56 @@
:id key})
(defn- database-manager
[{:keys [::db/pool ::wrk/executor ::main/props]}]
^{::wrk/executor executor
::db/pool pool
::main/props props}
[pool]
(reify ISessionManager
(read [_ token]
(px/with-dispatch executor
(db/exec-one! pool (sql/select :http-session {:id token}))))
(db/exec-one! pool (sql/select :http-session {:id token})))
(write! [_ key params]
(px/with-dispatch executor
(let [params (prepare-session-params key params)]
(db/insert! pool :http-session params)
params)))
(let [params (prepare-session-params key params)]
(db/insert! pool :http-session params)
params))
(update! [_ params]
(let [updated-at (dt/now)]
(px/with-dispatch executor
(db/update! pool :http-session
{:updated-at updated-at}
{:id (:id params)})
(assoc params :updated-at updated-at))))
(db/update! pool :http-session
{:updated-at updated-at}
{:id (:id params)})
(assoc params :updated-at updated-at)))
(delete! [_ token]
(px/with-dispatch executor
(db/delete! pool :http-session {:id token})
nil))))
(db/delete! pool :http-session {:id token})
nil)))
(defn inmemory-manager
[{:keys [::db/pool ::wrk/executor ::main/props]}]
[]
(let [cache (atom {})]
^{::main/props props
::wrk/executor executor
::db/pool pool}
(reify ISessionManager
(read [_ token]
(p/do (get @cache token)))
(get @cache token))
(write! [_ key params]
(p/do
(let [params (prepare-session-params key params)]
(swap! cache assoc key params)
params)))
(let [params (prepare-session-params key params)]
(swap! cache assoc key params)
params))
(update! [_ params]
(p/do
(let [updated-at (dt/now)]
(swap! cache update (:id params) assoc :updated-at updated-at)
(assoc params :updated-at updated-at))))
(let [updated-at (dt/now)]
(swap! cache update (:id params) assoc :updated-at updated-at)
(assoc params :updated-at updated-at)))
(delete! [_ token]
(p/do
(swap! cache dissoc token)
nil)))))
(swap! cache dissoc token)
nil))))
(defmethod ig/pre-init-spec ::manager [_]
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::manager
[_ {:keys [::db/pool] :as cfg}]
[_ {:keys [::db/pool]}]
(if (db/read-only? pool)
(inmemory-manager cfg)
(database-manager cfg)))
(inmemory-manager)
(database-manager pool)))
(defmethod ig/halt-key! ::manager
[_ _])
@@ -154,40 +137,35 @@
(declare ^:private gen-token)
(defn create-fn
[{:keys [::manager]} profile-id]
[{:keys [::manager ::main/props]} profile-id]
(us/assert! ::manager manager)
(us/assert! ::us/uuid profile-id)
(let [props (-> manager meta ::main/props)]
(fn [request response]
(let [uagent (yrq/get-header request "user-agent")
params {:profile-id profile-id
:user-agent uagent
:created-at (dt/now)}
token (gen-token props params)]
(fn [request response]
(let [uagent (yrq/get-header request "user-agent")
params {:profile-id profile-id
:user-agent uagent
:created-at (dt/now)}
token (gen-token props params)
session (write! manager token params)]
(l/trace :hint "create" :profile-id (str profile-id))
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)))))
(->> (write! manager token params)
(p/fmap (fn [session]
(l/trace :hint "create" :profile-id (str profile-id))
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)))))))))
(defn delete-fn
[{:keys [::manager]}]
(us/assert! ::manager manager)
(letfn [(delete [{:keys [profile-id] :as request}]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
cookie (yrq/get-cookie request cname)]
(l/trace :hint "delete" :profile-id profile-id)
(some->> (:value cookie) (delete! manager))))]
(fn [request response]
(p/do
(delete request)
(-> response
(assoc :status 204)
(assoc :body nil)
(clear-auth-token-cookie)
(clear-authenticated-cookie))))))
(fn [request response]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
cookie (yrq/get-cookie request cname)]
(l/trace :hint "delete" :profile-id (:profile-id request))
(some->> (:value cookie) (delete! manager))
(-> response
(assoc :status 204)
(assoc :body nil)
(clear-auth-token-cookie)
(clear-authenticated-cookie)))))
(defn- gen-token
[props {:keys [profile-id created-at]}]
@@ -216,58 +194,39 @@
(let [elapsed (dt/diff updated-at (dt/now))]
(neg? (compare default-renewal-max-age elapsed)))))
(defn- wrap-reneval
[respond manager session]
(fn [response]
(p/let [session (update! manager session)]
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)
(respond)))))
(defn- wrap-soft-auth
[handler {:keys [::manager]}]
[handler {:keys [::manager ::main/props]}]
(us/assert! ::manager manager)
(letfn [(handle-request [request]
(try
(let [token (get-token request)
claims (decode-token props token)]
(cond-> request
(map? claims)
(-> (assoc ::token-claims claims)
(assoc ::token token))))
(catch Throwable cause
(l/trace :hint "exception on decoding malformed token" :cause cause)
request)))]
(let [{:keys [::wrk/executor ::main/props]} (meta manager)]
(fn [request respond raise]
(let [token (ex/try! (get-token request))]
(if (ex/exception? token)
(raise token)
(->> (px/submit! executor (partial decode-token props token))
(p/fnly (fn [claims cause]
(when cause
(l/trace :hint "exception on decoding malformed token" :cause cause))
(let [request (cond-> request
(map? claims)
(-> (assoc ::token-claims claims)
(assoc ::token token)))]
(handler request respond raise))))))))))
(let [request (handle-request request)]
(handler request respond raise)))))
(defn- wrap-authz
[handler {:keys [::manager]}]
(us/assert! ::manager manager)
(fn [request respond raise]
(if-let [token (::token request)]
(->> (get-session manager token)
(p/fnly (fn [session cause]
(cond
(some? cause)
(raise cause)
(fn [request]
(let [session (get-session manager (::token request))
request (cond-> request
(some? session)
(assoc ::profile-id (:profile-id session)
::id (:id session)))]
(nil? session)
(handler request respond raise)
:else
(let [request (-> request
(assoc ::profile-id (:profile-id session))
(assoc ::id (:id session)))
respond (cond-> respond
(renew-session? session)
(wrap-reneval manager session))]
(handler request respond raise))))))
(handler request respond raise))))
(cond-> (handler request)
(renew-session? session)
(-> (assign-auth-token-cookie session)
(assign-authenticated-cookie session))))))
(def soft-auth
{:name ::soft-auth

View File

@@ -11,15 +11,16 @@
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.http.session :as session]
[app.metrics :as mtx]
[app.msgbus :as mbus]
[app.util.time :as dt]
[app.util.websocket :as ws]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec.csp :as sp]
[yetti.websocket :as yws]))
(def recv-labels
@@ -34,70 +35,38 @@
(def state (atom {}))
(defn- on-connect
[{:keys [::mtx/metrics]} wsp]
(let [created-at (dt/now)]
(swap! state assoc (::ws/id @wsp) wsp)
(mtx/run! metrics
:id :websocket-active-connections
:inc 1)
(fn []
(swap! state dissoc (::ws/id @wsp))
(mtx/run! metrics :id :websocket-active-connections :dec 1)
(mtx/run! metrics
:id :websocket-session-timing
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)))))
(defn- on-rcv-message
[{:keys [::mtx/metrics]} _ message]
(mtx/run! metrics
:id :websocket-messages-total
:labels recv-labels
:inc 1)
message)
(defn- on-snd-message
[{:keys [::mtx/metrics]} _ message]
(mtx/run! metrics
:id :websocket-messages-total
:labels send-labels
:inc 1)
message)
;; REPL HELPERS
(defn repl-get-connections-for-file
[file-id]
(->> (vals @state)
(filter #(= file-id (-> % deref ::file-subscription :file-id)))
(map deref)
(map ::ws/id)))
(defn repl-get-connections-for-team
[team-id]
(->> (vals @state)
(filter #(= team-id (-> % deref ::team-subscription :team-id)))
(map deref)
(map ::ws/id)))
(defn repl-close-connection
[id]
(when-let [wsp (get @state id)]
(a/>!! (::ws/close-ch @wsp) [8899 "closed from server"])
(a/close! (::ws/close-ch @wsp))))
(when-let [{:keys [::ws/close-ch] :as wsp} (get @state id)]
(sp/put! close-ch [8899 "closed from server"])
(sp/close! close-ch)))
(defn repl-get-connection-info
[id]
(when-let [wsp (get @state id)]
{:id id
:created-at (::created-at @wsp)
:profile-id (::profile-id @wsp)
:session-id (::session-id @wsp)
:user-agent (::ws/user-agent @wsp)
:ip-addr (::ws/remote-addr @wsp)
:last-activity-at (::ws/last-activity-at @wsp)
:subscribed-file (-> wsp deref ::file-subscription :file-id)
:subscribed-team (-> wsp deref ::team-subscription :team-id)}))
:created-at (::created-at wsp)
:profile-id (::profile-id wsp)
:session-id (::session-id wsp)
:user-agent (::ws/user-agent wsp)
:ip-addr (::ws/remote-addr wsp)
:last-activity-at (::ws/last-activity-at wsp)
:subscribed-file (-> wsp ::file-subscription :file-id)
:subscribed-team (-> wsp ::team-subscription :team-id)}))
(defn repl-print-connection-info
[id]
@@ -117,224 +86,218 @@
(fn [_ _ message]
(:type message)))
(defmethod handle-message :connect
[cfg wsp _]
(defmethod handle-message :open
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/output-ch ::ws/state ::profile-id ::session-id] :as wsp} _]
(l/trace :fn "handle-message" :event "open" :conn-id id)
(let [ch (sp/chan :buf (sp/dropping-buffer 16)
:xf (remove #(= (:session-id %) session-id)))]
(let [msgbus (::mbus/msgbus cfg)
conn-id (::ws/id @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
;; Subscribe to the profile channel and forward all messages to websocket output
;; channel (send them to the client).
(swap! state assoc ::profile-subscription {:channel ch})
xform (remove #(= (:session-id %) session-id))
channel (a/chan (a/dropping-buffer 16) xform)]
;; Forward the subscription messages directly to the websocket output channel
(sp/pipe ch output-ch false)
(l/trace :fn "handle-message" :event "connect" :conn-id conn-id)
;; Subscribe to the profile topic on msgbus/redis
(mbus/sub! msgbus :topic profile-id :chan ch)
;; Subscribe to the profile channel and forward all messages to
;; websocket output channel (send them to the client).
(swap! wsp assoc ::profile-subscription channel)
(a/pipe channel output-ch false)
(mbus/sub! msgbus :topic profile-id :chan channel)))
;; Subscribe to the system topic on msgbus/redis
(mbus/sub! msgbus :topic (str uuid/zero) :chan ch)))
(defmethod handle-message :disconnect
[cfg wsp _]
(let [msgbus (::mbus/msgbus cfg)
conn-id (::ws/id @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
profile-ch (::profile-subscription @wsp)
fsub (::file-subscription @wsp)
tsub (::team-subscription @wsp)
(defmethod handle-message :close
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::profile-id ::session-id]} _]
(l/trace :fn "handle-message" :event "close" :conn-id id)
(let [psub (::profile-subscription @state)
fsub (::file-subscription @state)
tsub (::team-subscription @state)
msg {:type :disconnect
:subs-id profile-id
:profile-id profile-id
:session-id session-id}]
message {:type :disconnect
:subs-id profile-id
:profile-id profile-id
:session-id session-id}]
;; Close profile subscription if exists
(when-let [ch (:channel psub)]
(sp/close! ch)
(mbus/purge! msgbus [ch]))
(l/trace :fn "handle-message"
:event :disconnect
:conn-id conn-id)
(a/go
;; Close the main profile subscription
(a/close! profile-ch)
(a/<! (mbus/purge! msgbus [profile-ch]))
;; Close tram subscription if exists
(when-let [channel (:channel tsub)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel)))
;; Close team subscription if exists
(when-let [ch (:channel tsub)]
(sp/close! ch)
(mbus/purge! msgbus [ch]))
;; Close file subscription if exists
(when-let [{:keys [topic channel]} fsub]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))
(a/<! (mbus/pub! msgbus :topic topic :message message))))))
(sp/close! channel)
(mbus/purge! msgbus [channel])
(mbus/pub! msgbus :topic topic :message msg))))
(defmethod handle-message :subscribe-team
[cfg wsp {:keys [team-id] :as params}]
(let [msgbus (::mbus/msgbus cfg)
conn-id (::ws/id @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
prev-subs (get @wsp ::team-subscription)
xform (comp
(remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id team-id)))
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id]} {:keys [team-id] :as params}]
(l/trace :fn "handle-message" :event "subscribe-team" :team-id team-id :conn-id id)
(let [prev-subs (get @state ::team-subscription)
channel (sp/chan :buf (sp/dropping-buffer 64)
:xf (comp
(remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id team-id))))]
channel (a/chan (a/dropping-buffer 64) xform)]
(sp/pipe channel output-ch false)
(mbus/sub! msgbus :topic team-id :chan channel)
(l/trace :fn "handle-message"
:event :subscribe-team
:team-id team-id
:conn-id conn-id)
(let [subs {:team-id team-id :channel channel :topic team-id}]
(swap! state assoc ::team-subscription subs))
(a/pipe channel output-ch false)
;; Close previous subscription if exists
(when-let [ch (:channel prev-subs)]
(sp/close! ch)
(mbus/purge! msgbus [ch]))))
(let [state {:team-id team-id :channel channel :topic team-id}]
(swap! wsp assoc ::team-subscription state))
(a/go
;; Close previous subscription if exists
(when-let [channel (:channel prev-subs)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))))
(a/go
(a/<! (mbus/sub! msgbus :topic team-id :chan channel)))))
(defmethod handle-message :subscribe-file
[cfg wsp {:keys [file-id version] :as params}]
(let [msgbus (::mbus/msgbus cfg)
conn-id (::ws/id @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
prev-subs (::file-subscription @wsp)
xform (comp (remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id file-id)))
channel (a/chan (a/dropping-buffer 64) xform)]
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id ::profile-id]} {:keys [file-id] :as params}]
(l/trace :fn "handle-message" :event "subscribe-file" :file-id file-id :conn-id id)
(let [psub (::file-subscription @state)
fch (sp/chan :buf (sp/dropping-buffer 64)
:xf (comp (remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id file-id))))]
(l/trace :fn "handle-message"
:event :subscribe-file
:file-id file-id
:conn-id conn-id)
(let [subs {:file-id file-id :channel fch :topic file-id}]
(swap! state assoc ::file-subscription subs))
(let [state {:file-id file-id :channel channel :topic file-id}]
(swap! wsp assoc ::file-subscription state))
;; Close previous subscription if exists
(when-let [ch (:channel psub)]
(sp/close! ch)
(mbus/purge! msgbus [ch]))
(a/go
;; Close previous subscription if exists
(when-let [channel (:channel prev-subs)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))))
(sp/go-loop []
(when-let [{:keys [type] :as message} (sp/take! fch)]
(sp/put! output-ch message)
(when (or (= :join-file type)
(= :leave-file type)
(= :disconnect type))
(let [message {:type :presence
:file-id file-id
:session-id session-id
:profile-id profile-id}]
(mbus/pub! msgbus
:topic file-id
:message message)))
(recur)))
;; Message forwarding
(a/go
(loop []
(when-let [{:keys [type] :as message} (a/<! channel)]
(when (or (= :join-file type)
(= :leave-file type)
(= :disconnect type))
(let [message {:type :presence
:file-id file-id
:session-id session-id
:profile-id profile-id
:version version}]
(a/<! (mbus/pub! msgbus :topic file-id :message message))))
(a/>! output-ch message)
(recur))))
;; Subscribe to file topic
(mbus/sub! msgbus :topic file-id :chan fch)
(a/go
;; Subscribe to file topic
(a/<! (mbus/sub! msgbus :topic file-id :chan channel))
;; Notifify the rest of participants of the new connection.
(let [message {:type :join-file
:file-id file-id
:subs-id file-id
:session-id session-id
:profile-id profile-id}]
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
;; Notifify the rest of participants of the new connection.
(let [message {:type :join-file
:file-id file-id
:subs-id file-id
:session-id session-id
:profile-id profile-id}]
(mbus/pub! msgbus :topic file-id :message message))))
(defmethod handle-message :unsubscribe-file
[cfg wsp {:keys [file-id] :as params}]
(let [msgbus (::mbus/msgbus cfg)
conn-id (::ws/id @wsp)
session-id (::session-id @wsp)
profile-id (::profile-id @wsp)
subs (::file-subscription @wsp)
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::session-id ::profile-id]} {:keys [file-id] :as params}]
(l/trace :fn "handle-message" :event "unsubscribe-file" :file-id file-id :conn-id id)
message {:type :leave-file
:file-id file-id
:session-id session-id
:profile-id profile-id}]
(let [subs (::file-subscription @state)
message {:type :leave-file
:file-id file-id
:session-id session-id
:profile-id profile-id}]
(l/trace :fn "handle-message"
:event :unsubscribe-file
:file-id file-id
:conn-id conn-id)
(a/go
(when (= (:file-id subs) file-id)
(let [channel (:channel subs)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))
(a/<! (mbus/pub! msgbus :topic file-id :message message)))))))
(when (= (:file-id subs) file-id)
(mbus/pub! msgbus :topic file-id :message message)
(let [ch (:channel subs)]
(sp/close! ch)
(mbus/purge! msgbus [ch])))))
(defmethod handle-message :keepalive
[_ _ _]
(l/trace :fn "handle-message" :event :keepalive)
(a/go :nothing))
(l/trace :fn "handle-message" :event :keepalive))
(defmethod handle-message :broadcast
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::session-id ::profile-id]} message]
(l/trace :fn "handle-message" :event "broadcast" :conn-id id)
(let [message (-> message
(assoc :subs-id profile-id)
(assoc :profile-id profile-id)
(assoc :session-id session-id))]
(mbus/pub! msgbus :topic profile-id :message message)))
(defmethod handle-message :pointer-update
[cfg wsp {:keys [file-id] :as message}]
(let [msgbus (::mbus/msgbus cfg)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
subs (::file-subscription @wsp)
message (-> message
(assoc :subs-id file-id)
(assoc :profile-id profile-id)
(assoc :session-id session-id))]
(a/go
;; Only allow receive pointer updates when active subscription
(when subs
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
[{:keys [::mbus/msgbus]} {:keys [::ws/state ::session-id ::profile-id]} {:keys [file-id] :as message}]
(when (::file-subscription @state)
(let [message (-> message
(assoc :subs-id file-id)
(assoc :profile-id profile-id)
(assoc :session-id session-id))]
(mbus/pub! msgbus :topic file-id :message message))))
(defmethod handle-message :default
[_ wsp message]
(let [conn-id (::ws/id @wsp)]
(l/warn :hint "received unexpected message"
:message message
:conn-id conn-id)
(a/go :none)))
[_ {:keys [::ws/id]} message]
(l/warn :hint "received unexpected message"
:message message
:conn-id id))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- on-connect
[{:keys [::mtx/metrics]} {:keys [::ws/id] :as wsp}]
(let [created-at (dt/now)]
(l/trace :fn "on-connect" :conn-id id)
(swap! state assoc id wsp)
(mtx/run! metrics
:id :websocket-active-connections
:inc 1)
(assoc wsp ::ws/on-disconnect
(fn []
(l/trace :fn "on-disconnect" :conn-id id)
(swap! state dissoc id)
(mtx/run! metrics :id :websocket-active-connections :dec 1)
(mtx/run! metrics
:id :websocket-session-timing
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))))))
(defn- on-rcv-message
[{:keys [::mtx/metrics ::profile-id ::session-id]} message]
(mtx/run! metrics
:id :websocket-messages-total
:labels recv-labels
:inc 1)
(assoc message :profile-id profile-id :session-id session-id))
(defn- on-snd-message
[{:keys [::mtx/metrics]} message]
(mtx/run! metrics
:id :websocket-messages-total
:labels send-labels
:inc 1)
message)
(s/def ::session-id ::us/uuid)
(s/def ::handler-params
(s/keys :req-un [::session-id]))
(defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request} respond raise]
[cfg {:keys [params ::session/profile-id] :as request}]
(let [{:keys [session-id]} (us/conform ::handler-params params)]
(cond
(not profile-id)
(raise (ex/error :type :authentication
:hint "Authentication required."))
(ex/raise :type :authentication
:hint "Authentication required.")
(not (yws/upgrade-request? request))
(raise (ex/error :type :validation
:code :websocket-request-expected
:hint "this endpoint only accepts websocket connections"))
(ex/raise :type :validation
:code :websocket-request-expected
:hint "this endpoint only accepts websocket connections")
:else
(do
(l/trace :hint "websocket request" :profile-id profile-id :session-id session-id)
(->> (ws/handler
::ws/on-rcv-message (partial on-rcv-message cfg)
::ws/on-snd-message (partial on-snd-message cfg)
@@ -342,8 +305,7 @@
::ws/handler (partial handle-message cfg)
::profile-id profile-id
::session-id session-id)
(yws/upgrade request)
(respond))))))
(yws/upgrade request))))))
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::mbus/msgbus

View File

@@ -16,13 +16,16 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.client :as http.client]
[app.loggers.audit.tasks :as-alias tasks]
[app.loggers.webhooks :as-alias webhooks]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.retry :as rtry]
[app.tokens :as tokens]
[app.util.retry :as rtry]
[app.util.services :as-alias sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
@@ -92,6 +95,15 @@
;; --- SPECS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COLLECTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Defines a service that collects the audit/activity log using
;; internal database. Later this audit log can be transferred to
;; an external storage and data cleared.
(s/def ::profile-id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::type ::us/string)
@@ -104,20 +116,13 @@
(s/or :fn fn? :str string? :kw keyword?))
(s/def ::event
(s/keys :req-un [::type ::name ::profile-id]
:opt-un [::ip-addr ::props]
:opt [::webhooks/event?
(s/keys :req [::type ::name ::profile-id]
:opt [::ip-addr
::props
::webhooks/event?
::webhooks/batch-timeout
::webhooks/batch-key]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COLLECTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Defines a service that collects the audit/activity log using
;; internal database. Later this audit log can be transferred to
;; an external storage and data cleared.
(s/def ::collector
(s/keys :req [::wrk/executor ::db/pool]))
@@ -133,15 +138,64 @@
:else
cfg))
(defn prepare-event
[cfg mdata params result]
(let [resultm (meta result)
request (-> params meta ::http/request)
profile-id (or (::profile-id resultm)
(:profile-id result)
(::rpc/profile-id params)
uuid/zero)
props (-> (or (::replace-props resultm)
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props))
token-id (::actoken/id request)
context (d/without-nils
{:access-token-id (some-> token-id str)})]
{::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)
::props props
::context context
;; NOTE: for batch-key lookup we need the params as-is
;; because the rpc api does not need to know the
;; audit/webhook specific object layout.
::rpc/params params
::webhooks/batch-key
(or (::webhooks/batch-key mdata)
(::webhooks/batch-key resultm))
::webhooks/batch-timeout
(or (::webhooks/batch-timeout mdata)
(::webhooks/batch-timeout resultm))
::webhooks/event?
(or (::webhooks/event? mdata)
(::webhooks/event? resultm)
false)}))
(defn- handle-event!
[conn-or-pool event]
(us/verify! ::event event)
(let [params {:id (uuid/next)
:name (:name event)
:type (:type event)
:profile-id (:profile-id event)
:ip-addr (:ip-addr event)
:props (:props event)}]
:name (::name event)
:type (::type event)
:profile-id (::profile-id event)
:ip-addr (::ip-addr event)
:context (::context event)
:props (::props event)}]
(when (contains? cf/flags :audit-log)
;; NOTE: this operation may cause primary key conflicts on inserts
@@ -149,11 +203,13 @@
;; this case we just retry the operation.
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 6
::rtry/label "persist-audit-log"}
::rtry/label "persist-audit-log"
::db/conn (dm/check db/connection? conn-or-pool)}
(let [now (dt/now)]
(db/insert! conn-or-pool :audit-log
(-> params
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet)
(assoc :created-at now)
(assoc :tracked-at now)
@@ -186,9 +242,8 @@
(defn submit!
"Submit audit event to the collector."
[{:keys [::wrk/executor] :as cfg} params]
[cfg params]
(let [conn (or (::db/conn cfg) (::db/pool cfg))]
(us/assert! ::wrk/executor executor)
(us/assert! ::db/pool-or-conn conn)
(try
(handle-event! conn (d/without-nils params))
@@ -207,7 +262,7 @@
(s/def ::tasks/uri ::us/string)
(defmethod ig/pre-init-spec ::tasks/archive-task [_]
(s/keys :req [::db/pool ::main/props ::http/client]))
(s/keys :req [::db/pool ::main/props ::http.client/client]))
(defmethod ig/init-key ::tasks/archive
[_ cfg]
@@ -231,7 +286,7 @@
(if n
(do
(px/sleep 100)
(recur (+ total n)))
(recur (+ total ^long n)))
(when (pos? total)
(l/debug :hint "events archived" :total total)))))))))
@@ -281,7 +336,7 @@
:method :post
:headers headers
:body body}
resp (http/req! cfg params {:sync? true})]
resp (http.client/req! cfg params {:sync? true})]
(if (= (:status resp) 204)
true
(do

View File

@@ -11,6 +11,7 @@
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
@@ -32,35 +33,44 @@
(when-not (db/read-only? pool)
(db/insert! pool :server-error-report
{:id id
:version 2
:version 3
:content (db/tjson report)})))
(defn record->report
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
(us/assert! ::l/record record)
(merge
{:context (-> context
(let [data (ex-data cause)
ctx (-> context
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (cf/get :public-uri))
(assoc :version (:full cf/version))
(assoc :logger-name logger)
(assoc :logger-level level)
(dissoc :params)
(pp/pprint-str :width 200))
:params (some-> (:params context)
(pp/pprint-str :width 200))
:props (pp/pprint-str props :width 200)
:hint (or (ex-message cause) @message)
:trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)}
(assoc :logger/name logger)
(assoc :logger/level level)
(dissoc :request/params :value :params :data))]
(merge
{:context (-> (into (sorted-map) ctx)
(pp/pprint-str :width 200 :length 50 :level 10))
:props (pp/pprint-str props :width 200 :length 50)
:hint (or (ex-message cause) @message)
:trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)}
(when-let [data (ex-data cause)]
{:spec-value (some-> (::s/value data) (pp/pprint-str :width 200))
:spec-explain (ex/explain data)
:data (-> data
(dissoc ::s/problems ::s/value ::s/spec :hint)
(pp/pprint-str :width 200))})))
(when-let [params (or (:request/params context) (:params context))]
{:params (pp/pprint-str params :width 200 :length 50 :level 10)})
(when-let [value (:value context)]
{:value (pp/pprint-str value :width 200 :length 50 :level 10)})
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
{:data (pp/pprint-str data :width 200)})
(when-let [explain (ex/explain data {:level 10 :length 50})]
{:explain explain}))))
(defn error-record?
[{:keys [::l/level ::l/cause]}]
(and (= :error level)
(ex/exception? cause)))
(defn- handle-event
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
@@ -74,20 +84,16 @@
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn error-record?
[{:keys [::l/level ::l/cause]}]
(and (= :error level)
(ex/exception? cause)))
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::reporter
[_ cfg]
(let [input (sp/chan (sp/sliding-buffer 32) (filter error-record?))]
(let [input (sp/chan :buf (sp/sliding-buffer 32)
:xf (filter error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(px/thread
{:name "penpot/database-reporter" :virtual true}
(px/thread {:name "penpot/database-reporter" :virtual true}
(l/info :hint "initializing database error persistence")
(try
(loop []

View File

@@ -30,7 +30,9 @@
"```\n"
"- host: `" (:host report) "`\n"
"- tenant: `" (:tenant report) "`\n"
"- version: `" (:version report) "`\n"
"- request-path: `" (:request-path report) "`\n"
"- frontend-version: `" (:frontend-version report) "`\n"
"- backend-version: `" (:backend-version report) "`\n"
"\n"
"Trace:\n"
(:trace report)
@@ -50,13 +52,15 @@
(defn record->report
[{:keys [::l/context ::l/id ::l/cause] :as record}]
(us/assert! ::l/record record)
{:id id
:tenant (cf/get :tenant)
:host (cf/get :host)
:public-uri (cf/get :public-uri)
:version (:full cf/version)
:profile-id (:profile-id context)
:trace (ex/format-throwable cause :detail? false :header? false)})
{:id id
:tenant (cf/get :tenant)
:host (cf/get :host)
:public-uri (cf/get :public-uri)
:backend-version (or (:version/backend context) (:full cf/version))
:frontend-version (:version/frontend context)
:profile-id (:request/profile-id context)
:request-path (:request/path context)
:trace (ex/format-throwable cause :detail? false :header? false)})
(defn handle-event
[cfg record]
@@ -77,7 +81,8 @@
{:name "penpot/mattermost-reporter"
:virtual true}
(l/info :hint "initializing error reporter" :uri uri)
(let [input (sp/chan (sp/sliding-buffer 128) (filter ldb/error-record?))]
(let [input (sp/chan :buf (sp/sliding-buffer 128)
:xf (filter ldb/error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(try
(loop []

View File

@@ -14,7 +14,6 @@
[app.db :as-alias db]
[app.email :as-alias email]
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.assets :as-alias http.assets]
[app.http.awsns :as http.awsns]
[app.http.client :as-alias http.client]
@@ -30,6 +29,7 @@
[app.redis :as-alias rds]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.setup :as-alias setup]
[app.srepl :as-alias srepl]
[app.storage :as-alias sto]
[app.storage.fs :as-alias sto.fs]
@@ -37,7 +37,8 @@
[app.util.time :as dt]
[app.worker :as-alias wrk]
[cuerdas.core :as str]
[integrant.core :as ig])
[integrant.core :as ig]
[promesa.exec :as px])
(:gen-class))
(def default-metrics
@@ -102,15 +103,15 @@
::mdef/labels ["name"]
::mdef/type :summary}
:rpc-climit-queue-size
{::mdef/name "penpot_rpc_climit_queue_size"
::mdef/help "Current number of queued submissions on the CLIMIT."
:rpc-climit-queue
{::mdef/name "penpot_rpc_climit_queue"
::mdef/help "Current number of queued submissions."
::mdef/labels ["name"]
::mdef/type :gauge}
:rpc-climit-concurrency
{::mdef/name "penpot_rpc_climit_concurrency"
::mdef/help "Current number of used concurrency capacity on the CLIMIT"
:rpc-climit-permits
{::mdef/name "penpot_rpc_climit_permits"
::mdef/help "Current number of available permits"
::mdef/labels ["name"]
::mdef/type :gauge}
@@ -174,10 +175,8 @@
;; Default thread pool for IO operations
::wrk/executor
{::wrk/parallelism (cf/get :default-executor-parallelism 100)}
::wrk/scheduled-executor
{::wrk/parallelism (cf/get :scheduled-executor-parallelism 20)}
{::wrk/parallelism (cf/get :default-executor-parallelism
(+ 3 (* (px/get-available-processors) 3)))}
::wrk/monitor
{::mtx/metrics (ig/ref ::mtx/metrics)
@@ -194,17 +193,16 @@
{::mtx/metrics (ig/ref ::mtx/metrics)}
::rds/redis
{::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)}
{::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)}
::mbus/msgbus
{:backend (cf/get :msgbus-backend :redis)
:executor (ig/ref ::wrk/executor)
:redis (ig/ref ::rds/redis)}
{::wrk/executor (ig/ref ::wrk/executor)
::rds/redis (ig/ref ::rds/redis)}
:app.storage.tmp/cleaner
{::wrk/executor (ig/ref ::wrk/executor)
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
::sto/gc-deleted-task
{::db/pool (ig/ref ::db/pool)
@@ -217,20 +215,13 @@
{::wrk/executor (ig/ref ::wrk/executor)}
::session/manager
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)}
::actoken/manager
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)}
{::db/pool (ig/ref ::db/pool)}
::session.tasks/gc
{::db/pool (ig/ref ::db/pool)}
::http.awsns/routes
{::props (ig/ref :app.setup/props)
{::props (ig/ref ::setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)
::wrk/executor (ig/ref ::wrk/executor)}
@@ -239,8 +230,7 @@
{::http/port (cf/get :http-server-port)
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::http/metrics (ig/ref ::mtx/metrics)
::http/executor (ig/ref ::wrk/executor)
::wrk/executor (ig/ref ::wrk/executor)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
@@ -274,8 +264,7 @@
::oidc/routes
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::props (ig/ref :app.setup/props)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref ::setup/props)
::oidc/providers {:google (ig/ref ::oidc.providers/google)
:github (ig/ref ::oidc.providers/github)
:gitlab (ig/ref ::oidc.providers/gitlab)
@@ -284,12 +273,10 @@
:app.http/router
{::session/manager (ig/ref ::session/manager)
::actoken/manager (ig/ref ::actoken/manager)
::wrk/executor (ig/ref ::wrk/executor)
::db/pool (ig/ref ::db/pool)
::rpc/routes (ig/ref ::rpc/routes)
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
::props (ig/ref :app.setup/props)
::props (ig/ref ::setup/props)
::mtx/routes (ig/ref ::mtx/routes)
::oidc/routes (ig/ref ::oidc/routes)
::http.debug/routes (ig/ref ::http.debug/routes)
@@ -303,10 +290,10 @@
::session/manager (ig/ref ::session/manager)
::sto/storage (ig/ref ::sto/storage)}
:app.http.websocket/routes
::http.ws/routes
{::db/pool (ig/ref ::db/pool)
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref :app.msgbus/msgbus)
::mbus/msgbus (ig/ref ::mbus/msgbus)
::session/manager (ig/ref ::session/manager)}
:app.http.assets/routes
@@ -321,8 +308,7 @@
::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/executor)
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/methods
{::http.client/client (ig/ref ::http.client/client)
@@ -337,11 +323,10 @@
::rpc/climit (ig/ref ::rpc/climit)
::rpc/rlimit (ig/ref ::rpc/rlimit)
::props (ig/ref :app.setup/props)
::setup/templates (ig/ref ::setup/templates)
::props (ig/ref ::setup/props)
:pool (ig/ref ::db/pool)
:templates (ig/ref :app.setup/builtin-templates)
}
:app.rpc.doc/routes
@@ -352,8 +337,7 @@
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager)
::actoken/manager (ig/ref ::actoken/manager)
::props (ig/ref :app.setup/props)}
::props (ig/ref ::setup/props)}
::wrk/registry
{::mtx/metrics (ig/ref ::mtx/metrics)
@@ -397,7 +381,8 @@
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-gc/handler
{::db/pool (ig/ref ::db/pool)}
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-xlog-gc/handler
{::db/pool (ig/ref ::db/pool)}
@@ -405,7 +390,7 @@
:app.tasks.telemetry/handler
{::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)
::props (ig/ref :app.setup/props)}
::props (ig/ref ::setup/props)}
[::srepl/urepl ::srepl/server]
{::srepl/port (cf/get :urepl-port 6062)
@@ -415,10 +400,9 @@
{::srepl/port (cf/get :prepl-port 6063)
::srepl/host (cf/get :prepl-host "localhost")}
:app.setup/builtin-templates
{::http.client/client (ig/ref ::http.client/client)}
::setup/templates {}
:app.setup/props
::setup/props
{::db/pool (ig/ref ::db/pool)
::key (cf/get :secret-key)
@@ -427,7 +411,7 @@
::migrations (ig/ref :app.migrations/migrations)}
::audit.tasks/archive
{::props (ig/ref :app.setup/props)
{::props (ig/ref ::setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)}
@@ -468,8 +452,7 @@
(def worker-config
{::wrk/cron
{::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)
::wrk/registry (ig/ref ::wrk/registry)
{::wrk/registry (ig/ref ::wrk/registry)
::db/pool (ig/ref ::db/pool)
::wrk/entries
[{:cron #app/cron "0 0 * * * ?" ;; hourly

View File

@@ -10,12 +10,16 @@
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.schema.openapi :as-alias oapi]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as-alias db]
[app.storage :as-alias sto]
[app.storage.tmp :as tmp]
[app.util.svg :as svg]
[app.util.time :as dt]
[buddy.core.bytes :as bb]
[buddy.core.codecs :as bc]
[clojure.java.shell :as sh]
@@ -28,6 +32,9 @@
org.im4java.core.IMOperation
org.im4java.core.Info))
(def default-max-file-size
(* 1024 1024 30)) ; 30 MiB
(s/def ::path fs/path?)
(s/def ::filename string?)
(s/def ::size integer?)
@@ -43,6 +50,27 @@
(s/keys :req-un [::path]
:opt-un [::mtype]))
(sm/def! ::fs/path
{:type ::fs/path
:pred fs/path?
:type-properties
{:title "path"
:description "filesystem path"
:error/message "expected a valid fs path instance"
:gen/gen (sg/generator :string)
::oapi/type "string"
::oapi/format "unix-path"
::oapi/decode fs/path}})
(sm/def! ::upload
[:map {:title "Upload"}
[:filename :string]
[:size :int]
[:path ::fs/path]
[:mtype {:optional true} :string]
[:headers {:optional true}
[:map-of :string :string]]])
(defn validate-media-type!
([upload] (validate-media-type! upload cm/valid-image-types))
([upload allowed]
@@ -53,6 +81,16 @@
upload))
(defn validate-media-size!
[upload]
(when (> (:size upload) (cf/get :media-max-file-size default-max-file-size))
(ex/raise :type :restriction
:code :media-max-file-size-reached
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
(:size upload)
default-max-file-size)))
upload)
(defmulti process :cmd)
(defmulti process-error class)
@@ -168,7 +206,7 @@
(ex/raise :type :validation
:code :invalid-svg-file
:hint "uploaded svg does not provides dimensions"))
(merge input info))
(merge input info {:ts (dt/now)}))
(let [instance (Info. (str path))
mtype' (.getProperty instance "Mime type")]
@@ -183,7 +221,8 @@
;; any frame.
(assoc input
:width (.getPageWidth instance)
:height (.getPageHeight instance))))))
:height (.getPageHeight instance)
:ts (dt/now))))))
(defmethod process-error org.im4java.core.InfoException
[error]

View File

@@ -89,12 +89,12 @@
(defn- handler
[registry _ respond _]
[registry _]
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
writer (StringWriter.)]
(TextFormat/write004 writer samples)
(respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)})))
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)}))

View File

@@ -315,7 +315,22 @@
{:name "0101-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0101-mod-server-error-report-table.sql")}
])
{:name "0102-mod-access-token-table"
:fn (mg/resource "app/migrations/sql/0102-mod-access-token-table.sql")}
{:name "0103-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0103-mod-file-object-thumbnail-table.sql")}
{:name "0104-mod-file-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0104-mod-file-thumbnail-table.sql")}
{:name "0105-mod-file-change-table"
:fn (mg/resource "app/migrations/sql/0105-mod-file-change-table.sql")}
{:name "0105-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0105-mod-server-error-report-table.sql")}
])
(defn apply-migrations!
[pool name migrations]

View File

@@ -0,0 +1,2 @@
ALTER TABLE access_token
ADD COLUMN expires_at timestamptz NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE file_object_thumbnail
ADD COLUMN media_id uuid NULL REFERENCES storage_object(id) ON DELETE CASCADE DEFERRABLE;

View File

@@ -0,0 +1,2 @@
ALTER TABLE file_thumbnail
ADD COLUMN media_id uuid NULL REFERENCES storage_object(id) ON DELETE CASCADE DEFERRABLE;

View File

@@ -0,0 +1,9 @@
ALTER TABLE file_change
ADD COLUMN label text NULL;
ALTER TABLE file_change
ALTER COLUMN label SET STORAGE external;
CREATE INDEX file_change__label__idx
ON file_change (file_id, label)
WHERE label is not null;

View File

@@ -0,0 +1,2 @@
CREATE INDEX server_error_report__created_at__idx
ON server_error_report ( created_at );

View File

@@ -8,20 +8,18 @@
"The msgbus abstraction implemented using redis as underlying backend."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.transit :as t]
[app.config :as cfg]
[app.redis :as redis]
[app.util.async :as aa]
[app.redis :as rds]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]))
[promesa.exec :as px]
[promesa.exec.csp :as sp]))
(set! *warn-on-reflection* true)
@@ -34,132 +32,116 @@
(def ^:private xform-prefix-topic
(map (fn [obj] (update obj :topic prefix-topic))))
(declare ^:private redis-connect)
(declare ^:private redis-disconnect)
(declare ^:private redis-pub)
(declare ^:private redis-sub)
(declare ^:private redis-unsub)
(declare ^:private redis-pub!)
(declare ^:private redis-sub!)
(declare ^:private redis-unsub!)
(declare ^:private start-io-loop!)
(declare ^:private subscribe-to-topics)
(declare ^:private unsubscribe-channels)
(defmethod ig/prep-key ::msgbus
[_ cfg]
(merge {:buffer-size 128
:timeout (dt/duration {:seconds 30})}
(d/without-nils cfg)))
(s/def ::cmd-ch ::aa/channel)
(s/def ::rcv-ch ::aa/channel)
(s/def ::pub-ch ::aa/channel)
(s/def ::cmd-ch sp/chan?)
(s/def ::rcv-ch sp/chan?)
(s/def ::pub-ch sp/chan?)
(s/def ::state ::us/agent)
(s/def ::pconn ::redis/connection-holder)
(s/def ::sconn ::redis/connection-holder)
(s/def ::pconn ::rds/connection-holder)
(s/def ::sconn ::rds/connection-holder)
(s/def ::msgbus
(s/keys :req [::cmd-ch ::rcv-ch ::pub-ch ::state ::pconn ::sconn ::wrk/executor]))
(s/def ::buffer-size ::us/integer)
(defmethod ig/pre-init-spec ::msgbus [_]
(s/keys :req-un [::buffer-size ::redis/timeout ::redis/redis ::wrk/executor]))
(s/keys :req [::rds/redis ::wrk/executor]))
(defmethod ig/prep-key ::msgbus
[_ cfg]
(-> cfg
(assoc ::buffer-size 128)
(assoc ::timeout (dt/duration {:seconds 30}))))
(defmethod ig/init-key ::msgbus
[_ {:keys [buffer-size executor] :as cfg}]
[_ {:keys [::buffer-size ::wrk/executor ::timeout ::rds/redis] :as cfg}]
(l/info :hint "initialize msgbus" :buffer-size buffer-size)
(let [cmd-ch (a/chan buffer-size)
rcv-ch (a/chan (a/dropping-buffer buffer-size))
pub-ch (a/chan (a/dropping-buffer buffer-size) xform-prefix-topic)
(let [cmd-ch (sp/chan :buf buffer-size)
rcv-ch (sp/chan :buf (sp/dropping-buffer buffer-size))
pub-ch (sp/chan :buf (sp/dropping-buffer buffer-size)
:xf xform-prefix-topic)
state (agent {})
msgbus (-> (redis-connect cfg)
pconn (rds/connect redis :timeout timeout)
sconn (rds/connect redis :type :pubsub :timeout timeout)
msgbus (-> cfg
(assoc ::pconn pconn)
(assoc ::sconn sconn)
(assoc ::cmd-ch cmd-ch)
(assoc ::rcv-ch rcv-ch)
(assoc ::pub-ch pub-ch)
(assoc ::state state)
(assoc ::wrk/executor executor))]
(us/verify! ::msgbus msgbus)
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
(set-error-mode! state :continue)
(start-io-loop! msgbus)
msgbus))
(defn sub!
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
(let [done-ch (a/chan)
topics (into [] (map prefix-topic) (if topic [topic] topics))]
(l/debug :hint "subscribe" :topics topics)
(send-via executor state subscribe-to-topics cfg topics chan done-ch)
done-ch))
(defn pub!
[{::keys [pub-ch]} & {:as params}]
(a/go
(a/>! pub-ch params)))
(defn purge!
[{:keys [::state ::wrk/executor] :as msgbus} chans]
(l/trace :hint "purge" :chans (count chans))
(let [done-ch (a/chan)]
(send-via executor state unsubscribe-channels msgbus chans done-ch)
done-ch))
(assoc msgbus ::io-thr (start-io-loop! msgbus))))
(defmethod ig/halt-key! ::msgbus
[_ msgbus]
(redis-disconnect msgbus)
(a/close! (::cmd-ch msgbus))
(a/close! (::rcv-ch msgbus))
(a/close! (::pub-ch msgbus)))
(px/interrupt! (::io-thr msgbus))
(sp/close! (::cmd-ch msgbus))
(sp/close! (::rcv-ch msgbus))
(sp/close! (::pub-ch msgbus))
(d/close! (::pconn msgbus))
(d/close! (::sconn msgbus)))
(defn sub!
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
(let [topics (into [] (map prefix-topic) (if topic [topic] topics))]
(l/debug :hint "subscribe" :topics topics :chan (hash chan))
(send-via executor state subscribe-to-topics cfg topics chan)
nil))
(defn pub!
[{::keys [pub-ch]} & {:as params}]
(sp/put! pub-ch params))
(defn purge!
[{:keys [::state ::wrk/executor] :as msgbus} chans]
(l/debug :hint "purge" :chans (count chans))
(send-via executor state unsubscribe-channels msgbus chans)
nil)
;; --- IMPL
(defn- redis-connect
[{:keys [timeout redis] :as cfg}]
(let [pconn (redis/connect redis :timeout timeout)
sconn (redis/connect redis :type :pubsub :timeout timeout)]
{::pconn pconn
::sconn sconn}))
(defn- redis-disconnect
[{:keys [::pconn ::sconn] :as cfg}]
(d/close! pconn)
(d/close! sconn))
(defn- conj-subscription
"A low level function that is responsible to create on-demand
subscriptions on redis. It reuses the same subscription if it is
already established. Intended to be executed in agent."
already established."
[nsubs cfg topic chan]
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
(when (= 1 (count nsubs))
(l/trace :hint "open subscription" :topic topic ::l/sync? true)
(redis-sub cfg topic))
(redis-sub! cfg topic))
nsubs))
(defn- disj-subscription
"A low level function responsible on removing subscriptions. The
subscription is truly removed from redis once no single local
subscription is look for it. Intended to be executed in agent."
subscription is look for it."
[nsubs cfg topic chan]
(let [nsubs (disj nsubs chan)]
(when (empty? nsubs)
(l/trace :hint "close subscription" :topic topic ::l/sync? true)
(redis-unsub cfg topic))
(redis-unsub! cfg topic))
nsubs))
(defn- subscribe-to-topics
"Function responsible to attach local subscription to the
state. Intended to be used in agent."
[state cfg topics chan done-ch]
(aa/with-closing done-ch
(let [state (update state :chans assoc chan topics)]
(reduce (fn [state topic]
(update-in state [:topics topic] conj-subscription cfg topic chan))
state
topics))))
"Function responsible to attach local subscription to the state."
[state cfg topics chan]
(let [state (update state :chans assoc chan topics)]
(reduce (fn [state topic]
(update-in state [:topics topic] conj-subscription cfg topic chan))
state
topics)))
(defn- unsubscribe-single-channel
(defn- unsubscribe-channel
"Auxiliary function responsible on removing a single local
subscription from the state."
[state cfg chan]
@@ -174,87 +156,113 @@
"Function responsible from detach from state a seq of channels,
useful when client disconnects or in-bulk unsubscribe
operations. Intended to be executed in agent."
[state cfg channels done-ch]
(aa/with-closing done-ch
(reduce #(unsubscribe-single-channel %1 cfg %2) state channels)))
[state cfg channels]
(reduce #(unsubscribe-channel %1 cfg %2) state channels))
(defn- create-listener
[rcv-ch]
(redis/pubsub-listener
(rds/pubsub-listener
:on-message (fn [_ topic message]
;; There are no back pressure, so we use a slidding
;; buffer for cases when the pubsub broker sends
;; more messages that we can process.
(let [val {:topic topic :message (t/decode message)}]
(when-not (a/offer! rcv-ch val)
(when-not (sp/offer! rcv-ch val)
(l/warn :msg "dropping message on subscription loop"))))))
(defn- process-input!
[{:keys [::state ::wrk/executor] :as cfg} topic message]
(let [chans (get-in @state [:topics topic])]
(when-let [closed (loop [chans (seq chans)
closed #{}]
(if-let [ch (first chans)]
(if (sp/put! ch message)
(recur (rest chans) closed)
(recur (rest chans) (conj closed ch)))
(seq closed)))]
(send-via executor state unsubscribe-channels cfg closed))))
(defn start-io-loop!
[{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}]
(redis/add-listener! sconn (create-listener rcv-ch))
(letfn [(send-to-topic [topic message]
(a/go-loop [chans (seq (get-in @state [:topics topic]))
closed #{}]
(if-let [ch (first chans)]
(if (a/>! ch message)
(recur (rest chans) closed)
(recur (rest chans) (conj closed ch)))
(seq closed))))
(rds/add-listener! sconn (create-listener rcv-ch))
(process-incoming [{:keys [topic message]}]
(a/go
(when-let [closed (a/<! (send-to-topic topic message))]
(send-via executor state unsubscribe-channels cfg closed nil))))
]
(px/thread
{:name "penpot/msgbus-io-loop"}
(px/thread
{:name "penpot/msgbus/io-loop"
:virtual true}
(try
(loop []
(let [[val port] (a/alts!! [pub-ch rcv-ch])]
(let [timeout-ch (sp/timeout-chan 1000)
[val port] (sp/alts! [timeout-ch pub-ch rcv-ch])]
(cond
(nil? val)
(do
(l/trace :hint "stopping io-loop, nil received")
(send-via executor state (fn [state]
(->> (vals state)
(mapcat identity)
(filter some?)
(run! a/close!))
nil)))
(= port rcv-ch)
(do
(a/<!! (process-incoming val))
(identical? port timeout-ch)
(let [closed (->> (:chans @state)
(map key)
(filter sp/closed?))]
(when (seq closed)
(send-via executor state unsubscribe-channels cfg closed)
(l/debug :hint "proactively purge channels" :count (count closed)))
(recur))
(= port pub-ch)
(let [result (a/<!! (redis-pub cfg val))]
(when (ex/exception? result)
(l/error :hint "unexpected error on publishing"
:message val
:cause result))
(recur))))))))
(nil? val)
(throw (InterruptedException. "internally interrupted"))
(defn- redis-pub
(identical? port rcv-ch)
(let [{:keys [topic message]} val]
(process-input! cfg topic message)
(recur))
(identical? port pub-ch)
(do
(redis-pub! cfg val)
(recur)))))
(catch InterruptedException _
(l/trace :hint "io-loop thread interrumpted"))
(catch Throwable cause
(l/error :hint "unexpected exception on io-loop thread"
:cause cause))
(finally
(l/trace :hint "clearing io-loop state")
(when-let [chans (:chans @state)]
(run! sp/close! (keys chans)))
(l/debug :hint "io-loop thread terminated")))))
(defn- redis-pub!
"Publish a message to the redis server. Asynchronous operation,
intended to be used in core.async go blocks."
[{:keys [::pconn] :as cfg} {:keys [topic message]}]
(let [message (t/encode message)
res (a/chan 1)]
(-> (redis/publish! pconn topic message)
(p/finally (fn [_ cause]
(when (and cause (redis/open? pconn))
(a/offer! res cause))
(a/close! res))))
res))
(try
(p/await! (rds/publish! pconn topic (t/encode message)))
(catch InterruptedException cause
(throw cause))
(catch Throwable cause
(l/error :hint "unexpected error on publishing"
:message message
:cause cause))))
(defn redis-sub
(defn- redis-sub!
"Create redis subscription. Blocking operation, intended to be used
inside an agent."
[{:keys [::sconn] :as cfg} topic]
(redis/subscribe! sconn topic))
(try
(rds/subscribe! sconn topic)
(catch InterruptedException cause
(throw cause))
(catch Throwable cause
(l/trace :hint "exception on subscribing" :topic topic :cause cause))))
(defn redis-unsub
(defn- redis-unsub!
"Removes redis subscription. Blocking operation, intended to be used
inside an agent."
[{:keys [::sconn] :as cfg} topic]
(redis/unsubscribe! sconn topic))
(try
(rds/unsubscribe! sconn topic)
(catch InterruptedException cause
(throw cause))
(catch Throwable cause
(l/trace :hint "exception on unsubscribing" :topic topic :cause cause))))

View File

@@ -8,17 +8,21 @@
"The msgbus abstraction implemented using redis as underlying backend."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.metrics :as mtx]
[app.redis.script :as-alias rscript]
[app.util.cache :as cache]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.core :as c]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p])
[promesa.core :as p]
[promesa.exec :as px])
(:import
clojure.lang.IDeref
clojure.lang.MapEntry
@@ -87,7 +91,7 @@
(s/def ::connect? ::us/boolean)
(s/def ::io-threads ::us/integer)
(s/def ::worker-threads ::us/integer)
(s/def ::cache #(instance? clojure.lang.Atom %))
(s/def ::cache some?)
(s/def ::redis
(s/keys :req [::resources
@@ -99,11 +103,11 @@
(defmethod ig/prep-key ::redis
[_ cfg]
(let [runtime (Runtime/getRuntime)
cpus (.availableProcessors ^Runtime runtime)]
(let [cpus (px/get-available-processors)
threads (max 1 (int (* cpus 0.2)))]
(merge {::timeout (dt/duration "10s")
::io-threads (max 3 cpus)
::worker-threads (max 3 cpus)}
::io-threads (max 3 threads)
::worker-threads (max 3 threads)}
(d/without-nils cfg))))
(defmethod ig/pre-init-spec ::redis [_]
@@ -129,6 +133,15 @@
(def string-codec
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
(defn- create-cache
[{:keys [::wrk/executor] :as cfg}]
(letfn [(on-remove [key val cause]
(l/trace :hint "evict connection (cache)" :key key :reason cause)
(some-> val d/close!))]
(cache/create :executor executor
:on-remove on-remove
:keepalive "5m")))
(defn- initialize-resources
"Initialize redis connection resources"
[{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}]
@@ -145,19 +158,21 @@
(timer ^Timer timer)
(build))
redis-uri (RedisURI/create ^String uri)]
redis-uri (RedisURI/create ^String uri)
cfg (-> cfg
(assoc ::resources resources)
(assoc ::timer timer)
(assoc ::redis-uri redis-uri))]
(-> cfg
(assoc ::resources resources)
(assoc ::timer timer)
(assoc ::cache (atom {}))
(assoc ::redis-uri redis-uri))))
(assoc cfg ::cache (create-cache cfg))))
(defn- shutdown-resources
[{:keys [::resources ::cache ::timer]}]
(run! d/close! (vals @cache))
(cache/invalidate-all! cache)
(when resources
(.shutdown ^ClientResources resources))
(when timer
(.stop ^Timer timer)))
@@ -173,6 +188,7 @@
:default (.connect ^RedisClient client ^RedisCodec codec)
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
(l/trc :hint "connect" :hid (hash client))
(.setTimeout ^StatefulConnection conn ^Duration timeout)
(reify
IDeref
@@ -180,8 +196,9 @@
AutoCloseable
(close [_]
(.close ^StatefulConnection conn)
(.shutdown ^RedisClient client)))))
(ex/ignoring (.close ^StatefulConnection conn))
(ex/ignoring (.shutdown ^RedisClient client))
(l/trc :hint "disconnect" :hid (hash client))))))
(defn connect
[state & {:as opts}]
@@ -194,15 +211,10 @@
(defn get-or-connect
[{:keys [::cache] :as state} key options]
(us/assert! ::redis state)
(-> state
(assoc ::connection
(or (get @cache key)
(-> (swap! cache (fn [cache]
(when-let [prev (get cache key)]
(d/close! prev))
(assoc cache key (connect* state options))))
(get key))))
(dissoc ::cache)))
(let [connection (cache/get cache key (fn [_] (connect* state options)))]
(-> state
(dissoc ::cache)
(assoc ::connection connection))))
(defn add-listener!
[{:keys [::connection] :as conn} listener]
@@ -344,7 +356,7 @@
(do
(l/error :hint "no script found" :name sname :cause cause)
(->> (load-script)
(p/mapcat eval-script)))
(p/mcat eval-script)))
(if-let [on-error (::rscript/on-error script)]
(on-error cause)
(p/rejected cause))))
@@ -375,15 +387,16 @@
(load-script []
(l/trace :hint "load script" :name sname)
(->> (.scriptLoad ^RedisScriptingAsyncCommands cmd
^String (read-script))
(p/map (fn [sha]
(swap! scripts-cache assoc sname sha)
sha))))]
^String (read-script))
(p/fmap (fn [sha]
(swap! scripts-cache assoc sname sha)
sha))))]
(if-let [sha (get @scripts-cache sname)]
(eval-script sha)
(->> (load-script)
(p/mapcat eval-script))))))
(p/await!
(if-let [sha (get @scripts-cache sname)]
(eval-script sha)
(->> (load-script)
(p/mapcat eval-script)))))))
(defn timeout-exception?
[cause]

View File

@@ -10,8 +10,8 @@
[app.common.data :as d]
[app.common.exceptions :as ex]
[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]
[app.http :as-alias http]
@@ -19,7 +19,6 @@
[app.http.client :as-alias http.client]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.loggers.webhooks :as-alias webhooks]
[app.main :as-alias main]
[app.metrics :as mtx]
[app.msgbus :as-alias mbus]
@@ -35,7 +34,6 @@
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
@@ -47,12 +45,10 @@
(defn- handle-response-transformation
[response request mdata]
(let [transform-fn (reduce (fn [res-fn transform-fn]
(fn [request response]
(p/then (res-fn request response) #(transform-fn request %))))
(constantly response)
(::response-transform-fns mdata))]
(transform-fn request response)))
(reduce (fn [response transform-fn]
(transform-fn request response))
response
(::response-transform-fns mdata)))
(defn- handle-before-comple-hook
[response mdata]
@@ -63,67 +59,18 @@
(defn- handle-response
[request result]
(if (fn? result)
(p/wrap (result request))
(result request)
(let [mdata (meta result)]
(p/-> (yrs/response {:status (::http/status mdata 200)
:headers (::http/headers mdata {})
:body (rph/unwrap result)})
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata)))))
(-> {::yrs/status (::http/status mdata 200)
::yrs/headers (::http/headers mdata {})
::yrs/body (rph/unwrap result)}
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata)))))
(defn- rpc-query-handler
"Ring handler that dispatches query requests and convert between
internal async flow into ring async flow."
[methods {:keys [params path-params] :as request} respond raise]
(let [type (keyword (:type path-params))
profile-id (or (::session/profile-id request)
(::actoken/profile-id request))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request))
data (if profile-id
(-> data
(assoc :profile-id profile-id)
(assoc ::profile-id profile-id))
(dissoc data :profile-id ::profile-id))
method (get methods type default-handler)]
(->> (method data)
(p/mcat (partial handle-response request))
(p/fnly (fn [response cause]
(if cause
(raise cause)
(respond response)))))))
(defn- rpc-mutation-handler
"Ring handler that dispatches mutation requests and convert between
internal async flow into ring async flow."
[methods {:keys [params path-params] :as request} respond raise]
(let [type (keyword (:type path-params))
profile-id (or (::session/profile-id request)
(::actoken/profile-id request))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request))
data (if profile-id
(-> data
(assoc :profile-id profile-id)
(assoc ::profile-id profile-id))
(dissoc data :profile-id))
method (get methods type default-handler)]
(->> (method data)
(p/mcat (partial handle-response request))
(p/fnly (fn [response cause]
(if cause
(raise cause)
(respond response)))))))
(defn- rpc-command-handler
(defn- rpc-handler
"Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow."
[methods {:keys [params path-params] :as request} respond raise]
[methods {:keys [params path-params] :as request}]
(let [type (keyword (:type path-params))
etag (yrq/get-header request "if-none-match")
profile-id (or (::session/profile-id request)
@@ -132,20 +79,16 @@
data (-> params
(assoc ::request-at (dt/now))
(assoc ::session/id (::session/id request))
(assoc ::http/request request)
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)
(assoc ::profile-id profile-id)))
method (get methods type default-handler)]
data (vary-meta data assoc ::http/request request)
method (get methods type default-handler)]
(binding [cond/*enabled* true]
(->> (method data)
(p/mcat (partial handle-response request))
(p/fnly (fn [response cause]
(if cause
(raise cause)
(respond response))))))))
(let [response (method data)]
(handle-response request response)))))
(defn- wrap-metrics
"Wrap service method with metrics measurement."
@@ -153,127 +96,86 @@
(let [labels (into-array String [(::sv/name mdata)])]
(fn [cfg params]
(let [tp (dt/tpoint)]
(->> (f cfg params)
(p/fnly (fn [_ _]
(mtx/run! metrics
:id metrics-id
:val (inst-ms (tp))
:labels labels))))))))
(try
(f cfg params)
(finally
(mtx/run! metrics
:id metrics-id
:val (inst-ms (tp))
:labels labels)))))))
(defn- wrap-authentication
[_ f mdata]
(fn [cfg params]
(let [profile-id (::profile-id params)]
(if (and (::auth mdata true) (not (uuid? profile-id)))
(p/rejected
(ex/error :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint"))
(ex/raise :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint")
(f cfg params)))))
(defn- wrap-access-token
"Wraps service method with access token validation."
[_ f {:keys [::sv/name] :as mdata}]
(if (contains? cf/flags :access-tokens)
(fn [cfg params]
(let [request (::http/request params)]
(if (contains? request ::actoken/id)
(let [perms (::actoken/perms request #{})]
(if (contains? perms name)
(f cfg params)
(p/rejected
(ex/error :type :authorization
:code :operation-not-allowed
:allowed perms))))
(f cfg params))))
f))
(defn- wrap-dispatch
"Wraps service method into async flow, with the ability to dispatching
it to a preconfigured executor service."
[{:keys [::wrk/executor] :as cfg} f mdata]
(with-meta
(fn [cfg params]
(->> (px/submit! executor (px/wrap-bindings #(f cfg params)))
(p/mapcat p/wrap)
(p/map rph/wrap)))
mdata))
(defn- wrap-audit
[cfg f mdata]
[_ f mdata]
(if (or (contains? cf/flags :webhooks)
(contains? cf/flags :audit-log))
(letfn [(handle-audit [params result]
(let [resultm (meta result)
request (::http/request params)
profile-id (or (::audit/profile-id resultm)
(:profile-id result)
(if (= (::type cfg) "command")
(::profile-id params)
(:profile-id params))
uuid/zero)
props (-> (or (::audit/replace-props resultm)
(-> params
(merge (::audit/props resultm))
(dissoc :profile-id)
(dissoc :type)))
(audit/clean-props))
event {:type (or (::audit/type resultm)
(::type cfg))
:name (or (::audit/name resultm)
(::sv/name mdata))
:profile-id profile-id
:ip-addr (some-> request audit/parse-client-ip)
:props props
;; NOTE: for batch-key lookup we need the params as-is
;; because the rpc api does not need to know the
;; audit/webhook specific object layout.
::params (dissoc params ::http/request)
::webhooks/batch-key
(or (::webhooks/batch-key mdata)
(::webhooks/batch-key resultm))
::webhooks/batch-timeout
(or (::webhooks/batch-timeout mdata)
(::webhooks/batch-timeout resultm))
::webhooks/event?
(or (::webhooks/event? mdata)
(::webhooks/event? resultm)
false)}]
(audit/submit! cfg event)))
(handle-request [cfg params]
(->> (f cfg params)
(p/fnly (fn [result cause]
(when-not cause
(handle-audit params result))))))]
(if-not (::audit/skip mdata)
(with-meta handle-request mdata)
f))
(if-not (::audit/skip mdata)
(fn [cfg params]
(let [result (f cfg params)]
(->> (audit/prepare-event cfg mdata params result)
(audit/submit! cfg))
result))
f)
f))
(defn- wrap-spec-conform
[_ f mdata]
(let [spec (or (::sv/spec mdata) (s/spec any?))]
(fn [cfg params]
(let [params (ex/try! (us/conform spec params))]
(if (ex/exception? params)
(p/rejected params)
(f cfg params))))))
;; NOTE: skip spec conform operation on rpc methods that already
;; uses malli validation mechanism.
(if (contains? mdata ::sm/params)
f
(if-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
(fn [cfg params]
(f cfg (us/conform spec params)))
f)))
(defn- wrap-params-validation
[_ f mdata]
(if-let [schema (::sm/params mdata)]
(let [schema (sm/schema schema)
valid? (sm/validator schema)
explain (sm/explainer schema)
decode (sm/decoder schema sm/default-transformer)]
(fn [cfg params]
(let [params (decode params)]
(if (valid? params)
(f cfg params)
(ex/raise :type :validation
:code :params-validation
::sm/explain (explain params))))))
f))
(defn- wrap-output-validation
[_ f mdata]
(if (contains? cf/flags :rpc-output-validation)
(or (when-let [schema (::sm/result mdata)]
(let [schema (sm/schema schema)
valid? (sm/validator schema)
explain (sm/explainer schema)]
(fn [cfg params]
(let [response (f cfg params)]
(when (map? response)
(when-not (valid? response)
(ex/raise :type :validation
:code :data-validation
::sm/explain (explain response))))
response))))
f)
f))
(defn- wrap-all
[cfg f mdata]
(as-> f $
(wrap-dispatch cfg $ mdata)
(wrap-metrics cfg $ mdata)
(cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
@@ -281,43 +183,19 @@
(rlimit/wrap cfg $ mdata)
(wrap-audit cfg $ mdata)
(wrap-spec-conform cfg $ mdata)
(wrap-authentication cfg $ mdata)
(wrap-access-token cfg $ mdata)))
(wrap-output-validation cfg $ mdata)
(wrap-params-validation cfg $ mdata)
(wrap-authentication cfg $ mdata)))
(defn- wrap
[cfg f mdata]
(l/debug :hint "register method" :name (::sv/name mdata))
(let [f (wrap-all cfg f mdata)]
(with-meta #(f cfg %) mdata)))
(partial f cfg)))
(defn- process-method
[cfg vfn]
(let [mdata (meta vfn)]
[(keyword (::sv/name mdata))
(wrap cfg vfn mdata)]))
(defn- resolve-query-methods
[cfg]
(let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)]
(->> (sv/scan-ns
'app.rpc.queries.projects
'app.rpc.queries.profile
'app.rpc.queries.viewer
'app.rpc.queries.fonts)
(map (partial process-method cfg))
(into {}))))
(defn- resolve-mutation-methods
[cfg]
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
(->> (sv/scan-ns
'app.rpc.mutations.media
'app.rpc.mutations.profile
'app.rpc.mutations.projects
'app.rpc.mutations.fonts
'app.rpc.mutations.share-link)
(map (partial process-method cfg))
(into {}))))
[cfg [vfn mdata]]
[(keyword (::sv/name mdata)) [mdata (wrap cfg vfn mdata)]])
(defn- resolve-command-methods
[cfg]
@@ -336,6 +214,8 @@
'app.rpc.commands.files-share
'app.rpc.commands.files-temp
'app.rpc.commands.files-update
'app.rpc.commands.files-snapshot
'app.rpc.commands.files-thumbnails
'app.rpc.commands.ldap
'app.rpc.commands.management
'app.rpc.commands.media
@@ -366,23 +246,10 @@
(defmethod ig/init-key ::methods
[_ cfg]
(let [cfg (d/without-nils cfg)]
{:mutations (resolve-mutation-methods cfg)
:queries (resolve-query-methods cfg)
:commands (resolve-command-methods cfg)}))
(s/def ::mutations
(s/map-of keyword? fn?))
(s/def ::queries
(s/map-of keyword? fn?))
(s/def ::commands
(s/map-of keyword? fn?))
(resolve-command-methods cfg)))
(s/def ::methods
(s/keys :req-un [::mutations
::queries
::commands]))
(s/map-of keyword? (s/tuple map? fn?)))
(s/def ::routes vector?)
@@ -391,15 +258,11 @@
::db/pool
::main/props
::wrk/executor
::session/manager
::actoken/manager]))
::session/manager]))
(defmethod ig/init-key ::routes
[_ {:keys [::methods] :as cfg}]
[["/rpc" {:middleware [[session/authz cfg]
[actoken/authz cfg]]}
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
:allowed-methods #{:post}}]]])
(let [methods (update-vals methods peek)]
[["/rpc" {:middleware [[session/authz cfg]
[actoken/authz cfg]]}
["/command/:type" {:handler (partial rpc-handler methods)}]]]))

View File

@@ -6,14 +6,16 @@
(ns app.rpc.climit
"Concurrencly limiter for RPC."
(:refer-clojure :exclude [run!])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[app.metrics :as mtx]
[app.rpc :as-alias rpc]
[app.rpc.climit.config :as-alias config]
[app.util.cache :as cache]
[app.util.services :as-alias sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
@@ -23,184 +25,200 @@
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[promesa.exec.bulkhead :as pxb])
[promesa.exec.bulkhead :as pbh])
(:import
com.github.benmanes.caffeine.cache.Cache
com.github.benmanes.caffeine.cache.CacheLoader
com.github.benmanes.caffeine.cache.Caffeine
com.github.benmanes.caffeine.cache.RemovalListener))
clojure.lang.ExceptionInfo))
(defn- capacity-exception?
[o]
(and (ex/error? o)
(let [data (ex-data o)]
(and (= :bulkhead-error (:type data))
(= :capacity-limit-reached (:code data))))))
(set! *warn-on-reflection* true)
(defn invoke!
[limiter f]
(->> (px/submit! limiter f)
(p/hcat (fn [result cause]
(cond
(capacity-exception? cause)
(p/rejected
(ex/error :type :internal
:code :concurrency-limit-reached
:queue (-> limiter meta ::bkey name)
:cause cause))
(defn- create-bulkhead-cache
[{:keys [::wrk/executor]} config]
(letfn [(load-fn [key]
(let [config (get config (nth key 0))]
(l/trace :hint "insert into cache" :key key)
(pbh/create :permits (or (:permits config) (:concurrency config))
:queue (or (:queue config) (:queue-size config))
:timeout (:timeout config)
:executor executor
:type (:type config :semaphore))))
(some? cause)
(p/rejected cause)
(on-remove [_ _ cause]
(l/trace :hint "evict from cache" :key key :reason (str cause)))]
:else
(p/resolved result))))))
(cache/create :executor :same-thread
:on-remove on-remove
:keepalive "5m"
:load-fn load-fn)))
(defn- create-limiter
[{:keys [::wrk/executor ::mtx/metrics ::bkey ::skey concurrency queue-size]}]
(let [labels (into-array String [(name bkey)])
on-queue (fn [instance]
(l/trace :hint "enqueued"
:key (name bkey)
:skey (str skey)
:queue-size (get instance ::pxb/current-queue-size)
:concurrency (get instance ::pxb/current-concurrency))
(mtx/run! metrics
:id :rpc-climit-queue-size
:val (get instance ::pxb/current-queue-size)
:labels labels)
(mtx/run! metrics
:id :rpc-climit-concurrency
:val (get instance ::pxb/current-concurrency)
:labels labels))
on-run (fn [instance task]
(let [elapsed (- (inst-ms (dt/now))
(inst-ms task))]
(l/trace :hint "execute"
:key (name bkey)
:skey (str skey)
:elapsed (str elapsed "ms"))
(mtx/run! metrics
:id :rpc-climit-timing
:val elapsed
:labels labels)
(mtx/run! metrics
:id :rpc-climit-queue-size
:val (get instance ::pxb/current-queue-size)
:labels labels)
(mtx/run! metrics
:id :rpc-climit-concurrency
:val (get instance ::pxb/current-concurrency)
:labels labels)))
options {:executor executor
:concurrency concurrency
:queue-size (or queue-size Integer/MAX_VALUE)
:on-queue on-queue
:on-run on-run}]
(-> (pxb/create options)
(vary-meta assoc ::bkey bkey ::skey skey))))
(defn- create-cache
[{:keys [::wrk/executor] :as params} config]
(let [listener (reify RemovalListener
(onRemoval [_ key _val cause]
(l/trace :hint "cache: remove" :key key :reason (str cause))))
loader (reify CacheLoader
(load [_ key]
(let [[bkey skey] key]
(when-let [config (get config bkey)]
(-> (merge params config)
(assoc ::bkey bkey)
(assoc ::skey skey)
(create-limiter))))))]
(.. (Caffeine/newBuilder)
(weakValues)
(executor executor)
(removalListener listener)
(build loader))))
(defprotocol IConcurrencyManager)
(s/def ::concurrency ::us/integer)
(s/def ::queue-size ::us/integer)
(s/def ::config/permits ::us/integer)
(s/def ::config/queue ::us/integer)
(s/def ::config/timeout ::us/integer)
(s/def ::config
(s/map-of keyword?
(s/keys :req-un [::concurrency]
:opt-un [::queue-size])))
(s/keys :opt-un [::config/permits
::config/queue
::config/timeout])))
(defmethod ig/prep-key ::rpc/climit
[_ cfg]
(merge {::path (cf/get :rpc-climit-config)}
(d/without-nils cfg)))
(assoc cfg ::path (cf/get :rpc-climit-config)))
(s/def ::path ::fs/path)
(defmethod ig/pre-init-spec ::rpc/climit [_]
(s/keys :req [::wrk/executor ::mtx/metrics ::path]))
(defmethod ig/init-key ::rpc/climit
[_ {:keys [::path] :as params}]
[_ {:keys [::path ::mtx/metrics ::wrk/executor] :as cfg}]
(when (contains? cf/flags :rpc-climit)
(if-let [config (some->> path slurp edn/read-string)]
(do
(l/info :hint "initializing concurrency limit" :config (str path))
(us/verify! ::config config)
(let [cache (create-cache params config)]
^{::cache cache}
(reify
IConcurrencyManager
clojure.lang.IDeref
(deref [_] config)
clojure.lang.ILookup
(valAt [_ key]
(let [key (if (vector? key) key [key])]
(.get ^Cache cache key))))))
(l/warn :hint "unable to load configuration" :config (str path)))))
(when-let [params (some->> path slurp edn/read-string)]
(l/info :hint "initializing concurrency limit" :config (str path))
(us/verify! ::config params)
{::cache (create-bulkhead-cache cfg params)
::config params
::wrk/executor executor
::mtx/metrics metrics})))
(s/def ::cache cache/cache?)
(s/def ::instance
(s/keys :req [::cache ::config ::wrk/executor]))
(s/def ::rpc/climit
(s/nilable #(satisfies? IConcurrencyManager %)))
(s/nilable ::instance))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn invoke!
[cache metrics id key f]
(let [limiter (cache/get cache [id key])
tpoint (dt/tpoint)
labels (into-array String [(name id)])
wrapped
(fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(l/trace :hint "executed"
:id (name id)
:key key
:fnh (hash f)
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats)
:elapsed (dt/format-duration elapsed))
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels labels)
(try
(f)
(finally
(let [elapsed (tpoint)]
(l/trace :hint "finished"
:id (name id)
:key key
:fnh (hash f)
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats)
:elapsed (dt/format-duration elapsed)))))))
measure!
(fn [stats]
(mtx/run! metrics
:id :rpc-climit-queue
:val (:queue stats)
:labels labels)
(mtx/run! metrics
:id :rpc-climit-permits
:val (:permits stats)
:labels labels))]
(try
(let [stats (pbh/get-stats limiter)]
(measure! stats)
(l/trace :hint "enqueued"
:id (name id)
:key key
:fnh (hash f)
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats))
(pbh/invoke! limiter wrapped))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]
(if (= :bulkhead-error type)
(ex/raise :type :concurrency-limit
:code code
:hint "concurrency limit reached")
(throw cause))))
(finally
(measure! (pbh/get-stats limiter))))))
(defn run!
[{:keys [::id ::cache ::mtx/metrics]} f]
(if (and cache id)
(invoke! cache metrics id nil f)
(f)))
(defn submit!
[{:keys [::id ::cache ::wrk/executor ::mtx/metrics]} f]
(let [f (partial px/submit! executor (px/wrap-bindings f))]
(if (and cache id)
(p/await! (invoke! cache metrics id nil f))
(p/await! (f)))))
(defn configure
([{:keys [::rpc/climit]} id]
(us/assert! ::rpc/climit climit)
(assoc climit ::id id))
([{:keys [::rpc/climit]} id executor]
(us/assert! ::rpc/climit climit)
(-> climit
(assoc ::id id)
(assoc ::wrk/executor executor))))
(defmacro with-dispatch!
"Dispatch blocking operation to a separated thread protected with the
specified concurrency limiter. If climit is not active, the function
will be scheduled to execute without concurrency monitoring."
[instance & body]
(if (vector? instance)
`(-> (app.rpc.climit/configure ~@instance)
(app.rpc.climit/run! (^:once fn* [] ~@body)))
`(run! ~instance (^:once fn* [] ~@body))))
(defmacro with-dispatch
[lim & body]
`(if ~lim
(invoke! ~lim (^:once fn [] (p/wrap (do ~@body))))
(p/wrap (do ~@body))))
"Dispatch blocking operation to a separated thread protected with
the specified semaphore.
DEPRECATED"
[& params]
`(with-dispatch! ~@params))
(def noop-fn (constantly nil))
(defn wrap
[{:keys [::rpc/climit]} f {:keys [::queue ::key-fn] :as mdata}]
(if (and (some? climit)
(some? queue))
(if-let [config (get @climit queue)]
(do
[{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
(if (and (some? climit) (some? id))
(if-let [config (get-in climit [::config id])]
(let [cache (::cache climit)]
(l/debug :hint "wrap: instrumenting method"
:limit-name (name queue)
:limit (name id)
:service-name (::sv/name mdata)
:queue-size (or (:queue-size config) Integer/MAX_VALUE)
:concurrency (:concurrency config)
:timeout (:timeout config)
:permits (:permits config)
:queue (:queue config)
:keyed? (some? key-fn))
(if (some? key-fn)
(fn [cfg params]
(let [key [queue (key-fn params)]
lim (get climit key)]
(invoke! lim (partial f cfg params))))
(let [lim (get climit queue)]
(fn [cfg params]
(invoke! lim (partial f cfg params))))))
(fn [cfg params]
(invoke! cache metrics id (key-fn params) (partial f cfg params))))
(do
(l/warn :hint "wrap: no config found"
:queue (name queue)
:service (::sv/name mdata))
(l/warn :hint "no config found for specified queue" :id id)
f))
f))

View File

@@ -19,18 +19,19 @@
[clojure.spec.alpha :as s]))
(defn- decode-row
[{:keys [perms] :as row}]
(cond-> row
(db/pgarray? perms "text")
(assoc :perms (db/decode-pgarray perms #{}))))
[row]
(dissoc row :perms))
(defn- create-access-token
[{:keys [::conn ::main/props]} profile-id name perms]
(defn create-access-token
[{:keys [::db/conn ::main/props]} profile-id name expiration]
(let [created-at (dt/now)
token-id (uuid/next)
token (tokens/generate props {:iss "access-token"
:tid token-id
:iat created-at})]
:iat created-at})
expires-at (some-> expiration dt/in-future)]
(db/insert! conn :access-token
{:id token-id
:name name
@@ -38,33 +39,36 @@
:profile-id profile-id
:created-at created-at
:updated-at created-at
:perms (db/create-array conn "text" perms)})))
:expires-at expires-at
:perms (db/create-array conn "text" [])})))
(defn repl-create-access-token
[{:keys [::db/pool] :as system} profile-id name perms]
[{:keys [::db/pool] :as system} profile-id name expiration]
(db/with-atomic [conn pool]
(let [props (:app.setup/props system)]
(create-access-token {::conn conn ::main/props props}
(create-access-token {::db/conn conn ::main/props props}
profile-id
name
perms))))
expiration))))
(s/def ::name ::us/not-empty-string)
(s/def ::perms ::us/set-of-strings)
(s/def ::expiration ::dt/duration)
(s/def ::create-access-token
(s/keys :req [::rpc/profile-id]
:req-un [::name ::perms]))
:req-un [::name]
:opt-un [::expiration]))
(sv/defmethod ::create-access-token
{::doc/added "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name perms]}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg ::conn conn)]
(let [cfg (assoc cfg ::db/conn conn)]
(quotes/check-quote! conn
{::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(-> (create-access-token cfg profile-id name perms)
(-> (create-access-token cfg profile-id name expiration)
(decode-row)))))
(s/def ::delete-access-token
@@ -83,5 +87,8 @@
(sv/defmethod ::get-access-tokens
{::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
(->> (db/query pool :access-token {:profile-id profile-id})
(->> (db/query pool :access-token
{:profile-id profile-id}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:id :name :perms :created-at :updated-at :expires-at]})
(mapv decode-row)))

View File

@@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -19,12 +19,7 @@
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]))
[app.util.services :as sv]))
(defn- event->row [event]
[(uuid/next)
@@ -42,8 +37,9 @@
:profile-id :ip-addr :props :context])
(defn- handle-events
[{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request]}]
(let [ip-addr (audit/parse-client-ip request)
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request)
ip-addr (audit/parse-client-ip request)
xform (comp
(map #(assoc % :profile-id profile-id))
(map #(assoc % :ip-addr ip-addr))
@@ -54,34 +50,38 @@
(when (seq events)
(db/insert-multi! pool :audit-log event-columns events))))
(s/def ::name ::us/string)
(s/def ::type ::us/string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::timestamp dt/instant?)
(s/def ::context (s/map-of ::us/keyword any?))
(def schema:event
[:map {:title "Event"}
[:name [:string {:max 250}]]
[:type [:string {:max 250}]]
[:props
[:map-of :keyword :any]]
[:context {:optional true}
[:map-of :keyword :any]]])
(s/def ::event
(s/keys :req-un [::type ::name ::props ::timestamp]
:opt-un [::context]))
(s/def ::events (s/every ::event))
(s/def ::push-audit-events
(s/keys :req [::rpc/profile-id]
:req-un [::events]))
(def schema:push-audit-events
[:map {:title "push-audit-events"}
[:events [:vector schema:event]]])
(sv/defmethod ::push-audit-events
{::climit/queue :push-audit-events
{::climit/id :submit-audit-events-by-profile
::climit/key-fn ::rpc/profile-id
::sm/params schema:push-audit-events
::audit/skip true
::doc/skip true
::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} params]
[{:keys [::db/pool] :as cfg} params]
(if (or (db/read-only? pool)
(not (contains? cf/flags :audit-log)))
(do
(l/warn :hint "audit: http handler disabled or db is read-only")
(rph/wrap nil))
(->> (px/submit! executor #(handle-events cfg params))
(p/fmap (constantly nil)))))
(do
(try
(handle-events cfg params)
(catch Throwable cause
(l/error :hint "unexpected error on persisting audit events from frontend"
:cause cause)))
(rph/wrap nil))))

View File

@@ -8,8 +8,10 @@
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -18,7 +20,6 @@
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
@@ -26,31 +27,13 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/string)
(s/def ::path ::us/string)
(s/def ::password ::us/not-empty-string)
(s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string)
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::token ::us/not-empty-string)
(def schema:password
[::sm/word-string {:max 500}])
;; ---- HELPERS
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string."
[domains email]
(if (or (empty? domains)
(nil? domains))
true
(let [[_ candidate] (-> (str/lower email)
(str/split #"@" 2))]
(contains? domains candidate))))
(def schema:token
[::sm/word-string {:max 6000}])
;; ---- COMMAND: login with password
@@ -63,14 +46,18 @@
:code :login-disabled
:hint "login is disabled in this instance"))
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
(letfn [(check-password [conn profile password]
(if (= (:password profile) "!")
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password"))
(:valid (auth/verify-password password (:password profile))))
:hint "the current account does not have password")
(let [result (profile/verify-password cfg password (:password profile))]
(when (:update result)
(l/trace :hint "updating profile password" :id (:id profile) :email (:email profile))
(profile/update-profile-password! conn (assoc profile :password password)))
(:valid result))))
(validate-profile [profile]
(validate-profile [conn profile]
(when-not profile
(ex/raise :type :validation
:code :wrong-credentials))
@@ -80,7 +67,7 @@
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(when-not (check-password profile password)
(when-not (check-password conn profile password)
(ex/raise :type :validation
:code :wrong-credentials))
(when-let [deleted-at (:deleted-at profile)]
@@ -92,8 +79,7 @@
(db/with-atomic [conn pool]
(let [profile (->> (profile/get-profile-by-email conn email)
(validate-profile)
(profile/decode-row)
(validate-profile conn)
(profile/strip-private-attrs))
invitation (when-let [token (:invitation-token params)]
@@ -111,23 +97,22 @@
(rph/with-meta {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(s/def ::login-with-password
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(def schema:login-with-password
[:map {:title "login-with-password"}
[:email ::sm/email]
[:password schema:password]
[:invitation-token {:optional true} schema:token]])
(sv/defmethod ::login-with-password
"Performs authentication using penpot password."
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
::doc/added "1.15"
::sm/params schema:login-with-password}
[cfg params]
(login-with-password cfg params))
;; ---- COMMAND: Logout
(s/def ::logout
(s/keys :opt [::rpc/profile-id]))
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
{::rpc/auth false
@@ -144,7 +129,7 @@
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (auth/derive-password password)]
(let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool]
@@ -152,14 +137,15 @@
(update-password conn))
nil)))
(s/def ::token ::us/not-empty-string)
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(def schema:recover-profile
[:map {:title "recover-profile"}
[:token schema:token]
[:password schema:password]])
(sv/defmethod ::recover-profile
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
::doc/added "1.15"
::sm/params schema:recover-profile}
[cfg params]
(recover-profile cfg params))
@@ -169,19 +155,20 @@
[{:keys [::db/pool] :as cfg} params]
(when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token)
(when-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)
(let [invitation (tokens/verify (::main/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")))))
:code :registration-disabled)))
(when-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params))
(ex/raise :type :validation
:code :email-domain-is-not-allowed)))
(when (contains? params :invitation-token)
(let [invitation (tokens/verify (::main/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
:code :email-domain-is-not-allowed))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spammer.
@@ -239,13 +226,16 @@
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
(s/def ::prepare-register-profile
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(def schema:prepare-register-profile
[:map {:title "prepare-register-profile"}
[:email ::sm/email]
[:password schema:password]
[:invitation-token {:optional true} schema:token]])
(sv/defmethod ::prepare-register-profile
{::rpc/auth false
::doc/added "1.15"}
::doc/added "1.15"
::sm/params schema:prepare-register-profile}
[cfg params]
(prepare-register cfg params))
@@ -255,7 +245,7 @@
"Create the profile entry on the database with limited set of input
attrs (all the other attrs are filled with default values)."
[conn {:keys [email] :as params}]
(us/assert! ::us/email email)
(dm/assert! ::sm/email email)
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
@@ -264,9 +254,7 @@
:nudge {:big 10 :small 1}})
(db/tjson))
password (if-let [password (:password params)]
(auth/derive-password password)
"!")
password (or (:password params) "!")
locale (:locale params)
locale (when (and (string? locale) (not (str/blank? locale)))
@@ -335,17 +323,20 @@
:extra-data ptoken})))
(defn register-profile
[{:keys [::db/conn] :as cfg} {:keys [token] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}]
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
params (merge params claims)
params (assoc claims :fullname fullname)
is-active (or (:is-active params)
(not (contains? cf/flags :email-verification)))
profile (if-let [profile-id (:profile-id claims)]
(profile/get-profile conn profile-id)
(->> (create-profile! conn (assoc params :is-active is-active))
(create-profile-rels! conn)))
(let [params (-> params
(assoc :is-active is-active)
(update :password #(profile/derive-password cfg %)))]
(->> (create-profile! conn params)
(create-profile-rels! conn))))
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
@@ -356,9 +347,9 @@
(when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
(audit/submit! cfg
{:type "fact"
:name "register-profile-retry"
:profile-id id}))
{::audit/type "fact"
::audit/name "register-profile-retry"
::audit/profile-id id}))
(cond
;; If invitation token comes in params, this is because the
@@ -401,13 +392,16 @@
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))
(def schema:register-profile
[:map {:title "register-profile"}
[:token schema:token]
[:fullname [::sm/word-string {:max 100}]]])
(sv/defmethod ::register-profile
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
::doc/added "1.15"
::sm/params schema:register-profile}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg ::db/conn conn)
@@ -459,12 +453,15 @@
(create-recovery-token)
(send-email-notification conn))))))
(s/def ::request-profile-recovery
(s/keys :req-un [::email]))
(def schema:request-profile-recovery
[:map {:title "request-profile-recovery"}
[:email ::sm/email]])
(sv/defmethod ::request-profile-recovery
{::rpc/auth false
::doc/added "1.15"}
::doc/added "1.15"
::sm/params schema:request-profile-recovery}
[cfg params]
(request-profile-recovery cfg params))

View File

@@ -8,11 +8,14 @@
(:refer-clojure :exclude [assert])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.files.migrations :as pmg]
[app.common.fressian :as fres]
[app.common.logging :as l]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -28,7 +31,6 @@
[app.storage.tmp :as tmp]
[app.tasks.file-gc]
[app.util.blob :as blob]
[app.util.fressian :as fres]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
@@ -45,8 +47,7 @@
java.io.DataInputStream
java.io.DataOutputStream
java.io.InputStream
java.io.OutputStream
java.lang.AutoCloseable))
java.io.OutputStream))
(set! *warn-on-reflection* true)
@@ -294,28 +295,40 @@
[output & {:keys [level] :or {level 0}}]
(ZstdOutputStream. ^OutputStream output (int level)))
(defn- retrieve-file
[pool file-id]
(with-open [^AutoCloseable conn (db/open pool)]
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
(some-> (db/get* conn :file {:id file-id})
(files/decode-row)
(update :data files/process-pointers deref)))))
(defn- get-files
[cfg ids]
(letfn [(get-files* [{:keys [::db/conn]}]
(let [sql (str "SELECT id FROM file "
" WHERE id = ANY(?) ")
ids (db/create-array conn "uuid" ids)]
(->> (db/exec! conn [sql ids])
(into [] (map :id))
(not-empty))))]
(def ^:private sql:file-media-objects
"SELECT * FROM file_media_object WHERE id = ANY(?)")
(db/run! cfg get-files*)))
(defn- retrieve-file-media
[pool {:keys [data id] :as file}]
(with-open [^AutoCloseable conn (db/open pool)]
(defn- get-file
[cfg file-id]
(letfn [(get-file* [{:keys [::db/conn]}]
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
(some-> (db/get* conn :file {:id file-id} {::db/remove-deleted? false})
(files/decode-row)
(files/process-pointers deref))))]
(db/run! cfg get-file*)))
(defn- get-file-media
[{:keys [::db/pool]} {:keys [data id] :as file}]
(dm/with-open [conn (db/open pool)]
(let [ids (app.tasks.file-gc/collect-used-media data)
ids (db/create-array conn "uuid" ids)]
ids (db/create-array conn "uuid" ids)
sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")]
;; We assoc the file-id again to the file-media-object row
;; because there are cases that used objects refer to other
;; files and we need to ensure in the exportation process that
;; all ids matches
(->> (db/exec! conn [sql:file-media-objects ids])
(->> (db/exec! conn [sql ids])
(mapv #(assoc % :file-id id))))))
(def ^:private storage-object-id-xf
@@ -325,35 +338,32 @@
(def ^:private sql:file-libraries
"WITH RECURSIVE libs AS (
SELECT fl.id, fl.deleted_at
SELECT fl.id
FROM file AS fl
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
WHERE flr.file_id = ANY(?)
UNION
SELECT fl.id, fl.deleted_at
SELECT fl.id
FROM file AS fl
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
JOIN libs AS l ON (flr.file_id = l.id)
)
SELECT DISTINCT l.id
FROM libs AS l
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
FROM libs AS l")
(defn- retrieve-libraries
[pool ids]
(with-open [^AutoCloseable conn (db/open pool)]
(defn- get-libraries
[{:keys [::db/pool]} ids]
(dm/with-open [conn (db/open pool)]
(let [ids (db/create-array conn "uuid" ids)]
(map :id (db/exec! pool [sql:file-libraries ids])))))
(def ^:private sql:file-library-rels
"SELECT * FROM file_library_rel
WHERE file_id = ANY(?)")
(defn- retrieve-library-relations
[pool ids]
(with-open [^AutoCloseable conn (db/open pool)]
(db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)])))
(defn- get-library-relations
[cfg ids]
(db/run! cfg (fn [{:keys [::db/conn]}]
(let [ids (db/create-array conn "uuid" ids)
sql (str "SELECT flr.* FROM file_library_rel AS flr "
" WHERE flr.file_id = ANY(?)")]
(db/exec! conn [sql ids])))))
(defn- create-or-update-file
[conn params]
@@ -373,13 +383,12 @@
;; --- GENERAL PURPOSE DYNAMIC VARS
(def ^:dynamic *state*)
(def ^:dynamic *options*)
(def ^:dynamic *state* nil)
(def ^:dynamic *options* nil)
;; --- EXPORT WRITER
(defn- embed-file-assets
[data conn file-id]
[data cfg file-id]
(letfn [(walk-map-form [form state]
(cond
(uuid? (:fill-color-ref-file form))
@@ -409,7 +418,7 @@
;; 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 (retrieve-file conn lib-id)]
(if-let [lib (get-file cfg lib-id)]
(reduce (partial process-asset lib) data items)
data))
@@ -477,31 +486,43 @@
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
(defmethod write-section :v1/metadata
[{:keys [::db/pool ::output ::file-ids ::include-libraries?]}]
(let [libs (when include-libraries?
(retrieve-libraries pool file-ids))
files (into file-ids libs)]
(write-obj! output {:version cf/version :files files})
(vswap! *state* assoc :files files)))
[{:keys [::output ::file-ids ::include-libraries?] :as cfg}]
(if-let [fids (get-files cfg file-ids)]
(let [lids (when include-libraries?
(get-libraries cfg file-ids))
ids (into fids lids)]
(write-obj! output {:version cf/version :files ids})
(vswap! *state* assoc :files ids))
(ex/raise :type :not-found
:code :files-not-found
:hint "unable to retrieve files for export")))
(defmethod write-section :v1/files
[{:keys [::db/pool ::output ::embed-assets?]}]
[{:keys [::output ::embed-assets? ::include-libraries?] :as cfg}]
;; Initialize SIDS with empty vector
(vswap! *state* assoc :sids [])
(doseq [file-id (-> *state* deref :files)]
(let [file (cond-> (retrieve-file pool file-id)
(let [detach? (and (not embed-assets?) (not include-libraries?))
file (cond-> (get-file cfg file-id)
detach?
(-> (ctf/detach-external-references file-id)
(dissoc :libraries))
embed-assets?
(update :data embed-file-assets pool file-id))
(update :data embed-file-assets cfg file-id))
media (retrieve-file-media pool file)]
media (get-file-media cfg file)]
(l/debug :hint "write penpot file"
:id file-id
:name (:name file)
:media (count media)
::l/sync? true)
(doseq [item media]
(l/debug :hint "write penpot file media object" :id (:id item) ::l/sync? true))
(doto output
(write-obj! file)
(write-obj! media))
@@ -509,9 +530,10 @@
(vswap! *state* update :sids into storage-object-id-xf media))))
(defmethod write-section :v1/rels
[{:keys [::db/pool ::output ::include-libraries?]}]
(let [rels (when include-libraries?
(retrieve-library-relations pool (-> *state* deref :files)))]
[{:keys [::output ::include-libraries?] :as cfg}]
(let [ids (-> *state* deref :files)
rels (when include-libraries?
(get-library-relations cfg ids))]
(l/debug :hint "found rels" :total (count rels) ::l/sync? true)
(write-obj! output rels)))
@@ -519,6 +541,7 @@
[{:keys [::sto/storage ::output]}]
(let [sids (-> *state* deref :sids)
storage (media/configure-assets-storage storage)]
(l/debug :hint "found sobjects"
:items (count sids)
::l/sync? true)
@@ -527,13 +550,13 @@
(write-obj! output sids)
(doseq [id sids]
(let [{:keys [size] :as obj} @(sto/get-object storage id)]
(let [{:keys [size] :as obj} (sto/get-object storage id)]
(l/debug :hint "write sobject" :id id ::l/sync? true)
(doto output
(write-uuid! id)
(write-obj! (meta obj)))
(with-open [^InputStream stream @(sto/get-object-data storage obj)]
(with-open [^InputStream stream (sto/get-object-data storage obj)]
(let [written (write-stream! output stream size)]
(when (not= written size)
(ex/raise :type :validation
@@ -593,7 +616,7 @@
(let [options (-> options
(assoc ::section section)
(assoc ::input input)
(assoc :conn conn))]
(assoc ::db/conn conn))]
(binding [*options* options]
(read-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
@@ -617,37 +640,43 @@
(-> data
(update :pages-index update-vals #(update % :objects omap-wrap))
(update :pages-index update-vals pmap-wrap)
(update :components update-vals #(update % :objects omap-wrap))
(update :components update-vals #(d/update-when % :objects omap-wrap))
(update :components pmap-wrap))))
(defmethod read-section :v1/files
[{:keys [conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
[{:keys [::db/conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}]
(doseq [expected-file-id (-> *state* deref :files)]
(let [file (read-obj! input)
media' (read-obj! input)
file-id (:id file)
features files/default-features]
features (files/get-default-features)]
(when (not= file-id expected-file-id)
(ex/raise :type :validation
:code :inconsistent-penpot-file
:found-id file-id
:expected-id expected-file-id
:hint "the penpot file seems corrupt, found unexpected uuid (file-id)"))
;; Update index using with media
(l/debug :hint "update index with media" ::l/sync? true)
(l/dbg :hint "update index with media" ::l/sync? true)
(vswap! *state* update :index update-index (map :id media'))
;; Store file media for later insertion
(l/debug :hint "update media references" ::l/sync? true)
(l/dbg :hint "update media references" ::l/sync? true)
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
(l/debug :hint "processing file" :file-id file-id ::features features ::l/sync? true)
(binding [ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)
pmap/*tracked* (atom {})]
(l/dbg :hint "processing file"
:id file-id
:features features
:version (-> file :data :version)
::l/sync? true)
(let [file-id' (lookup-index file-id)
data (-> (:data file)
(assoc :id file-id')
@@ -679,22 +708,31 @@
(db/delete! conn :file-thumbnail {:file-id file-id'})))))))
(defmethod read-section :v1/rels
[{:keys [conn ::input ::timestamp]}]
(let [rels (read-obj! input)]
[{:keys [::db/conn ::input ::timestamp]}]
(let [rels (read-obj! input)
ids (into #{} (-> *state* deref :files))]
;; Insert all file relations
(doseq [rel rels]
(doseq [{:keys [library-file-id] :as rel} rels]
(let [rel (-> rel
(assoc :synced-at timestamp)
(update :file-id lookup-index)
(update :library-file-id lookup-index))]
(l/debug :hint "create file library link"
:file-id (:file-id rel)
:lib-id (:library-file-id rel)
::l/sync? true)
(db/insert! conn :file-library-rel rel)))))
(if (contains? ids library-file-id)
(do
(l/debug :hint "create file library link"
:file-id (:file-id rel)
:lib-id (:library-file-id rel)
::l/sync? true)
(db/insert! conn :file-library-rel rel))
(l/warn :hint "ignoring file library link"
:file-id (:file-id rel)
:lib-id (:library-file-id rel)
::l/sync? true))))))
(defmethod read-section :v1/sobjects
[{:keys [::sto/storage conn ::input ::overwrite?]}]
[{:keys [::sto/storage ::db/conn ::input ::overwrite?]}]
(let [storage (media/configure-assets-storage storage)
ids (read-obj! input)]
@@ -719,7 +757,7 @@
(assoc ::sto/touched-at (dt/now))
(assoc :bucket "file-media-object"))
sobject @(sto/put-object! storage params)]
sobject (sto/put-object! storage params)]
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
(vswap! *state* update :index assoc id (:id sobject)))))
@@ -743,7 +781,7 @@
(defn- lookup-index
[id]
(let [val (get-in @*state* [:index id])]
(l/trace :fn "lookup-index" :id id :val val ::l/sync? true)
(l/trc :fn "lookup-index" :id id :val val ::l/sync? true)
(when (and (not (::ignore-index-errors? *options*)) (not val))
(ex/raise :type :validation
:code :incomplete-index
@@ -756,7 +794,7 @@
index index]
(if-let [id (first items)]
(let [new-id (if (::overwrite? *options*) id (uuid/next))]
(l/trace :fn "update-index" :id id :new-id new-id ::l/sync? true)
(l/trc :fn "update-index" :id id :new-id new-id ::l/sync? true)
(recur (rest items)
(assoc index id new-id)))
index)))
@@ -774,8 +812,7 @@
(update-in [:metadata :id] lookup-index)
;; Relink paths with fill image
(and (map? (:fill-image form))
(= :path (:type form)))
(map? (:fill-image form))
(update-in [:fill-image :id] lookup-index)
;; This covers old shapes and the new :fills.
@@ -835,7 +872,7 @@
cs (volatile! nil)]
(try
(l/info :hint "start exportation" :export-id id)
(with-open [^AutoCloseable output (io/output-stream output)]
(dm/with-open [output (io/output-stream output)]
(binding [*position* (atom 0)]
(write-export! (assoc cfg ::output output))))
@@ -858,7 +895,7 @@
(defn export-to-tmpfile!
[cfg]
(let [path (tmp/tempfile :prefix "penpot.export.")]
(with-open [^AutoCloseable output (io/output-stream path)]
(dm/with-open [output (io/output-stream path)]
(export! cfg output)
path)))
@@ -870,7 +907,7 @@
(l/info :hint "import: started" :import-id id)
(try
(binding [*position* (atom 0)]
(with-open [^AutoCloseable input (io/input-stream input)]
(dm/with-open [input (io/input-stream input)]
(read-import! (assoc cfg ::input input))))
(catch Throwable cause
@@ -910,7 +947,9 @@
(export! output-stream))))]
(fn [_]
(yrs/response 200 body {"content-type" "application/octet-stream"}))))
{::yrs/status 200
::yrs/body body
::yrs/headers {"content-type" "application/octet-stream"}})))
(s/def ::file ::media/upload)
(s/def ::import-binfile
@@ -928,5 +967,10 @@
::input (:path file)
::project-id project-id
::ignore-index-errors? true))]
(db/update! conn :project
{:modified-at (dt/now)}
{:id project-id})
(rph/with-meta ids
{::audit/props {:file nil :file-ids ids}}))))

View File

@@ -19,8 +19,8 @@
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.quotes :as quotes]
[app.rpc.retry :as rtry]
[app.util.pointer-map :as pmap]
[app.util.retry :as rtry]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -101,7 +101,7 @@
(sv/defmethod ::get-comment-threads
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comment-threads conn profile-id file-id)))
@@ -144,7 +144,7 @@
(sv/defmethod ::get-unread-comment-threads
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(get-unread-comment-threads conn profile-id team-id)))
@@ -191,7 +191,7 @@
(sv/defmethod ::get-comment-thread
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")]
@@ -211,7 +211,7 @@
(sv/defmethod ::get-comments
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comments conn thread-id))))
@@ -263,7 +263,7 @@
{::doc/added "1.15"
::doc/changes ["1.15" "Imported from queries and renamed."]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-file-comments-users conn file-id profile-id)))
@@ -309,7 +309,8 @@
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 3
::rtry/label "create-comment-thread"}
::rtry/label "create-comment-thread"
::db/conn conn}
(create-comment-thread conn
{:created-at request-at
:profile-id profile-id
@@ -467,8 +468,8 @@
{::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [thread-id] :as comment} (get-comment conn id ::db/for-update? true)
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)]
(let [{:keys [thread-id owner-id] :as comment} (get-comment conn id ::db/for-update? true)
{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)

View File

@@ -13,6 +13,7 @@
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -48,7 +49,7 @@
:fullname fullname
:is-active true
:deleted-at (dt/in-future cf/deletion-delay)
:password password
:password (profile/derive-password cfg password)
:props {}}]
(db/with-atomic [conn pool]

View File

File diff suppressed because it is too large Load Diff

View File

@@ -34,22 +34,25 @@
(db/insert! conn :file-profile-rel))))
(defn create-file
[conn {:keys [id name project-id is-shared data revn
[conn {:keys [id name project-id is-shared revn
modified-at deleted-at create-page
ignore-sync-until features]
:or {is-shared false revn 0 create-page true}
:as params}]
(let [id (or id (:id data) (uuid/next))
features (-> (into files/default-features features)
(files/check-features-compatibility!))
data (or data
(binding [ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)]
(if create-page
(ctf/make-file-data id)
(ctf/make-file-data id nil))))
(let [id (or id (uuid/next))
features (->> features
(into (files/get-default-features))
(files/check-features-compatibility!))
pointers (atom {})
data (binding [pmap/*tracked* pointers
ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)]
(if create-page
(ctf/make-file-data id)
(ctf/make-file-data id nil)))
features (db/create-array conn "text" features)
file (db/insert! conn :file
@@ -65,6 +68,9 @@
:modified-at modified-at
:deleted-at deleted-at}))]
(binding [pmap/*tracked* pointers]
(files/persist-pointers! conn id))
(->> (assoc params :file-id id :role :owner)
(create-file-role! conn))
@@ -84,6 +90,7 @@
(sv/defmethod ::create-file
{::doc/added "1.17"
::doc/module :files
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(db/with-atomic [conn pool]

View File

@@ -36,7 +36,8 @@
Share links are resources that allows external users access to specific
pages of a file with specific permissions (who-comment and who-inspect)."
{::doc/added "1.18"}
{::doc/added "1.18"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
@@ -62,7 +63,8 @@
:req-un [::us/id]))
(sv/defmethod ::delete-share-link
{::doc/added "1.18"}
{::doc/added "1.18"
::doc/module ::files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [slink (db/get-by-id conn :share-link id)]

View File

@@ -0,0 +1,135 @@
;; 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.rpc.commands.files-snapshot
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.main :as-alias main]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]))
(defn check-authorized!
[{:keys [::db/pool]} profile-id]
(when-not (or (= "devenv" (cf/get :host))
(let [profile (ex/ignoring (profile/get-profile pool profile-id))
admins (or (cf/get :admins) #{})]
(contains? admins (:email profile))))
(ex/raise :type :authentication
:code :authentication-required
:hint "only admins allowed")))
(defn get-file-snapshots
[{:keys [::db/conn]} {:keys [file-id limit start-at]
:or {limit Long/MAX_VALUE}}]
(let [query (str "select id, label, revn, created_at "
" from file_change "
" where file_id = ? "
" and created_at < ? "
" and data is not null "
" order by created_at desc "
" limit ?")
start-at (or start-at (dt/now))
limit (min limit 20)]
(->> (db/exec! conn [query file-id start-at limit])
(mapv (fn [row]
(update row :created-at dt/format-instant :rfc1123))))))
(def ^:private schema:get-file-snapshots
[:map [:file-id ::sm/uuid]])
(sv/defmethod ::get-file-snapshots
{::doc/added "1.20"
::doc/skip true
::sm/params schema:get-file-snapshots}
[cfg {:keys [::rpc/profile-id] :as params}]
(check-authorized! cfg profile-id)
(db/run! cfg #(get-file-snapshots % params)))
(defn restore-file-snapshot!
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id id]}]
(let [storage (media/configure-assets-storage storage conn)
params {:id id :file-id file-id}
options {:columns [:id :data :revn]}
snapshot (db/get* conn :file-change params options)]
(when (and (some? snapshot)
(some? (:data snapshot)))
(l/debug :hint "snapshot found"
:snapshot-id (:id snapshot)
:file-id file-id)
(db/update! conn :file
{:data (:data snapshot)}
{:id file-id})
;; clean object thumbnails
(let [sql (str "delete from file_object_thumbnail "
" where file_id=? returning media_id")
res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)]
(sto/del-object! storage media-id)))
;; clean object thumbnails
(let [sql (str "delete from file_thumbnail "
" where file_id=? returning media_id")
res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)]
(sto/del-object! storage media-id)))
{:id (:id snapshot)})))
(def ^:private schema:restore-file-snapshot
[:map
[:file-id ::sm/uuid]
[:id ::sm/uuid]])
(sv/defmethod ::restore-file-snapshot
{::doc/added "1.20"
::doc/skip true
::sm/params schema:restore-file-snapshot}
[cfg {:keys [::rpc/profile-id] :as params}]
(check-authorized! cfg profile-id)
(db/tx-run! cfg #(restore-file-snapshot! % params)))
(defn take-file-snapshot!
[{:keys [::db/conn]} {:keys [file-id label]}]
(when-let [file (db/get* conn :file {:id file-id})]
(let [id (uuid/next)
label (or label (str "Snapshot at " (dt/format-instant (dt/now) :rfc1123)))]
(l/debug :hint "persisting file snapshot" :file-id file-id :label label)
(db/insert! conn :file-change
{:id id
:revn (:revn file)
:data (:data file)
:features (:features file)
:file-id (:id file)
:label label})
{:id id})))
(def ^:private schema:take-file-snapshot
[:map [:file-id ::sm/uuid]])
(sv/defmethod ::take-file-snapshot
{::doc/added "1.20"
::doc/skip true
::sm/params schema:take-file-snapshot}
[cfg {:keys [::rpc/profile-id] :as params}]
(check-authorized! cfg profile-id)
(db/tx-run! cfg #(take-file-snapshot! % params)))

View File

@@ -36,7 +36,8 @@
::create-page]))
(sv/defmethod ::create-temp-file
{::doc/added "1.17"}
{::doc/added "1.17"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(db/with-atomic [conn pool]
(projects/check-edition-permissions! conn profile-id project-id)
@@ -64,7 +65,8 @@
::files/id]))
(sv/defmethod ::update-temp-file
{::doc/added "1.17"}
{::doc/added "1.17"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(update-temp-file conn (assoc params :profile-id profile-id))
@@ -84,16 +86,16 @@
(ex/raise :type :validation
:code :cant-persist-already-persisted-file))
(loop [revs (seq revs)
data (blob/decode (:data file))]
(if-let [rev (first revs)]
(recur (rest revs)
(->> rev :changes blob/decode (cp/process-changes data)))
(db/update! conn :file
{:deleted-at nil
:revn revn
:data (blob/encode data)}
{:id id})))
(let [data
(->> revs
(mapcat #(->> % :changes blob/decode))
(cp/process-changes (blob/decode (:data file))))]
(db/update! conn :file
{:deleted-at nil
:revn revn
:data (blob/encode data)}
{:id id}))
nil))
(s/def ::persist-temp-file
@@ -101,7 +103,8 @@
:req-un [::files/id]))
(sv/defmethod ::persist-temp-file
{::doc/added "1.17"}
{::doc/added "1.17"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)

View File

@@ -0,0 +1,462 @@
;; 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.rpc.commands.files-thumbnails
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.types.shape-tree :as ctt]
[app.db :as db]
[app.db.sql :as sql]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.storage :as sto]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
;; --- FEATURES
(def long-cache-duration
(dt/duration {:days 7}))
;; --- COMMAND QUERY: get-file-object-thumbnails
(defn- get-object-thumbnails
([conn file-id]
(let [sql (str/concat
"select object_id, data, media_id "
" from file_object_thumbnail"
" where file_id=?")
res (db/exec! conn [sql file-id])]
(->> res
(d/index-by :object-id (fn [row]
(or (some-> row :media-id files/resolve-public-uri)
(:data row))))
(d/without-nils))))
([conn file-id object-ids]
(let [sql (str/concat
"select object_id, data, media_id "
" from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)")
ids (db/create-array conn "text" (seq object-ids))
res (db/exec! conn [sql file-id ids])]
(d/index-by :object-id
(fn [row]
(or (some-> row :media-id files/resolve-public-uri)
(:data row)))
res))))
(sv/defmethod ::get-file-object-thumbnails
"Retrieve a file object thumbnails."
{::doc/added "1.17"
::doc/module :files
::sm/params [:map {:title "get-file-object-thumbnails"}
[:file-id ::sm/uuid]]
::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}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(get-object-thumbnails conn file-id)))
;; --- COMMAND QUERY: get-file-thumbnail
(defn get-file-thumbnail
[conn file-id revn]
(let [sql (sql/select :file-thumbnail
(cond-> {:file-id file-id}
revn (assoc :revn revn))
{:limit 1
:order-by [[:revn :desc]]})
row (db/exec-one! conn sql)]
(when-not row
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
(when-not (:data row)
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
{:data (:data row)
:props (some-> (:props row) db/decode-transit-pgobject)
:revn (:revn row)
:file-id (:file-id row)}))
(s/def ::revn ::us/integer)
(s/def ::file-id ::us/uuid)
(s/def ::get-file-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::revn]))
(sv/defmethod ::get-file-thumbnail
{::doc/added "1.17"
::doc/module :files
::doc/deprecated "1.19"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
(dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(-> (get-file-thumbnail conn file-id revn)
(rph/with-http-cache long-cache-duration))))
;; --- COMMAND QUERY: get-file-data-for-thumbnail
;; We need to improve how we set frame for thumbnail in order to avoid
;; loading all pages into memory for find the frame set for thumbnail.
(defn get-file-data-for-thumbnail
[conn {:keys [data id] :as file}]
(letfn [;; function responsible on finding the frame marked to be
;; used as thumbnail; the returned frame always have
;; the :page-id set to the page that it belongs.
(get-thumbnail-frame [data]
;; NOTE: this is a hack for avoid perform blocking
;; operation inside the for loop, clojure lazy-seq uses
;; synchronized blocks that does not plays well with
;; virtual threads, so we need to perform the load
;; operation first. This operation forces all pointer maps
;; load into the memory.
(->> (-> data :pages-index vals)
(filter pmap/pointer-map?)
(run! pmap/load!))
;; Then proceed to find the frame set for thumbnail
(d/seek #(or (:use-for-thumbnail %)
(:use-for-thumbnail? %)) ; NOTE: backward comp (remove on v1.21)
(for [page (-> data :pages-index vals)
frame (-> page :objects ctt/get-frames)]
(assoc frame :page-id (:id page)))))
;; function responsible to filter objects data structure of
;; all unneeded shapes if a concrete frame is provided. If no
;; frame, the objects is returned untouched.
(filter-objects [objects frame-id]
(d/index-by :id (cph/get-children-with-self objects frame-id)))
;; function responsible of assoc available thumbnails
;; to frames and remove all children shapes from objects if
;; thumbnails is available
(assoc-thumbnails [objects page-id thumbnails]
(loop [objects objects
frames (filter cph/frame-shape? (vals objects))]
(if-let [frame (-> frames first)]
(let [frame-id (:id frame)
object-id (str page-id frame-id)
frame (if-let [thumb (get thumbnails object-id)]
(assoc frame :thumbnail thumb :shapes [])
(dissoc frame :thumbnail))
children-ids
(cph/get-children-ids objects frame-id)
bounds
(when (:show-content frame)
(gsh/shapes->rect (cons frame (map (d/getf objects) children-ids))))
frame
(cond-> frame
(some? bounds)
(assoc :children-bounds bounds))]
(if (:thumbnail frame)
(recur (-> objects
(assoc frame-id frame)
(d/without-keys children-ids))
(rest frames))
(recur (assoc objects frame-id frame)
(rest frames))))
objects)))]
(binding [pmap/*load-fn* (partial files/load-pointer conn id)]
(let [frame (get-thumbnail-frame data)
frame-id (:id frame)
page-id (or (:page-id frame)
(-> data :pages first))
page (dm/get-in data [:pages-index page-id])
page (cond-> page (pmap/pointer-map? page) deref)
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
obj-ids (map #(str page-id %) frame-ids)
thumbs (get-object-thumbnails conn id obj-ids)]
(cond-> page
;; If we have frame, we need to specify it on the page level
;; and remove the all other unrelated objects.
(some? frame-id)
(-> (assoc :thumbnail-frame-id frame-id)
(update :objects filter-objects frame-id))
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecessary data.
:always
(update :objects assoc-thumbnails page-id thumbs))))))
(def ^:private schema:get-file-data-for-thumbnail
[:map {:title "get-file-data-for-thumbnail"}
[:file-id ::sm/uuid]
[:features {:optional true} files/schema:features]])
(def ^:private schema:partial-file
[:map {:title "PartialFile"}
[:id ::sm/uuid]
[:revn {:min 0} :int]
[:page :any]])
(sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used
mainly for render thumbnails on dashboard."
{::doc/added "1.17"
::doc/module :files
::sm/params schema:get-file-data-for-thumbnail
::sm/result schema:partial-file}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
(dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
;; NOTE: we force here the "storage/pointer-map" feature, because
;; it used internally only and is independent if user supports it
;; or not.
(let [feat (into #{"storage/pointer-map"} features)
file (files/get-file conn file-id feat)]
{:file-id file-id
:revn (:revn file)
:page (get-file-data-for-thumbnail conn file)})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
(def sql:upsert-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, data)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set data = ?;")
(defn upsert-file-object-thumbnail!
[conn {:keys [file-id object-id data]}]
(if data
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id})))
(s/def ::data (s/nilable ::us/string))
(s/def ::object-id ::us/string)
(s/def ::upsert-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id]
:opt-un [::data]))
(sv/defmethod ::upsert-file-object-thumbnail
{::doc/added "1.17"
::doc/module :files
::doc/deprecated "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(upsert-file-object-thumbnail! conn params)
nil)))
;; --- MUTATION COMMAND: create-file-object-thumbnail
(def ^:private sql:create-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, media_id)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set media_id = ?;")
(defn- create-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id media]
(let [path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? false
:content-type mtype
:bucket "file-object-thumbnail"})]
(db/exec-one! conn [sql:create-object-thumbnail file-id object-id
(:id media) (:id media)])))
(def schema:create-file-object-thumbnail
[:map {:title "create-file-object-thumbnail"}
[:file-id ::sm/uuid]
[:object-id :string]
[:media ::media/upload]])
(sv/defmethod ::create-file-object-thumbnail
{:doc/added "1.19"
::doc/module :files
::audit/skip true
::sm/params schema:create-file-object-thumbnail}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(media/validate-media-type! media)
(media/validate-media-size! media)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-object-thumbnail! file-id object-id media))
nil)))
;; --- MUTATION COMMAND: delete-file-object-thumbnail
(defn- delete-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id]
(when-let [{:keys [media-id]} (db/get* conn :file-object-thumbnail
{:file-id file-id
:object-id object-id}
{::db/for-update? true})]
(when media-id
(sto/del-object! storage media-id))
(db/delete! conn :file-object-thumbnail
{:file-id file-id
:object-id object-id})
nil))
(s/def ::delete-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id]))
(sv/defmethod ::delete-file-object-thumbnail
{:doc/added "1.19"
::doc/module :files
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(delete-file-object-thumbnail! file-id object-id))
nil)))
;; --- MUTATION COMMAND: upsert-file-thumbnail
(def ^:private sql:upsert-file-thumbnail
"insert into file_thumbnail (file_id, revn, data, props)
values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
update set data = ?, props=?, updated_at=now();")
(defn- upsert-file-thumbnail!
[conn {:keys [file-id revn data props]}]
(let [props (db/tjson (or props {}))]
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data props data props])))
(s/def ::revn ::us/integer)
(s/def ::props map?)
(s/def ::upsert-file-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::revn ::props ::data]))
(sv/defmethod ::upsert-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.17"
::doc/module :files
::doc/deprecated "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(upsert-file-thumbnail! conn params)
nil)))
;; --- MUTATION COMMAND: create-file-thumbnail
(def ^:private sql:create-file-thumbnail
"insert into file_thumbnail (file_id, revn, media_id, props)
values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
update set media_id=?, props=?, updated_at=now();")
(defn- create-file-thumbnail!
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
(media/validate-media-type! media)
(media/validate-media-size! media)
(let [props (db/tjson (or props {}))
path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? false
:content-type mtype
:bucket "file-thumbnail"})]
(db/exec-one! conn [sql:create-file-thumbnail file-id revn
(:id media) props
(:id media) props])
media))
(sv/defmethod ::create-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.19"
::doc/module :files
::audit/skip true
::sm/params [:map {:title "create-file-thumbnail"}
[:file-id ::sm/uuid]
[:revn :int]
[:media ::media/upload]]
}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [media (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn)
(create-file-thumbnail! params))]
{:uri (files/resolve-public-uri (:id media))}))))

View File

@@ -8,10 +8,12 @@
(:require
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.files.migrations :as pmg]
[app.common.logging :as l]
[app.common.pages :as cp]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.pages.changes :as cpc]
[app.common.schema :as sm]
[app.common.schema.generators :as smg]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -21,7 +23,7 @@
[app.metrics :as mtx]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.climit :as climit]
[app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
@@ -29,36 +31,40 @@
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
[app.util.time :as dt]))
;; --- SPECS
;; --- SCHEMA
(s/def ::changes
(s/coll-of map? :kind vector?))
(sm/def! ::changes
[:vector ::cpc/change])
(s/def ::hint-origin ::us/keyword)
(s/def ::hint-events
(s/every ::us/keyword :kind vector?))
(sm/def! ::change-with-metadata
[:map {:title "ChangeWithMetadata"}
[:changes ::changes]
[:hint-origin {:optional true} :keyword]
[:hint-events {:optional true} [:vector :string]]])
(s/def ::change-with-metadata
(s/keys :req-un [::changes]
:opt-un [::hint-origin
::hint-events]))
(sm/def! ::update-file-params
[:map {:title "UpdateFileParams"}
[:id ::sm/uuid]
[:session-id ::sm/uuid]
[:revn {:min 0} :int]
[:features {:optional true
:gen/max 3
:gen/gen (smg/subseq files/supported-features)}
::sm/set-of-strings]
[:changes {:optional true} ::changes]
[:changes-with-metadata {:optional true}
[:vector ::change-with-metadata]]])
(s/def ::changes-with-metadata
(s/every ::change-with-metadata :kind vector?))
(s/def ::session-id ::us/uuid)
(s/def ::revn ::us/integer)
(s/def ::update-file
(s/and
(s/keys :req [::rpc/profile-id]
:req-un [::files/id ::session-id ::revn]
:opt-un [::changes ::changes-with-metadata ::features])
(fn [o]
(or (contains? o :changes)
(contains? o :changes-with-metadata)))))
(sm/def! ::update-file-result
[:vector {:title "UpdateFileResults"}
[:map {:title "UpdateFileResult"}
[:changes ::changes]
[:file-id ::sm/uuid]
[:id ::sm/uuid]
[:revn {:min 0} :int]
[:session-id ::sm/uuid]]])
;; --- HELPERS
@@ -78,8 +84,7 @@
(defn- library-change?
[{:keys [type] :as change}]
(or (contains? library-change-types type)
(and (contains? file-change-types type)
(some? (:component-id change)))))
(contains? file-change-types type)))
(def ^:private sql:get-file
"SELECT f.*, p.team_id
@@ -101,7 +106,7 @@
(defn- wrap-with-pointer-map-context
[f]
(fn [{:keys [conn] :as cfg} {:keys [id] :as file}]
(fn [{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(binding [pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn id)
ffeat/*wrap-with-pointer-map-fn* pmap/wrap]
@@ -119,6 +124,7 @@
(declare send-notifications!)
(declare update-file)
(declare update-file*)
(declare update-file-data)
(declare take-snapshot?)
;; If features are specified from params and the final feature
@@ -126,18 +132,23 @@
;; database.
(sv/defmethod ::update-file
{::climit/queue :update-file
{::climit/id :update-file-by-id
::climit/key-fn :id
::webhooks/event? true
::webhooks/batch-timeout (dt/duration "2m")
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
::sm/params ::update-file-params
::sm/result ::update-file-result
::doc/module :files
::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id)
(let [cfg (assoc cfg :conn conn)
(let [cfg (assoc cfg ::db/conn conn)
params (assoc params :profile-id profile-id)
tpoint (dt/tpoint)]
(-> (update-file cfg params)
@@ -145,17 +156,18 @@
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
(defn update-file
[{:keys [conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
[{:keys [::db/conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
(let [file (get-file conn id)
features (->> (concat (:features file)
(:features params))
(into files/default-features)
(into (files/get-default-features))
(files/check-features-compatibility!))]
(files/check-edition-permissions! conn profile-id (:id file))
(binding [ffeat/*current* features
ffeat/*previous* (:features file)]
(let [update-fn (cond-> update-file*
(contains? features "storage/pointer-map")
(wrap-with-pointer-map-context)
@@ -198,23 +210,13 @@
:team-id (:team-id file)}))))))
(defn- update-file*
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
(let [file (-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
:always
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
[{:keys [::db/conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
(let [;; Process the file data in the CLIMIT context; scheduling it
;; to be executed on a separated executor for avoid to do the
;; CPU intensive operation on vthread.
file (-> (climit/configure cfg :update-file)
(climit/submit! (partial update-file-data file changes)))]
(and (contains? ffeat/*current* "components/v2")
(not (contains? ffeat/*previous* "components/v2")))
(ctf/migrate-to-components-v2)
:always
(-> (cp/process-changes changes)
(blob/encode))))))]
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
@@ -246,6 +248,25 @@
;; Retrieve and return lagged data
(get-lagged-changes conn params))))
(defn- update-file-data
[file changes]
(-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
:always
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
(and (contains? ffeat/*current* "components/v2")
(not (contains? ffeat/*previous* "components/v2")))
(ctf/migrate-to-components-v2)
:always
(-> (cp/process-changes changes)
(blob/encode)))))))
(defn- take-snapshot?
"Defines the rule when file `data` snapshot should be saved."
[{:keys [revn modified-at] :as file}]
@@ -273,11 +294,10 @@
(vec)))
(defn- send-notifications!
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [file changes session-id] :as params}]
(let [lchanges (filter library-change? changes)
msgbus (::mbus/msgbus cfg)]
;; Asynchronously publish message to the msgbus
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-change
@@ -290,7 +310,6 @@
(when (and (:is-shared file) (seq lchanges))
(let [team-id (or (:team-id file)
(files/get-team-id conn (:project-id file)))]
;; Asynchronously publish message to the msgbus
(mbus/pub! msgbus
:topic team-id
:message {:type :library-change

View File

@@ -6,7 +6,7 @@
(ns app.rpc.commands.fonts
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
@@ -25,10 +25,7 @@
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]))
[clojure.spec.alpha :as s]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
@@ -39,6 +36,7 @@
(s/def ::id ::us/uuid)
(s/def ::name ::us/not-empty-string)
(s/def ::project-id ::us/uuid)
(s/def ::share-id ::us/uuid)
(s/def ::style valid-style)
(s/def ::team-id ::us/uuid)
(s/def ::weight valid-weight)
@@ -50,7 +48,8 @@
(s/keys :req [::rpc/profile-id]
:opt-un [::team-id
::file-id
::project-id])
::project-id
::share-id])
(fn [o]
(or (contains? o :team-id)
(contains? o :file-id)
@@ -58,8 +57,8 @@
(sv/defmethod ::get-font-variants
{::doc/added "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id share-id] :as params}]
(dm/with-open [conn (db/open pool)]
(cond
(uuid? team-id)
(do
@@ -77,11 +76,12 @@
(uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})]
(files/check-read-permissions! conn profile-id file-id)
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
perms (files/get-permissions conn profile-id file-id share-id)]
(files/check-read-permissions! perms)
(db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})))))
{:team-id (:team-id project)
:deleted-at nil})))))
(declare create-font-variant)
@@ -107,50 +107,45 @@
(create-font-variant cfg (assoc params :profile-id profile-id))))
(defn create-font-variant
[{:keys [::sto/storage ::db/pool ::wrk/executor ::rpc/climit]} {:keys [data] :as params}]
(letfn [(generate-fonts [data]
(climit/with-dispatch (:process-font climit)
(media/run {:cmd :generate-fonts :input data})))
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [data] :as params}]
(letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf"))
(not (contains? data "font/ttf"))
(not (contains? data "font/woff"))
(not (contains? data "font/woff2")))
(ex/raise :type :validation
:code :invalid-font-upload
:hint "invalid font upload, unable to generate missing font assets"))
data))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))
(validate-data [data]
(when (and (not (contains? data "font/otf"))
(not (contains? data "font/ttf"))
(not (contains? data "font/woff"))
(not (contains? data "font/woff2")))
(ex/raise :type :validation
:code :invalid-font-upload))
data)
(persist-font-object [data mtype]
(prepare-font [data mtype]
(when-let [resource (get data mtype)]
(p/let [hash (calculate-hash resource)
content (-> (sto/content resource)
(sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content
::sto/touched-at (dt/now)
::sto/deduplicate? true
:content-type mtype
:bucket "team-font-variant"}))))
(let [hash (sto/calculate-hash resource)
content (-> (sto/content resource)
(sto/wrap-with-hash hash))]
{::sto/content content
::sto/touched-at (dt/now)
::sto/deduplicate? true
:content-type mtype
:bucket "team-font-variant"})))
(persist-fonts [data]
(p/let [otf (persist-font-object data "font/otf")
ttf (persist-font-object data "font/ttf")
woff1 (persist-font-object data "font/woff")
woff2 (persist-font-object data "font/woff2")]
(persist-fonts-files! [data]
(let [otf-params (prepare-font data "font/otf")
ttf-params (prepare-font data "font/ttf")
wf1-params (prepare-font data "font/woff")
wf2-params (prepare-font data "font/woff2")]
(cond-> {}
(some? otf-params)
(assoc :otf (sto/put-object! storage otf-params))
(some? ttf-params)
(assoc :ttf (sto/put-object! storage ttf-params))
(some? wf1-params)
(assoc :woff1 (sto/put-object! storage wf1-params))
(some? wf2-params)
(assoc :woff2 (sto/put-object! storage wf2-params)))))
(d/without-nils
{:otf otf
:ttf ttf
:woff1 woff1
:woff2 woff2})))
(insert-into-db [{:keys [woff1 woff2 otf ttf]}]
(insert-font-variant! [{:keys [woff1 woff2 otf ttf]}]
(db/insert! pool :team-font-variant
{:id (uuid/next)
:team-id (:team-id params)
@@ -164,13 +159,11 @@
:ttf-file-id (:id ttf)}))
]
(->> (generate-fonts data)
(p/fmap validate-data)
(p/mcat executor persist-fonts)
(p/fmap executor insert-into-db)
(p/fmap (fn [result]
(let [params (update params :data (comp vec keys))]
(rph/with-meta result {::audit/replace-props params})))))))
(let [data (-> (climit/configure cfg :process-font)
(climit/submit! (partial generate-missing! data)))
assets (persist-fonts-files! data)
result (insert-font-variant! assets)]
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
;; --- UPDATE FONT FAMILY

View File

@@ -38,7 +38,8 @@
"Performs the authentication using LDAP backend. Only works if LDAP
is properly configured and enabled with `login-with-ldap` flag."
{::rpc/auth false
::doc/added "1.15"}
::doc/added "1.15"
::doc/module :auth}
[{:keys [::main/props ::ldap/provider] :as cfg} params]
(when-not provider
(ex/raise :type :restriction

View File

@@ -9,7 +9,8 @@
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pages.migrations :as pmg]
[app.common.files.migrations :as pmg]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
@@ -20,12 +21,15 @@
[app.rpc.commands.projects :as proj]
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
[app.rpc.doc :as-alias doc]
[app.setup :as-alias setup]
[app.setup.templates :as tmpl]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]))
[clojure.walk :as walk]
[promesa.exec :as px]))
;; --- COMMAND: Duplicate File
@@ -233,7 +237,7 @@
(let [project (-> (db/get-by-id conn :project project-id)
(assoc :is-pinned false))
files (db/query conn :file
{:project-id (:id project)
:deleted-at nil}
@@ -319,6 +323,18 @@
;; delete possible broken relations on moved files
(db/exec-one! conn [sql:delete-broken-relations pids])
;; Update the modification date of the all affected projects
;; ensuring that the destination project is the most recent one.
(doseq [project-id (into (list project-id) source)]
;; NOTE: as this is executed on virtual thread, sleeping does
;; not causes major issues, and allows an easy way to set a
;; trully different modification date to each file.
(px/sleep 10)
(db/update! conn :project
{:modified-at (dt/now)}
{:id project-id}))
nil))
(s/def ::ids (s/every ::us/uuid :kind set?))
@@ -361,7 +377,6 @@
nil))
(s/def ::move-project
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::project-id]))
@@ -376,46 +391,54 @@
;; --- COMMAND: Clone Template
(declare clone-template)
(s/def ::template-id ::us/not-empty-string)
(s/def ::clone-template
(s/keys :req [::rpc/profile-id]
:req-un [::project-id ::template-id]))
(sv/defmethod ::clone-template
"Clone into the specified project the template by its id."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(clone-template (assoc params :profile-id profile-id)))))
(defn- clone-template
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
(let [template (d/seek #(= (:id %) template-id) templates)
(defn- clone-template!
[{:keys [::db/conn] :as cfg} {:keys [profile-id template-id project-id]}]
(let [template (tmpl/get-template-stream cfg template-id)
project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
(teams/check-edition-permissions! conn profile-id (:team-id project))
(when-not template
(ex/raise :type :not-found
:code :template-not-found
:hint "template not found"))
(teams/check-edition-permissions! conn profile-id (:team-id project))
(-> cfg
(assoc ::binfile/input (:path template))
;; FIXME: maybe reuse the conn instead of creating more
;; connections in the import process?
(dissoc ::db/conn)
(assoc ::binfile/input template)
(assoc ::binfile/project-id (:id project))
(assoc ::binfile/ignore-index-errors? true)
(assoc ::binfile/migrate? true)
(binfile/import!))))
(def schema:clone-template
[:map {:title "clone-template"}
[:project-id ::sm/uuid]
[:template-id ::sm/word-string]])
;; --- COMMAND: Retrieve list of builtin templates
(sv/defmethod ::clone-template
"Clone into the specified project the template by its id."
{::doc/added "1.16"
::webhooks/event? true
::sm/params schema:clone-template}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(-> (assoc cfg ::db/conn conn)
(clone-template! (assoc params :profile-id profile-id)))))
;; --- COMMAND: Get list of builtin templates
(s/def ::retrieve-list-of-builtin-templates any?)
(sv/defmethod ::retrieve-list-of-builtin-templates
{::doc/added "1.10"
::doc/deprecated "1.19"}
[cfg _params]
(mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg)))
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))
(sv/defmethod ::get-builtin-templates
{::doc/added "1.19"}
[cfg _params]
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))

View File

@@ -23,13 +23,9 @@
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.io :as io]
[promesa.core :as p]
[promesa.exec :as px]))
[datoteka.io :as io]))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
@@ -45,15 +41,6 @@
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(defn validate-content-size!
[content]
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
(ex/raise :type :restriction
:code :media-max-file-size-reached
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
(:size content)
default-max-file-size))))
;; --- Create File Media object (upload)
(declare create-file-media-object)
@@ -72,16 +59,15 @@
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(media/validate-media-type! content)
(validate-content-size! content)
(->> (create-file-media-object cfg params)
(p/fmap (fn [object]
(with-meta object
{::audit/replace-props
{:name (:name params)
:file-id file-id
:is-local (:is-local params)
:size (:size content)
:mtype (:mtype content)}}))))))
(media/validate-media-size! content)
(let [object (create-file-media-object cfg params)
props {:name (:name params)
:file-id file-id
:is-local (:is-local params)
:size (:size content)
:mtype (:mtype content)}]
(with-meta object
{::audit/replace-props props}))))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
@@ -118,71 +104,62 @@
;; witch holds the reference to storage object (it some kind of
;; inverse, soft referential integrity).
(defn- process-main-image
[info]
(let [hash (sto/calculate-hash (:path info))
data (-> (sto/content (:path info))
(sto/wrap-with-hash hash))]
{::sto/content data
::sto/deduplicate? true
::sto/touched-at (:ts info)
:content-type (:mtype info)
:bucket "file-media-object"}))
(defn- process-thumb-image
[info]
(let [thumb (-> thumbnail-options
(assoc :cmd :generic-thumbnail)
(assoc :input info)
(media/run))
hash (sto/calculate-hash (:data thumb))
data (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
{::sto/content data
::sto/deduplicate? true
::sto/touched-at (:ts info)
:content-type (:mtype thumb)
:bucket "file-media-object"}))
(defn- process-image
[content]
(let [info (media/run {:cmd :info :input content})]
(cond-> info
(and (not (svg-image? info))
(big-enough-for-thumbnail? info))
(assoc ::thumb (process-thumb-image info))
:always
(assoc ::image (process-main-image info)))))
(defn create-file-media-object
[{:keys [::sto/storage ::db/pool climit ::wrk/executor]}
[{:keys [::sto/storage ::db/pool] :as cfg}
{:keys [id file-id is-local name content]}]
(letfn [;; Function responsible to retrieve the file information, as
;; it is synchronous operation it should be wrapped into
;; with-dispatch macro.
(get-info [content]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :info :input content})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))
(let [result (-> (climit/configure cfg :process-image)
(climit/submit! (partial process-image content)))
;; Function responsible of generating thumnail. As it is synchronous
;; opetation, it should be wrapped into with-dispatch macro
(generate-thumbnail [info]
(climit/with-dispatch (:process-image climit)
(media/run (assoc thumbnail-options
:cmd :generic-thumbnail
:input info))))
image (sto/put-object! storage (::image result))
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]
(create-thumbnail [info]
(when (and (not (svg-image? info))
(big-enough-for-thumbnail? info))
(p/let [thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype thumb)
:bucket "file-media-object"}))))
(create-image [info]
(p/let [data (:path info)
hash (calculate-hash data)
content (-> (sto/content data)
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype info)
:bucket "file-media-object"})))
(insert-into-database [info image thumb]
(px/with-dispatch executor
(db/exec-one! pool [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
(:id thumb)
(:width info)
(:height info)
(:mtype info)])))]
(p/let [info (get-info content)
thumb (create-thumbnail info)
image (create-image info)]
(insert-into-database info image thumb))))
(db/exec-one! pool [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
(:id thumb)
(:width result)
(:height result)
(:mtype result)])))
;; --- Create File Media Object (from URL)
@@ -194,15 +171,16 @@
:opt-un [::id ::name]))
(sv/defmethod ::create-file-media-object-from-url
{::doc/added "1.17"}
{::doc/added "1.17"
::doc/deprecated "1.19"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(create-file-media-object-from-url cfg params)))
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(letfn [(parse-and-validate-size [headers]
(defn- download-image
[{:keys [::http/client]} uri]
(letfn [(parse-and-validate [{:keys [headers] :as response}]
(let [size (some-> (get headers "content-length") d/parse-integer)
mtype (get headers "content-type")
format (cm/mtype->format mtype)
@@ -225,32 +203,34 @@
:code :media-type-not-allowed
:hint "seems like the url points to an invalid media object"))
{:size size
:mtype mtype
:format format}))
{:size size :mtype mtype :format format}))]
(download-media [uri]
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
(p/then process-response)))
(let [{:keys [body] :as response} (http/req! client
{:method :get :uri uri}
{:response-type :input-stream :sync? true})
{:keys [size mtype]} (parse-and-validate response)
(process-response [{:keys [body headers] :as response}]
(let [{:keys [size mtype]} (parse-and-validate-size headers)
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write-to-file! body path :size size)]
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write-to-file! body path :size size)]
(when (not= written size)
(ex/raise :type :internal
:code :mismatch-write-size
:hint "unexpected state: unable to write to file"))
(when (not= written size)
(ex/raise :type :internal
:code :mismatch-write-size
:hint "unexpected state: unable to write to file"))
{:filename "tempfile"
:size size
:path path
:mtype mtype}))]
{:filename "tempfile"
:size size
:path path
:mtype mtype})))
(p/let [content (download-media url)]
(->> (merge params {:content content :name (or name (:filename content))})
(create-file-media-object cfg)))))
(defn- create-file-media-object-from-url
[cfg {:keys [url name] :as params}]
(let [content (download-image cfg url)
params (-> params
(assoc :content content)
(assoc :name (or name (:filename content))))]
(create-file-media-object cfg params)))
;; --- Clone File Media object (Upload and create from url)

View File

@@ -8,8 +8,9 @@
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
@@ -26,26 +27,41 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]))
[cuerdas.core :as str]))
(declare check-profile-existence!)
(declare decode-row)
(declare derive-password)
(declare filter-props)
(declare get-profile)
(declare strip-private-attrs)
(declare filter-props)
(declare check-profile-existence!)
(declare verify-password)
(def schema:profile
[:map {:title "Profile"}
[:id ::sm/uuid]
[:fullname [::sm/word-string {:max 250}]]
[:email ::sm/email]
[:is-active {:optional true} :boolean]
[:is-blocked {:optional true} :boolean]
[:is-demo {:optional true} :boolean]
[:is-muted {:optional true} :boolean]
[:created-at {:optional true} ::sm/inst]
[:modified-at {:optional true} ::sm/inst]
[:default-project-id {:optional true} ::sm/uuid]
[:default-team-id {:optional true} ::sm/uuid]
[:props {:optional true}
[:map-of {:title "ProfileProps"} :keyword :any]]])
(def profile?
(sm/pred-fn schema:profile))
;; --- QUERY: Get profile (own)
(s/def ::get-profile
(s/keys :opt [::rpc/profile-id]))
(sv/defmethod ::get-profile
{::rpc/auth false
::doc/added "1.18"}
::doc/added "1.18"
::sm/result schema:profile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id]}]
;; We need to return the anonymous profile object in two cases, when
;; no profile-id is in session, and when db call raises not found. In all other
@@ -63,22 +79,24 @@
(-> (db/get-by-id conn :profile id attrs)
(decode-row)))
;; --- MUTATION: Update Profile (own)
(s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/string)
(s/def ::theme ::us/string)
(s/def ::update-profile
(s/keys :req [::rpc/profile-id]
:req-un [::fullname]
:opt-un [::lang ::theme]))
(def schema:update-profile
[:map {:title "update-profile"}
[:fullname [::sm/word-string {:max 250}]]
[:lang {:optional true} [:string {:max 5}]]
[:theme {:optional true} [:string {:max 250}]]])
(sv/defmethod ::update-profile
{::doc/added "1.0"}
{::doc/added "1.0"
::sm/params schema:update-profile
::sm/result schema:profile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
(dm/assert!
"expected valid profile data"
(profile? 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
@@ -112,18 +130,21 @@
(declare update-profile-password!)
(declare invalidate-profile-session!)
(s/def ::password ::us/not-empty-string)
(s/def ::old-password (s/nilable ::us/string))
(s/def ::update-profile-password
(s/keys :req [::rpc/profile-id]
:req-un [::password ::old-password]))
(def schema:update-profile-password
[:map {:title "update-profile-password"}
[:password [::sm/word-string {:max 500}]]
;; Social registered users don't have old-password
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]])
(sv/defmethod ::update-profile-password
{::climit/queue :auth}
{:doc/added "1.0"
::sm/params schema:update-profile-password
::sm/result :nil}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
(db/with-atomic [conn pool]
(let [profile (validate-password! conn (assoc params :profile-id profile-id))
(let [cfg (assoc cfg ::db/conn conn)
profile (validate-password! cfg (assoc params :profile-id profile-id))
session-id (::session/id params)]
(when (= (str/lower (:email profile))
@@ -133,20 +154,20 @@
:hint "you can't use your email as password"))
(update-profile-password! conn (assoc profile :password password))
(invalidate-profile-session! conn profile-id session-id)
(invalidate-profile-session! cfg profile-id session-id)
nil)))
(defn- invalidate-profile-session!
"Removes all sessions except the current one."
[conn profile-id session-id]
[{:keys [::db/conn]} profile-id session-id]
(let [sql "delete from http_session where profile_id = ? and id != ?"]
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
(defn- validate-password!
[conn {:keys [profile-id old-password] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)]
(when (and (not= (:password profile) "!")
(not (:valid (auth/verify-password old-password (:password profile)))))
(not (:valid (verify-password cfg old-password (:password profile)))))
(ex/raise :type :validation
:code :old-password-not-match))
profile))
@@ -163,73 +184,63 @@
(declare upload-photo)
(declare update-profile-photo)
(s/def ::file ::media/upload)
(s/def ::update-profile-photo
(s/keys :req [::rpc/profile-id]
:req-un [::file]))
(def schema:update-profile-photo
[:map {:title "update-profile-photo"}
[:file ::media/upload]])
(sv/defmethod ::update-profile-photo
{:doc/added "1.1"
::sm/params schema:update-profile-photo
::sm/result :nil}
[cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(update-profile-photo cfg (assoc params :profile-id profile-id))))
;; TODO: reimplement it without p/let
(defn update-profile-photo
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}]
(letfn [(on-uploaded [photo]
(let [profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
(let [photo (upload-photo cfg params)
profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
;; Schedule deletion of old photo
(when-let [id (:photo-id profile)]
(sto/touch-object! storage id))
;; Schedule deletion of old photo
(when-let [id (:photo-id profile)]
(sto/touch-object! storage id))
;; Save new photo
(db/update! pool :profile
{:photo-id (:id photo)}
{:id profile-id})
;; Save new photo
(db/update! pool :profile
{:photo-id (:id photo)}
{:id profile-id})
(-> (rph/wrap)
(rph/with-meta {::audit/replace-props
{:file-name (:filename file)
:file-size (:size file)
:file-path (str (:path file))
:file-mtype (:mtype file)}}))))]
(->> (upload-photo cfg params)
(p/fmap executor on-uploaded))))
(-> (rph/wrap)
(rph/with-meta {::audit/replace-props
{:file-name (:filename file)
:file-size (:size file)
:file-path (str (:path file))
:file-mtype (:mtype file)}}))))
(defn- generate-thumbnail!
[file]
(let [input (media/run {:cmd :info :input file})
thumb (media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input input})
hash (sto/calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
{::sto/content content
::sto/deduplicate? true
:bucket "profile"
:content-type (:mtype thumb)}))
(defn upload-photo
[{:keys [::sto/storage ::wrk/executor climit] :as cfg} {:keys [file]}]
(letfn [(get-info [content]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :info :input content})))
(generate-thumbnail [info]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input info})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))]
(p/let [info (get-info file)
thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content
::sto/deduplicate? true
:bucket "profile"
:content-type (:mtype thumb)}))))
[{:keys [::sto/storage] :as cfg} {:keys [file]}]
(let [params (-> (climit/configure cfg :process-image)
(climit/submit! (partial generate-thumbnail! file)))]
(sto/put-object! storage params)))
;; --- MUTATION: Request Email Change
@@ -237,11 +248,13 @@
(declare ^:private request-email-change!)
(declare ^:private change-email-immediately!)
(s/def ::request-email-change
(s/keys :req [::rpc/profile-id]
:req-un [::email]))
(def schema:request-email-change
[:map {:title "request-email-change"}
[:email ::sm/email]])
(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)
@@ -302,12 +315,13 @@
;; --- MUTATION: Update Profile Props
(s/def ::props map?)
(s/def ::update-profile-props
(s/keys :req [::rpc/profile-id]
:req-un [::props]))
(def schema:update-profile-props
[:map {:title "update-profile-props"}
[:props [:map-of :keyword :any]]])
(sv/defmethod ::update-profile-props
{::doc/added "1.0"
::sm/params schema:update-profile-props}
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
(db/with-atomic [conn pool]
(let [profile (get-profile conn profile-id ::db/for-update? true)
@@ -327,15 +341,12 @@
(filter-props props))))
;; --- MUTATION: Delete Profile
(declare ^:private get-owned-teams-with-participants)
(s/def ::delete-profile
(s/keys :req [::rpc/profile-id]))
(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)
@@ -419,6 +430,17 @@
[props]
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
(defn derive-password
[cfg password]
(when password
(-> (climit/configure cfg :derive-password)
(climit/submit! (partial auth/derive-password password)))))
(defn verify-password
[cfg password password-data]
(-> (climit/configure cfg :derive-password)
(climit/submit! (partial auth/verify-password password password-data))))
(defn decode-row
[{:keys [props] :as row}]
(cond-> row

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.projects
(:require
[app.common.data.macros :as dm]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as-alias audit]
@@ -79,7 +80,7 @@
(sv/defmethod ::get-projects
{::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(get-projects conn profile-id team-id)))
@@ -114,7 +115,7 @@
(sv/defmethod ::get-all-projects
{::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(get-all-projects conn profile-id)))
(def sql:all-projects
@@ -157,7 +158,7 @@
(sv/defmethod ::get-project
{::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(let [project (db/get-by-id conn :project id)]
(check-read-permissions! conn profile-id id)
project)))

View File

@@ -64,6 +64,7 @@
:opt-un [::search-term]))
(sv/defmethod ::search-files
{::doc/added "1.17"}
{::doc/added "1.17"
::doc/module :files}
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}]
(some->> search-term (search-files pool profile-id team-id)))

View File

@@ -7,8 +7,10 @@
(ns app.rpc.commands.teams
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -27,11 +29,8 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]))
[cuerdas.core :as str]))
;; --- Helpers & Specs
@@ -84,13 +83,15 @@
(declare retrieve-teams)
(def counter (volatile! 0))
(s/def ::get-teams
(s/keys :req [::rpc/profile-id]))
(sv/defmethod ::get-teams
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(retrieve-teams conn profile-id)))
(def sql:teams
@@ -135,7 +136,7 @@
(sv/defmethod ::get-team
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(retrieve-team conn profile-id id)))
(defn retrieve-team
@@ -176,7 +177,7 @@
(sv/defmethod ::get-team-members
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(retrieve-team-members conn team-id)))
@@ -194,7 +195,7 @@
(sv/defmethod ::get-team-users
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(if team-id
(do
(check-read-permissions! conn profile-id team-id)
@@ -252,7 +253,7 @@
(sv/defmethod ::get-team-stats
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(retrieve-team-stats conn team-id)))
@@ -283,7 +284,7 @@
(sv/defmethod ::get-team-invitations
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(get-team-invitations conn team-id)))
@@ -594,10 +595,9 @@
(update-team-photo cfg (assoc params :profile-id profile-id))))
(defn update-team-photo
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}]
(p/let [team (px/with-dispatch executor
(retrieve-team pool profile-id team-id))
photo (profile/upload-photo cfg params)]
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}]
(let [team (retrieve-team pool profile-id team-id)
photo (profile/upload-photo cfg params)]
(db/with-atomic [conn pool]
(check-admin-permissions! conn profile-id team-id)
@@ -701,13 +701,13 @@
(l/info :hint "invitation token" :token itoken))
(audit/submit! cfg
{:type "action"
:name (if updated?
"update-team-invitation"
"create-team-invitation")
:profile-id (:id profile)
:props (-> (dissoc tprops :profile-id)
(d/without-nils))})
{::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))})
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
@@ -720,29 +720,22 @@
itoken))))
(s/def ::email ::us/email)
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-invitations
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::role]
:opt-un [::email ::emails]))
(def ^:private schema:create-team-invitations
[:map {:title "create-team-invitations"}
[:team-id ::sm/uuid]
[:role [::sm/one-of #{:owner :admin :editor}]]
[:emails ::sm/set-of-emails]])
(sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to
join the team."
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
{::doc/added "1.17"
::sm/params schema:create-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
;; Members emails. We don't re-send inviation to already existing members
member? (into #{}
(map :email)
(db/exec! conn [sql:team-members team-id]))
emails (cond-> (or emails #{}) (string? email) (conj email))]
team (db/get-by-id conn :team team-id)]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/invitations-per-team
@@ -765,15 +758,22 @@
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(let [cfg (assoc cfg ::db/conn conn)
invitations (->> emails
(remove member?)
(map (fn [email]
{:email (str/lower email)
:team team
:profile profile
:role role}))
(keep (partial create-invitation cfg)))]
(with-meta (vec invitations)
members (->> (db/exec! conn [sql:team-members team-id])
(into #{} (map :email)))
invitations (into #{}
(comp
;; We don't re-send inviation to already existing members
(remove (partial contains? members))
(map (fn [email]
{:email (str/lower email)
:team team
:profile profile
:role role}))
(keep (partial create-invitation cfg)))
emails)]
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}})))))
@@ -815,13 +815,13 @@
::quotes/incr (count emails)}))
(audit/submit! cfg
{:type "command"
:name "create-team-invitations"
:profile-id profile-id
:props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)}})
{::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)}})
(vary-meta team assoc ::audit/props {:invitations (count emails)}))))

View File

@@ -34,7 +34,8 @@
(sv/defmethod ::verify-token
{::rpc/auth false
::doc/added "1.15"}
::doc/added "1.15"
::doc/module :auth}
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify (::main/props cfg) {:token token})

View File

@@ -6,15 +6,16 @@
(ns app.rpc.commands.viewer
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.comments :as comments]
[app.rpc.commands.files :as files]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
[app.util.services :as sv]))
;; --- QUERY: View Only Bundle
@@ -26,9 +27,8 @@
[conn file-id profile-id features]
(let [file (files/get-file conn file-id features)
project (get-project conn (:project-id file))
libs (files/get-file-libraries conn file-id features)
libs (files/get-file-libraries conn file-id)
users (comments/get-file-comments-users conn file-id profile-id)
links (->> (db/query conn :share-link {:file-id file-id})
(mapv (fn [row]
(-> row
@@ -77,20 +77,18 @@
(update :data remove-not-allowed-pages (:pages perms))
:always
(update :data select-keys [:id :options :pages :pages-index]))))))
(update :data select-keys [:id :options :pages :pages-index :components]))))))
(s/def ::get-view-only-bundle
(s/keys :req-un [::files/file-id]
:opt-un [::files/share-id
::files/features]
:opt [::rpc/profile-id]))
(sm/def! ::get-view-only-bundle
[:map {:title "get-view-only-bundle"}
[:file-id ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]
[:features {:optional true} files/schema:features]])
(sv/defmethod ::get-view-only-bundle
{::rpc/auth false
::cond/get-object #(files/get-minimal-file %1 (:file-id %2))
::cond/key-fn files/get-file-etag
::cond/reuse-key? true
::doc/added "1.17"}
::doc/added "1.17"
::sm/params ::get-view-only-bundle}
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(get-view-only-bundle conn (assoc params :profile-id profile-id))))

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.webhooks
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uri :as u]
@@ -18,10 +19,8 @@
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]))
[cuerdas.core :as str]))
(defn decode-row
[{:keys [uri] :as row}]
@@ -48,30 +47,25 @@
(defn- validate-webhook!
[cfg whook params]
(letfn [(handle-exception [exception]
(if-let [hint (webhooks/interpret-exception exception)]
(ex/raise :type :validation
:code :webhook-validation
:hint hint)
(ex/raise :type :internal
:code :webhook-validation
:cause exception)))
(handle-response [response]
(when-let [hint (webhooks/interpret-response response)]
(ex/raise :type :validation
:code :webhook-validation
:hint hint)))]
(if (not= (:uri whook) (:uri params))
(->> (http/req! cfg {:method :head
:uri (str (:uri params))
:timeout (dt/duration "3s")})
(p/hmap (fn [response exception]
(if exception
(handle-exception exception)
(handle-response response)))))
(p/resolved nil))))
(when (not= (:uri whook) (:uri params))
(let [response (ex/try!
(http/req! cfg
{:method :head
:uri (str (:uri params))
:timeout (dt/duration "3s")}
{:sync? true}))]
(if (ex/exception? response)
(if-let [hint (webhooks/interpret-exception response)]
(ex/raise :type :validation
:code :webhook-validation
:hint hint)
(ex/raise :type :internal
:code :webhook-validation
:cause response))
(when-let [hint (webhooks/interpret-response response)]
(ex/raise :type :validation
:code :webhook-validation
:hint hint))))))
(defn- validate-quotes!
[{:keys [::db/pool]} {:keys [team-id]}]
@@ -106,22 +100,22 @@
(sv/defmethod ::create-webhook
{::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(check-edition-permissions! pool profile-id team-id)
(validate-quotes! cfg params)
(->> (validate-webhook! cfg nil params)
(p/fmap executor (fn [_] (insert-webhook! cfg params)))))
(validate-webhook! cfg nil params)
(insert-webhook! cfg params))
(s/def ::update-webhook
(s/keys :req-un [::id ::uri ::mtype ::is-active]))
(sv/defmethod ::update-webhook
{::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
(check-edition-permissions! pool profile-id (:team-id whook))
(->> (validate-webhook! cfg whook params)
(p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
(validate-webhook! cfg whook params)
(update-webhook! cfg whook params)))
(s/def ::delete-webhook
(s/keys :req [::rpc/profile-id]
@@ -149,7 +143,7 @@
(sv/defmethod ::get-webhooks
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(->> (db/exec! conn [sql:get-webhooks team-id])
(mapv decode-row))))

View File

@@ -27,8 +27,8 @@
[app.common.logging :as l]
[app.rpc.helpers :as rph]
[app.util.services :as-alias sv]
[promesa.core :as p]
[promesa.exec :as px]
[buddy.core.codecs :as bc]
[buddy.core.hash :as bh]
[yetti.response :as yrs]))
(def
@@ -36,32 +36,32 @@
:doc "Runtime flag for enable/disable conditional processing of RPC methods."}
*enabled* false)
(defn- encode
[s]
(-> s
bh/blake2b-256
bc/bytes->b64u
bc/bytes->str))
(defn- fmt-key
[s]
(when s
(str "W/\"" s "\"")))
(str "W/\"" (encode s) "\""))
(defn wrap
[{:keys [executor]} f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
[_ f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
(if (and (ifn? get-object) (ifn? key-fn))
(do
(l/debug :hint "instrumenting method" :service (::sv/name mdata))
(fn [cfg {:keys [::key] :as params}]
(if *enabled*
(->> (if (or key reuse-key?)
(->> (px/submit! executor (partial get-object cfg params))
(p/map key-fn)
(p/map fmt-key))
(p/resolved nil))
(p/mapcat (fn [key']
(if (and (some? key)
(= key key'))
(p/resolved (fn [_] (yrs/response 304)))
(->> (f cfg params)
(p/map (fn [result]
(->> (or (and reuse-key? key')
(-> result meta ::key fmt-key)
(-> result key-fn fmt-key))
(rph/with-header result "etag")))))))))
(let [key' (when (or key reuse-key?)
(some->> (get-object cfg params) (key-fn params) (fmt-key)))]
(if (and (some? key) (= key key'))
(fn [_] {::yrs/status 304})
(let [result (f cfg params)
etag (or (and reuse-key? key')
(some-> result meta ::key fmt-key)
(some-> result key-fn fmt-key))]
(rph/with-header result "etag" etag))))
(f cfg params))))
f))

View File

@@ -8,67 +8,197 @@
"API autogenerated documentation."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as smdj]
[app.common.schema.desc-native :as smdn]
[app.common.schema.openapi :as oapi]
[app.common.schema.registry :as sr]
[app.config :as cf]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.util.json :as json]
[app.util.services :as sv]
[app.util.template :as tmpl]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[malli.transform :as mt]
[pretty-spec.core :as ps]
[yetti.response :as yrs]))
(defn- get-spec-str
[k]
(with-out-str
(ps/pprint (s/form k)
{:ns-aliases {"clojure.spec.alpha" "s"
"clojure.core.specs.alpha" "score"
"clojure.core" nil}})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DOC (human readable)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- prepare-context
(defn- prepare-doc-context
[methods]
(letfn [(gen-doc [type [name f]]
(let [mdata (meta f)]
{:type (d/name type)
:name (d/name name)
:module (-> (:ns mdata) (str/split ".") last)
:auth (:auth mdata true)
:webhook (::webhooks/event? mdata false)
:docs (::sv/docstring mdata)
:deprecated (::deprecated mdata)
:added (::added mdata)
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
:spec (get-spec-str (::sv/spec mdata))}))]
(letfn [(fmt-spec [mdata]
(when-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
(with-out-str
(ps/pprint (s/form spec)
{:ns-aliases {"clojure.spec.alpha" "s"
"clojure.core.specs.alpha" "score"
"clojure.core" nil}}))))
(fmt-schema [type mdata key]
(when-let [schema (get mdata key)]
(if (= type :js)
(smdj/describe (sm/schema schema) {::smdj/max-level 4})
(-> (smdn/describe (sm/schema schema))
(pp/pprint-str {:level 5 :width 70})))))
(get-context [mdata]
{:name (::sv/name mdata)
:module (or (some-> (::module mdata) d/name)
(-> (:ns mdata) (str/split ".") last))
:auth (::rpc/auth mdata true)
:webhook (::webhooks/event? mdata false)
:docs (::sv/docstring mdata)
:deprecated (::deprecated mdata)
:added (::added mdata)
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
:spec (fmt-spec mdata)
:entrypoint (str (cf/get :public-uri) "/api/rpc/command/" (::sv/name mdata))
:params-schema-js (fmt-schema :js mdata ::sm/params)
:result-schema-js (fmt-schema :js mdata ::sm/result)
:webhook-schema-js (fmt-schema :js mdata ::sm/webhook)
:params-schema-clj (fmt-schema :clj mdata ::sm/params)
:result-schema-clj (fmt-schema :clj mdata ::sm/result)
:webhook-schema-clj (fmt-schema :clj mdata ::sm/webhook)})]
{:version (:main cf/version)
:command-methods
(->> (:commands methods)
(map (partial gen-doc :command))
(sort-by (juxt :module :name)))
:query-methods
(->> (:queries methods)
(map (partial gen-doc :query))
(sort-by (juxt :module :name)))
:mutation-methods
(->> (:mutations methods)
(map (partial gen-doc :query))
:methods
(->> methods
(map val)
(map first)
(remove ::skip)
(map get-context)
(sort-by (juxt :module :name)))}))
(defn- handler
[methods]
(defn- doc-handler
[context]
(if (contains? cf/flags :backend-api-doc)
(let [context (prepare-context methods)]
(fn [_ respond _]
(respond (yrs/response 200 (-> (io/resource "app/templates/api-doc.tmpl")
(tmpl/render context))))))
(fn [_ respond _]
(respond (yrs/response 404)))))
(fn [request]
(let [params (:query-params request)
pstyle (:type params "js")
context (assoc context :param-style pstyle)]
{::yrs/status 200
::yrs/body (-> (io/resource "app/templates/api-doc.tmpl")
(tmpl/render context))}))
(fn [_]
{::yrs/status 404})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OPENAPI / SWAGGER (v3.1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def output-transformer
(mt/transformer
sm/default-transformer
(mt/key-transformer {:encode str/camel
:decode (comp keyword str/kebab)})))
(defn prepare-openapi-context
[methods]
(letfn [(gen-response-doc [tsx schema]
(let [schema (sm/schema schema)
example (sm/generate schema)
example (sm/encode schema example output-transformer)]
{:default
{:description "A default response"
:content
{"application/json"
{:schema tsx
:example example}}}}))
(gen-params-doc [tsx schema]
(let [example (sm/generate schema)
example (sm/encode schema example output-transformer)]
{:required true
:content
{"application/json"
{:schema tsx
:example example}}}))
(gen-method-doc [options mdata]
(let [pschema (::sm/params mdata)
rschema (::sm/result mdata)
sparams (-> pschema (oapi/transform options) (gen-params-doc pschema))
sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema))
rpost {:description (::sv/docstring mdata)
:deprecated (::deprecated mdata false)
:requestBody sparams}
rpost (cond-> rpost
(some? sresp)
(assoc :responses sresp))]
{:name (-> mdata ::sv/name d/name)
:module (-> (:ns mdata) (str/split ".") last)
:repr {:post rpost}}))
]
(let [definitions (atom {})
options {:registry sr/default-registry
::oapi/definitions-path "#/components/schemas/"
::oapi/definitions definitions}
paths (binding [oapi/*definitions* definitions]
(->> methods
(map (comp first val))
(filter ::sm/params)
(map (partial gen-method-doc options))
(sort-by (juxt :module :name))
(map (fn [doc]
[(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
(into {})))]
{:openapi "3.0.0"
:info {:version (:main cf/version)}
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
;; :description "penpot backend"
}]
:security
{:api_key []}
:paths paths
:components {:schemas @definitions}})))
(defn openapi-json-handler
[context]
(if (contains? cf/flags :backend-openapi-doc)
(fn [_]
{::yrs/status 200
::yrs/headers {"content-type" "application/json; charset=utf-8"}
::yrs/body (json/encode context)})
(fn [_]
{::yrs/status 404})))
(defn openapi-handler
[]
(if (contains? cf/flags :backend-openapi-doc)
(fn [_]
(let [swagger-js (slurp (io/resource "app/assets/swagger-ui-4.18.3.js"))
swagger-cs (slurp (io/resource "app/assets/swagger-ui-4.18.3.css"))
context {:public-uri (cf/get :public-uri)
:swagger-js swagger-js
:swagger-css swagger-cs}]
{::yrs/status 200
::yrs/headers {"content-type" "text/html"}
::yrs/body (-> (io/resource "app/templates/openapi.tmpl")
(tmpl/render context))}))
(fn [_]
{::yrs/status 404})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MODULE INIT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::routes vector?)
@@ -77,6 +207,18 @@
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
["/_doc" {:handler (handler methods)
:allowed-methods #{:get}}])
[(let [context (prepare-doc-context methods)]
[["/_doc"
{:handler (doc-handler context)
:allowed-methods #{:get}}]
["/doc"
{:handler (doc-handler context)
:allowed-methods #{:get}}]])
(let [context (prepare-openapi-context methods)]
[["/openapi"
{:handler (openapi-handler)
:allowed-methods #{:get}}]
["/openapi.json"
{:handler (openapi-json-handler context)
:allowed-methods #{:get}}]])])

View File

@@ -10,7 +10,8 @@
(:require
[app.common.data.macros :as dm]
[app.http :as-alias http]
[app.rpc :as-alias rpc]))
[app.rpc :as-alias rpc]
[yetti.response :as-alias yrs]))
;; A utilty wrapper object for wrap service responses that does not
;; implements the IObj interface that make possible attach metadata to
@@ -35,7 +36,9 @@
o
(MetadataWrapper. o {})))
([o m]
(MetadataWrapper. o m)))
(if (instance? clojure.lang.IObj o)
(vary-meta o merge m)
(MetadataWrapper. o m))))
(defn wrapped?
[o]
@@ -74,4 +77,4 @@
(fn [_ response]
(let [exp (if (integer? max-age) max-age (inst-ms max-age))
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
(update response :headers assoc "cache-control" val)))))
(update response ::yrs/headers assoc "cache-control" val)))))

View File

@@ -1,116 +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.rpc.mutations.fonts
(:require
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc.commands.fonts :as fonts]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
(declare create-font-variant)
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::name ::us/not-empty-string)
(s/def ::weight valid-weight)
(s/def ::style valid-style)
(s/def ::font-id ::us/uuid)
(s/def ::data (s/map-of ::us/string any?))
(s/def ::create-font-variant
(s/keys :req-un [::profile-id ::team-id ::data
::font-id ::font-family ::font-weight ::font-style]))
(declare create-font-variant)
(sv/defmethod ::create-font-variant
{::doc/added "1.3"
::doc/deprecated "1.18"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id team-id)
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(fonts/create-font-variant cfg params)))
;; --- UPDATE FONT FAMILY
(s/def ::update-font
(s/keys :req-un [::profile-id ::team-id ::id ::name]))
(sv/defmethod ::update-font
{::doc/added "1.3"
::doc/deprecated "1.18"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(rph/with-meta
(db/update! conn :team-font-variant
{:font-family name}
{:font-id id
:team-id team-id})
{::audit/replace-props {:id id
:name name
:team-id team-id
:profile-id profile-id}})))
;; --- DELETE FONT
(s/def ::delete-font
(s/keys :req-un [::profile-id ::team-id ::id]))
(sv/defmethod ::delete-font
{::doc/added "1.3"
::doc/deprecated "1.18"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [font (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:font-id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:id id
:team-id team-id
:name (:font-family font)
:profile-id profile-id}}))))
;; --- DELETE FONT VARIANT
(s/def ::delete-font-variant
(s/keys :req-un [::profile-id ::team-id ::id]))
(sv/defmethod ::delete-font-variant
{::doc/added "1.3"
::doc/deprecated "1.18"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [variant (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}}))))

View File

@@ -1,55 +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.rpc.mutations.media
(:require
[app.db :as db]
[app.media :as media]
[app.rpc.commands.files :as files]
[app.rpc.commands.media :as cmd.media]
[app.rpc.doc :as-alias doc]
[app.storage :as-alias sto]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Create File Media object (upload)
(s/def ::upload-file-media-object ::cmd.media/upload-file-media-object)
(sv/defmethod ::upload-file-media-object
{::doc/added "1.2"
::doc/deprecated "1.18"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(media/validate-media-type! content)
(cmd.media/validate-content-size! content)
(cmd.media/create-file-media-object cfg params)))
;; --- Create File Media Object (from URL)
(s/def ::create-file-media-object-from-url ::cmd.media/create-file-media-object-from-url)
(sv/defmethod ::create-file-media-object-from-url
{::doc/added "1.3"
::doc/deprecated "1.18"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(#'cmd.media/create-file-media-object-from-url cfg params)))
;; --- Clone File Media object (Upload and create from url)
(s/def ::clone-file-media-object ::cmd.media/clone-file-media-object)
(sv/defmethod ::clone-file-media-object
{::doc/added "1.2"
::doc/deprecated "1.18"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(-> (assoc cfg :conn conn)
(cmd.media/clone-file-media-object params))))

View File

@@ -1,193 +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.rpc.mutations.profile
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.media :as media]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
;; --- Helpers & Specs
(s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/string)
(s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string)
(s/def ::old-password (s/nilable ::us/string))
(s/def ::theme ::us/string)
;; --- MUTATION: Update Profile (own)
(s/def ::update-profile
(s/keys :req-un [::fullname ::profile-id]
:opt-un [::lang ::theme]))
(sv/defmethod ::update-profile
{::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [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
;; the same row/object.
(let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true)
(profile/decode-row))
;; Update the profile map with direct params
profile (-> profile
(assoc :fullname fullname)
(assoc :lang lang)
(assoc :theme theme))
]
(db/update! conn :profile
{:fullname fullname
:lang lang
:theme theme
:props (db/tjson (:props profile))}
{:id profile-id})
(-> profile
(profile/strip-private-attrs)
(d/without-nils)
(rph/with-meta {::audit/props (audit/profile->props profile)})))))
;; --- MUTATION: Update Password
(s/def ::update-profile-password
(s/keys :req-un [::profile-id ::password ::old-password]))
(sv/defmethod ::update-profile-password
{::climit/queue :auth
::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [password] :as params}]
(db/with-atomic [conn pool]
(let [profile (#'profile/validate-password! conn params)
session-id (::session/id params)]
(when (= (str/lower (:email profile))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
(profile/update-profile-password! conn (assoc profile :password password))
(#'profile/invalidate-profile-session! conn (:id profile) session-id)
nil)))
;; --- MUTATION: Update Photo
(s/def ::file ::media/upload)
(s/def ::update-profile-photo
(s/keys :req-un [::profile-id ::file]))
(sv/defmethod ::update-profile-photo
{::doc/added "1.0"
::doc/deprecated "1.18"}
[cfg {:keys [file] :as params}]
;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(profile/update-profile-photo cfg params)))
;; --- MUTATION: Request Email Change
(s/def ::request-email-change
(s/keys :req-un [::email]))
(sv/defmethod ::request-email-change
{::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id email] :as params}]
(db/with-atomic [conn pool]
(let [profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg ::profile/conn conn)
params (assoc params
:profile profile
:email (str/lower email))]
(if (contains? cf/flags :smtp)
(#'profile/request-email-change! cfg params)
(#'profile/change-email-immediately! cfg params)))))
;; --- MUTATION: Update Profile Props
(s/def ::props map?)
(s/def ::update-profile-props
(s/keys :req-un [::profile-id ::props]))
(sv/defmethod ::update-profile-props
{::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id props]}]
(db/with-atomic [conn pool]
(let [profile (profile/get-profile conn profile-id ::db/for-update? true)
props (reduce-kv (fn [props k v]
;; We don't accept namespaced keys
(if (simple-ident? k)
(if (nil? v)
(dissoc props k)
(assoc props k v))
props))
(:props profile)
props)]
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id})
(profile/filter-props props))))
;; --- MUTATION: Delete Profile
(s/def ::delete-profile
(s/keys :req-un [::profile-id]))
(sv/defmethod ::delete-profile
{::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id] :as params}]
(db/with-atomic [conn pool]
(let [teams (#'profile/get-owned-teams-with-participants conn profile-id)
deleted-at (dt/now)]
;; If we found owned teams with participants, we don't allow
;; delete profile until the user properly transfer ownership or
;; explicitly removes all participants from the team
(when (some pos? (map :participants teams))
(ex/raise :type :validation
:code :owner-teams-with-people
: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}))
(db/update! conn :profile
{:deleted-at deleted-at}
{:id profile-id})
(rph/with-transform {} (session/delete-fn cfg)))))

View File

@@ -1,130 +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.rpc.mutations.projects
(:require
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
;; --- Mutation: Create Project
(s/def ::team-id ::us/uuid)
(s/def ::create-project
(s/keys :req-un [::profile-id ::team-id ::name]
:opt-un [::id]))
(sv/defmethod ::create-project
{::doc/added "1.0"
::doc/deprecated "1.18"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(let [project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned true})
(assoc project :is-pinned true))))
;; --- Mutation: Toggle Project Pin
(def ^:private
sql:update-project-pin
"insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned)
values (?, ?, ?, ?)
on conflict (team_id, project_id, profile_id)
do update set is_pinned=?")
(s/def ::is-pinned ::us/boolean)
(s/def ::project-id ::us/uuid)
(s/def ::update-project-pin
(s/keys :req-un [::profile-id ::id ::team-id ::is-pinned]))
(sv/defmethod ::update-project-pin
{::doc/added "1.0"
::doc/deprecated "1.18"
::webhooks/batch-timeout (dt/duration "5s")
::webhooks/batch-key :id
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [id profile-id team-id is-pinned] :as params}]
(db/with-atomic [conn pool]
(projects/check-edition-permissions! conn profile-id id)
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
nil))
;; --- Mutation: Rename Project
(declare rename-project)
(s/def ::rename-project
(s/keys :req-un [::profile-id ::name ::id]))
(sv/defmethod ::rename-project
{::doc/added "1.0"
::doc/deprecated "1.18"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [id profile-id name] :as params}]
(db/with-atomic [conn pool]
(projects/check-edition-permissions! conn profile-id id)
(let [project (db/get-by-id conn :project id)]
(db/update! conn :project
{:name name}
{:id id})
(rph/with-meta (rph/wrap)
{::audit/props {:team-id (:team-id project)
:prev-name (:name project)}}))))
;; --- Mutation: Delete Project
(s/def ::delete-project
(s/keys :req-un [::id ::profile-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.0"
::doc/deprecated "1.18"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(projects/check-edition-permissions! conn profile-id id)
(let [project (db/update! conn :project
{:deleted-at (dt/now)}
{:id id :is-default false})]
(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

@@ -1,71 +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.rpc.mutations.share-link
"Share link related rpc mutation methods."
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::who-comment ::us/string)
(s/def ::who-inspect ::us/string)
(s/def ::pages (s/every ::us/uuid :kind set?))
;; --- Mutation: Create Share Link
(declare create-share-link)
(s/def ::create-share-link
(s/keys :req-un [::profile-id ::file-id ::who-comment ::who-inspect ::pages]))
(sv/defmethod ::create-share-link
"Creates a share-link object.
Share links are resources that allows external users access to specific
pages of a file with specific permissions (who-comment and who-inspect)."
{::doc/added "1.5"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(create-share-link conn params)))
(defn create-share-link
[conn {:keys [profile-id file-id pages who-comment who-inspect]}]
(let [pages (db/create-array conn "uuid" pages)
slink (db/insert! conn :share-link
{:id (uuid/next)
:file-id file-id
:who-comment who-comment
:who-inspect who-inspect
:pages pages
:owner-id profile-id})]
(update slink :pages db/decode-pgarray #{})))
;; --- Mutation: Delete Share Link
(s/def ::delete-share-link
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-share-link
{::doc/added "1.5"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [slink (db/get-by-id conn :share-link id)]
(files/check-edition-permissions! conn profile-id (:file-id slink))
(db/delete! conn :share-link {:id id})
nil)))

View File

@@ -8,9 +8,20 @@
"A permission checking helper factories."
(:require
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.spec :as us]
[clojure.spec.alpha :as s]))
(sm/def! ::permissions
[:map {:title "Permissions"}
[:type {:gen/elements [:membership :share-link]} :keyword]
[:is-owner :boolean]
[:is-admin :boolean]
[:can-edit :boolean]
[:can-read :boolean]
[:is-logged :boolean]])
(s/def ::role #{:admin :owner :editor :viewer})
(defn assign-role-flags

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.rpc.queries.fonts
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Query: Font Variants
(s/def ::team-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::font-variants
(s/and
(s/keys :req-un [::profile-id]
:opt-un [::team-id
::file-id
::project-id])
(fn [o]
(or (contains? o :team-id)
(contains? o :file-id)
(contains? o :project-id)))))
(sv/defmethod ::font-variants
{::doc/added "1.7"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id file-id project-id] :as params}]
(with-open [conn (db/open pool)]
(cond
(uuid? team-id)
(do
(teams/check-read-permissions! conn profile-id team-id)
(db/query conn :team-font-variant
{:team-id team-id
:deleted-at nil}))
(uuid? project-id)
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
(projects/check-read-permissions! conn profile-id project-id)
(db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil}))
(uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})]
(files/check-read-permissions! conn profile-id file-id)
(db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})))))

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.rpc.queries.profile
(:require
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(s/def ::profile ::profile/get-profile)
(sv/defmethod ::profile
{::rpc/auth false
::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id]}]
;; We need to return the anonymous profile object in two cases, when
;; no profile-id is in session, and when db call raises not found. In all other
;; cases we need to reraise the exception.
(try
(-> (profile/get-profile pool profile-id)
(profile/strip-private-attrs)
(update :props profile/filter-props))
(catch Throwable _
{:id uuid/zero :fullname "Anonymous User"})))

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.rpc.queries.projects
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Query: Projects
(s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::projects
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::projects
{::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [pool]} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(projects/get-projects conn profile-id team-id)))
;; --- Query: All projects
(s/def ::profile-id ::us/uuid)
(s/def ::all-projects
(s/keys :req-un [::profile-id]))
(sv/defmethod ::all-projects
{::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [pool]} {:keys [profile-id]}]
(with-open [conn (db/open pool)]
(projects/get-all-projects conn profile-id)))
;; --- Query: Project
(s/def ::id ::us/uuid)
(s/def ::project
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::project
{::doc/added "1.0"
::doc/deprecated "1.18"}
[{:keys [pool]} {:keys [profile-id id]}]
(with-open [conn (db/open pool)]
(let [project (db/get-by-id conn :project id)]
(projects/check-read-permissions! conn profile-id id)
project)))

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.rpc.queries.viewer
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.viewer :as viewer]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(s/def ::components-v2 ::us/boolean)
(s/def ::view-only-bundle
(s/and ::viewer/get-view-only-bundle
(s/keys :opt-un [::components-v2])))
(sv/defmethod ::view-only-bundle
{::rpc/auth false
::doc/added "1.3"
::doc/deprecated "1.18"}
[{:keys [pool] :as cfg} {:keys [features components-v2] :as params}]
(with-open [conn (db/open pool)]
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
features (cond-> (or features #{})
components-v2 (conj "components/v2"))
params (assoc params :features features)]
(viewer/get-view-only-bundle conn params))))

View File

@@ -5,20 +5,20 @@
;; Copyright (c) KALEIDOS INC
(ns app.rpc.retry
"A fault tolerance RPC middleware. Allow retry some operations that we
know we can retry."
(:require
[app.common.logging :as l]
[app.util.retry :refer [conflict-exception?]]
[app.util.services :as sv]
[promesa.core :as p]))
[app.db :as db]
[app.util.services :as sv])
(:import
org.postgresql.util.PSQLException))
(defn conflict-db-insert?
(defn conflict-exception?
"Check if exception matches a insertion conflict on postgresql."
[e]
(conflict-exception? e))
(and (instance? PSQLException e)
(= "23505" (.getSQLState ^PSQLException e))))
(def always-false (constantly false))
(def ^:private always-false (constantly false))
(defn wrap-retry
[_ f {:keys [::matches ::sv/name] :or {matches always-false} :as mdata}]
@@ -28,18 +28,36 @@
(if-let [max-retries (::max-retries mdata)]
(fn [cfg params]
(letfn [(run [retry]
(->> (f cfg params)
(p/merr (partial handle-error retry))))
(handle-error [retry cause]
(if (matches cause)
(let [current-retry (inc retry)]
(l/trace :hint "running retry algorithm" :retry current-retry)
(if (<= current-retry max-retries)
(run current-retry)
(throw cause)))
(throw cause)))]
(run 1)))
((fn run [retry]
(try
(f cfg params)
(catch Throwable cause
(if (matches cause)
(let [current-retry (inc retry)]
(l/trace :hint "running retry algorithm" :retry current-retry)
(if (<= current-retry max-retries)
(run current-retry)
(throw cause)))
(throw cause))))) 1))
f))
(defmacro with-retry
[{:keys [::when ::max-retries ::label ::db/conn] :or {max-retries 3}} & body]
`(let [conn# ~conn]
(assert (or (nil? conn#) (db/connection? conn#)) "invalid database connection")
(loop [tnum# 1]
(let [result# (let [sp# (some-> conn# db/savepoint)]
(try
(let [result# (do ~@body)]
(some->> sp# (db/release! conn#))
result#)
(catch Throwable cause#
(some->> sp# (db/rollback! conn#))
(if (and (~when cause#) (<= tnum# ~max-retries))
::retry
(throw cause#)))))]
(if (= ::retry result#)
(do
(l/warn :hint "retrying operation" :label ~label :retry tnum#)
(recur (inc tnum#)))
result#)))))

View File

@@ -55,6 +55,7 @@
[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.services :as-alias sv]
[app.util.time :as dt]
@@ -64,7 +65,6 @@
[cuerdas.core :as str]
[datoteka.fs :as fs]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]))
(def ^:private default-timeout
@@ -82,7 +82,7 @@
{::rscript/name ::window-rate-limit
::rscript/path "app/rpc/rlimit/window.lua"})
(def enabled?
(def enabled
"Allows on runtime completely disable rate limiting."
(atom true))
@@ -119,122 +119,129 @@
(defmethod parse-limit :bucket
[[name strategy opts :as vlimit]]
(us/assert! ::limit-tuple vlimit)
(merge
{::name name
::strategy strategy}
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
(let [interval (dt/duration interval)
rate (parse-long rate)
capacity (parse-long capacity)]
{::capacity capacity
::rate rate
::interval interval
::opts opts
::params [(dt/->seconds interval) rate capacity]
::key (str "ratelimit.bucket." (d/name name))})
(ex/raise :type :validation
:code :invalid-bucket-limit-opts
:hint (str/ffmt "looks like '%' does not have a valid format" opts)))))
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
(let [interval (dt/duration interval)
rate (parse-long rate)
capacity (parse-long capacity)]
{::name name
::strategy strategy
::capacity capacity
::rate rate
::interval interval
::opts opts
::params [(dt/->seconds interval) rate capacity]
::key (str "ratelimit.bucket." (d/name name))})
(ex/raise :type :validation
:code :invalid-bucket-limit-opts
:hint (str/ffmt "looks like '%' does not have a valid format" opts))))
(defmethod process-limit :bucket
[redis user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id)])
(assoc ::rscript/vals (conj params (dt/->seconds now))))]
(->> (rds/eval! redis script)
(p/fmap (fn [result]
(let [allowed? (boolean (nth result 0))
remaining (nth result 1)
reset (* (/ (inst-ms interval) rate)
(- capacity remaining))]
(l/trace :hint "limit processed"
:service service
:limit (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
:allowed? allowed?
:remaining remaining)
(-> limit
(assoc ::lresult/allowed? allowed?)
(assoc ::lresult/reset (dt/plus now reset))
(assoc ::lresult/remaining remaining))))))))
(let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id)])
(assoc ::rscript/vals (conj params (dt/->seconds now))))
result (rds/eval! redis script)
allowed? (boolean (nth result 0))
remaining (nth result 1)
reset (* (/ (inst-ms interval) rate)
(- capacity remaining))]
(l/trace :hint "limit processed"
:service service
:limit (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
:allowed allowed?
:remaining remaining)
(-> limit
(assoc ::lresult/allowed allowed?)
(assoc ::lresult/reset (dt/plus now reset))
(assoc ::lresult/remaining remaining))))
(defmethod process-limit :window
[redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
(let [ts (dt/truncate now unit)
ttl (dt/diff now (dt/plus ts {unit 1}))
script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))]
(->> (rds/eval! redis script)
(p/fmap (fn [result]
(let [allowed? (boolean (nth result 0))
remaining (nth result 1)]
(l/trace :hint "limit processed"
:service service
:limit (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
:allowed? allowed?
:remaining remaining)
(-> limit
(assoc ::lresult/allowed? allowed?)
(assoc ::lresult/remaining remaining)
(assoc ::lresult/reset (dt/plus ts {unit 1})))))))))
(let [ts (dt/truncate now unit)
ttl (dt/diff now (dt/plus ts {unit 1}))
script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))
result (rds/eval! redis script)
allowed? (boolean (nth result 0))
remaining (nth result 1)]
(l/trace :hint "limit processed"
:service service
:limit (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
:allowed allowed?
:remaining remaining)
(-> limit
(assoc ::lresult/allowed allowed?)
(assoc ::lresult/remaining remaining)
(assoc ::lresult/reset (dt/plus ts {unit 1})))))
(defn- process-limits!
[redis user-id limits now]
(->> (p/all (map (partial process-limit redis user-id now) limits))
(p/fmap (fn [results]
(let [remaining (->> results
(d/index-by ::name ::lresult/remaining)
(uri/map->query-string))
reset (->> results
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
(uri/map->query-string))
rejected (->> results
(filter (complement ::lresult/allowed?))
(first))]
(let [results (into [] (map (partial process-limit redis user-id now)) limits)
remaining (->> results
(d/index-by ::name ::lresult/remaining)
(uri/map->query-string))
reset (->> results
(d/index-by ::name (comp dt/->seconds ::lresult/reset))
(uri/map->query-string))
(when rejected
(l/warn :hint "rejected rate limit"
:user-id (str user-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
rejected (d/seek (complement ::lresult/allowed) results)]
{:enabled? true
:allowed? (not (some? rejected))
:headers {"x-rate-limit-remaining" remaining
"x-rate-limit-reset" reset}})))))
(when rejected
(l/warn :hint "rejected rate limit"
:user-id (str user-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
(defn- handle-response
[f cfg params result]
(if (:enabled? result)
(let [headers (:headers result)]
(if (:allowed? result)
(->> (f cfg params)
(p/fmap (fn [response]
(vary-meta response update ::http/headers merge headers))))
(p/rejected
(ex/error :type :rate-limit
:code :request-blocked
:hint "rate limit reached"
::http/headers headers))))
(f cfg params)))
{::enabled true
::allowed (not (some? rejected))
::remaingin remaining
::reset reset
::headers {"x-rate-limit-remaining" remaining
"x-rate-limit-reset" reset}}))
(defn- get-limits
[state skey sname]
(some->> (or (get-in @state [::limits skey])
(get-in @state [::limits :default]))
(map #(assoc % ::service sname))
(seq)))
(when-let [limits (or (get-in @state [::limits skey])
(get-in @state [::limits :default]))]
(into [] (map #(assoc % ::service sname)) limits)))
(defn- get-uid
[{:keys [::http/request] :as params}]
(or (::rpc/profile-id params)
(some-> request parse-client-ip)
uuid/zero))
[{:keys [::rpc/profile-id] :as params}]
(let [request (-> params meta ::http/request)]
(or profile-id
(some-> request parse-client-ip)
uuid/zero)))
(defn process-request!
[{:keys [::rpc/rlimit ::rds/redis ::skey ::sname] :as cfg} params]
(when-let [limits (get-limits rlimit skey sname)]
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
uid (get-uid params)
;; FIXME: why not clasic try/catch?
result (ex/try! (process-limits! redis uid limits (dt/now)))]
(l/trc :hint "process-limits"
:service sname
:remaining (::remaingin result)
:reset (::reset result))
(cond
(ex/exception? result)
(do
(l/error :hint "error on processing rate-limit" :cause result)
{::enabled false})
(contains? cf/flags :soft-rpc-rlimit)
{::enabled false}
:else
result))))
(defn wrap
[{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata]
@@ -243,36 +250,25 @@
(if rlimit
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))]
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))
cfg (-> cfg
(assoc ::skey skey)
(assoc ::sname sname))]
(fn [cfg params]
(if @enabled?
(try
(let [uid (get-uid params)
rsp (when-let [limits (get-limits rlimit skey sname)]
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
rsp (->> (process-limits! redis uid limits (dt/now))
(p/merr (fn [cause]
;; If we have an error on processing the rate-limit we just skip
;; it for do not cause service interruption because of redis
;; downtime or similar situation.
(l/error :hint "error on processing rate-limit" :cause cause)
(p/resolved {:enabled? false}))))]
;; If soft rate are enabled, we process the rate-limit but return unprotected
;; response.
(if (contains? cf/flags :soft-rpc-rlimit)
{:enabled? false}
rsp)))]
(->> (p/promise rsp)
(p/fmap #(or % {:enabled? false}))
(p/mcat #(handle-response f cfg params %))))
(catch Throwable cause
(p/rejected cause)))
(f cfg params))))
(fn [hcfg params]
(if @enabled
(let [result (process-request! cfg params)]
(if (::enabled result)
(if (::allowed result)
(-> (f hcfg params)
(rph/wrap)
(vary-meta update ::http/headers merge (::headers result)))
(ex/raise :type :rate-limit
:code :request-blocked
:hint "rate limit reached"
::http/headers (::headers result)))
(f hcfg params)))
(f hcfg params))))
f))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -352,7 +348,7 @@
::limits limits}))))
(defn- refresh-config
[{:keys [::state ::path ::wrk/executor ::wrk/scheduled-executor] :as cfg}]
[{:keys [::state ::path ::wrk/executor] :as cfg}]
(letfn [(update-config [{:keys [::updated-at] :as state}]
(let [updated-at' (fs/last-modified-time path)]
(merge state
@@ -367,8 +363,7 @@
state)))))
(schedule-next [state]
(px/schedule! scheduled-executor
(inst-ms (::refresh state))
(px/schedule! (inst-ms (::refresh state))
(partial refresh-config cfg))
state)]
@@ -391,8 +386,7 @@
(and (fs/exists? path) (fs/regular-file? path) path)))
(defmethod ig/pre-init-spec :app.rpc/rlimit [_]
(s/keys :req [::wrk/executor
::wrk/scheduled-executor]))
(s/keys :req [::wrk/executor]))
(defmethod ig/init-key ::rpc/rlimit
[_ {:keys [::wrk/executor] :as cfg}]

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