Compare commits

...

856 Commits

Author SHA1 Message Date
Alejandro Alonso
3e21b0d8cc 📎 Update CHANGES.md file 2022-10-19 07:55:00 +02:00
Alejandro
b23ece88c2 Merge pull request #2434 from penpot/eva-fix-dragging
🐛 Fix draggin projects css
2022-10-17 15:18:51 +02:00
Eva
0765587373 🐛 Fix draggin projects css 2022-10-17 15:05:51 +02:00
Eva Marco
5c8710b8cb Merge pull request #2458 from penpot/palba-upload-font-fails-silent2
🐛 Fix custom font upload fails silently for unsupported formats
2022-10-17 13:16:32 +02:00
Pablo Alba
88cd19d21a 🐛 Fix custom font upload fails silently for unsupported formats 2022-10-17 13:10:45 +02:00
Eva Marco
ac3251b29e Merge pull request #2464 from penpot/superalex-remove-imported-and-updated-extra-words
🎉 Remove imported and updated extra words
2022-10-17 13:09:15 +02:00
Alejandro Alonso
a8150e1b05 🎉 Remove imported and updated extra words 2022-10-17 13:03:59 +02:00
Eva Marco
e2f6274ff2 Merge pull request #2454 from penpot/superalex-fix-assertion-error-trying-to-move-board
🐛 Fix assertion error trying to move board if path tool selected
2022-10-17 12:10:41 +02:00
Alejandro Alonso
c670d81a20 🐛 Fix assertion error trying to move board if path tool selected 2022-10-17 12:00:39 +02:00
Eva Marco
a8e6516059 Merge pull request #2442 from penpot/superalex-fix-grid-not-syncing-in-multi-user
🐛 Fix grid not syncing in multi user
2022-10-17 09:33:43 +02:00
Alejandro Alonso
87d323bb4c 🐛 Fix grid not syncing in multi user 2022-10-17 09:28:38 +02:00
Eva Marco
4b52612682 Merge pull request #2456 from penpot/superalex-show-spinner-while-loading-viewer-file
🎉 Show spinner while loading viewer file
2022-10-17 09:02:12 +02:00
Pablo Alba
e1403d74bd Merge pull request #2453 from penpot/superalex-support-multi-style-texts
🐛 Fix comments section not scrolling
2022-10-14 18:02:49 +02:00
Alejandro Alonso
41f5fb9621 🐛 Fix comments section not scrolling 2022-10-14 18:02:07 +02:00
Alejandro Alonso
563a6da83c 🐛 Fix partially missing english translation 2022-10-14 14:50:40 +02:00
Alejandro Alonso
3395fcb697 🎉 Show spinner while loading viewer file 2022-10-14 14:12:17 +02:00
Pablo Alba
b7d5960ec3 Merge pull request #2455 from penpot/superalex-fix-notification-for-newsletter-shown-in-all-cases
🐛 Fix notification for newsletter shown in all cases
2022-10-14 12:38:09 +02:00
Alejandro Alonso
c690a71b3e 🐛 Fix notification for newsletter shown in all cases 2022-10-14 12:26:59 +02:00
Eva Marco
b5ab9af5c9 Merge pull request #2441 from penpot/palba-gradient-handlers
🐛 Fix gradient handlers are under resize handlers
2022-10-13 16:29:29 +02:00
Pablo Alba
1e07c16633 🐛 Fix gradient handlers are under resize handlers 2022-10-13 15:55:36 +02:00
Eva Marco
9e6f12cb82 Merge pull request #2445 from penpot/palba-boards-grouped-titles
🐛 Fix boards grouped shouldn't show the title
2022-10-13 14:35:19 +02:00
Pablo Alba
00180f4fba 🐛 Fix boards grouped shouldn't show the title 2022-10-13 13:44:39 +02:00
Eva
ea15735372 🐛 Fix loading placeholder 2022-10-13 12:53:49 +02:00
Pablo Alba
0f5ba91f44 Merge pull request #2440 from penpot/eva-update-icon
💄 Update action icon
2022-10-13 12:41:09 +02:00
Eva
13cb186c70 💄 Update action icon 2022-10-13 12:24:48 +02:00
Alejandro Alonso
1dbaaf12fa Merge remote-tracking branch 'origin/main' into staging 2022-10-11 14:02:38 +02:00
Alejandro
adb19d0c83 Merge pull request #2433 from penpot/eva-fix-register-css
🐛 Fix social buttons in register form
2022-10-11 13:56:43 +02:00
Eva
d5d1cff420 🐛 Fix social buttons in register form 2022-10-11 13:36:11 +02:00
Alejandro Alonso
5ef390f07e WIP 2022-10-11 09:11:45 +02:00
Alejandro Alonso
0f7295dd7c Merge remote-tracking branch 'origin/main' into staging 2022-10-10 18:07:05 +02:00
Alejandro
556c0d0c2a Merge pull request #2431 from penpot/niwinz-event-handling-dashboard
 Improve consistency on event handling on dashboard
2022-10-10 18:02:51 +02:00
Andrey Antukh
582a20d369 Improve consistency on event handling on dashboard 2022-10-10 17:50:04 +02:00
Alejandro
cad2201c54 Merge pull request #2412 from penpot/niwinz-fixes-on-profile-deletion
🐛 Fix issues on profile deletion procedure
2022-10-10 10:54:41 +02:00
Alejandro
274e034033 Merge pull request #2430 from penpot/fix-text
💄 Fix text grammar
2022-10-10 10:47:01 +02:00
Andrés Moya
dec9c339cd 💄 Fix text grammar 2022-10-10 10:39:35 +02:00
Eva Marco
ec19ec9280 Merge pull request #2414 from penpot/superalex-fix-artboard-border-radius
🐛 Fix artboard border radius
2022-10-10 09:30:36 +02:00
Eva Marco
fe371c088b Merge pull request #2416 from penpot/superalex-fix-viewer-dashboard-link
🐛 Fix viewer dashboard link
2022-10-10 09:20:53 +02:00
Alejandro Alonso
ac5412301e 🐛 Fix viewer dashboard link 2022-10-06 11:12:17 +02:00
Andrey Antukh
c0b778b67a 📎 Minor change on changelog file 2022-10-06 10:12:15 +02:00
Andrey Antukh
1309b51320 📎 Update bundle build script 2022-10-06 10:11:19 +02:00
Alejandro Alonso
9391cc9a41 🐛 Fix artboard border radius 2022-10-06 09:17:24 +02:00
Andrey Antukh
cc1dff4d3d ⬆️ Update exporter yarn dependencies 2022-10-05 11:39:40 +02:00
Andrey Antukh
6e28bb9df8 🐛 Fix issues on profile deletion procedure 2022-10-05 11:27:11 +02:00
Alejandro Alonso
02c0c867d6 Merge remote-tracking branch 'origin/main' into staging 2022-10-05 11:16:41 +02:00
Alejandro Alonso
796fcee1d8 🐛 Fix twitter feedback link 2022-10-05 11:14:53 +02:00
Alejandro Alonso
80463536a8 Merge remote-tracking branch 'origin/main' into staging 2022-10-05 09:33:58 +02:00
Andrey Antukh
5a06749664 🐛 Disable broadcast-channel when it is not available (mainly safari) 2022-10-05 09:33:41 +02:00
Alejandro
5ad385cf93 Merge pull request #2409 from penpot/niwinz-hotfix
Fix compatibility issues with safari >=14 & < 16
2022-10-05 09:22:11 +02:00
Andrey Antukh
c534a40923 ⬆️ Update versions on default docker compose file 2022-10-05 08:42:14 +02:00
Andrey Antukh
348bc48db4 📎 Minor change on docker build script 2022-10-05 08:41:56 +02:00
Andrey Antukh
7fa44aa256 🐛 Disable broadcast-channel when it is not available (mainly safari) 2022-10-04 23:19:28 +02:00
Andrey Antukh
f1c3c41455 🐛 Fix compatibility issues with some bigint api and safari 2022-10-04 21:03:37 +02:00
Alejandro Alonso
65da328b25 Merge remote-tracking branch 'origin/main' into staging 2022-10-04 15:59:21 +02:00
Andrey Antukh
c53152f027 📎 Update manage.sh 2022-10-04 15:14:25 +02:00
Andrey Antukh
953607fc4a 📎 Update changes and version files 2022-10-04 14:16:36 +02:00
Andrey Antukh
50af997f55 Merge pull request #2386 from penpot/palba-newsletter-subscription-st
 Newsletter suscription
2022-10-04 14:00:22 +02:00
Alejandro
d468c74851 Merge pull request #2403 from penpot/niwinz-websockets-internal-fixes
♻️ Refactor internal websocket connection stage management
2022-10-04 11:19:31 +02:00
Alejandro
7d0f2d76e8 Merge pull request #2384 from penpot/hiru-interactions-nested-boards
 Improve interactions with nested boards
2022-10-04 11:15:59 +02:00
Alejandro
8b721d2024 Merge pull request #2382 from penpot/niwinz-lazy-load-graphics-assets
🎉 Add lazy load for graphic assets thumbnails
2022-10-04 11:02:26 +02:00
Alejandro
3044d0abcc Merge pull request #2381 from penpot/niwinz-chunked-exports
 Make the exportation streaming directly to response
2022-10-04 10:55:49 +02:00
Alejandro
b2fd13e6bf Merge pull request #2371 from penpot/eva-fix-viewmode-login
🐛 Fix view mode login size
2022-10-04 10:45:12 +02:00
Andrey Antukh
6fc5813182 Merge pull request #2402 from penpot/hiru-fix-touched
🐛 Fix touched detection when moving instances
2022-10-04 10:21:56 +02:00
Eva Marco
6e5ba88240 Merge pull request #2387 from penpot/hiru-new-dashboard
🐛 Show new dashboard only in v2, and fix loading screen
2022-10-03 16:29:00 +02:00
Andrés Moya
a6d9a65843 🐛 Show new dashboard only in v2, and fix loading screen 2022-10-03 16:13:29 +02:00
Andrey Antukh
8fae7f7aa6 ♻️ Refactor internal websocket connection stage management 2022-10-03 13:40:33 +02:00
Andrés Moya
e9d3e8a643 🐛 Fix touched detection when moving instances 2022-10-03 12:27:00 +02:00
Pablo Alba
16e8d1fcf2 Newsletter suscription 2022-09-30 12:22:53 +02:00
Andrés Moya
0e49625ebf Improve interactions with nested boards 2022-09-30 11:28:45 +02:00
Andrey Antukh
fcbb95e8b6 💄 Replace some use-callback with shorter use-fn on sidebar/assets 2022-09-29 15:09:28 +02:00
Andrey Antukh
0fc2442175 🎉 Make the graphics assets thumbnail load lazy 2022-09-29 15:07:57 +02:00
Andrey Antukh
8c39c3af9f Make the exportation streaming directly to response 2022-09-29 14:28:45 +02:00
Eva Marco
7dcd362abd Merge pull request #2380 from penpot/juan-dashboard-CSS
🎉 Dashboard new design review
2022-09-29 14:14:38 +02:00
Elhombretecla
23d1087bc5 🎉 Change pin icon 2022-09-29 14:00:11 +02:00
Elhombretecla
8d5a97f6e5 Change team UI dropdown refactor 2022-09-29 12:45:41 +02:00
Elhombretecla
80f49e06cc 🎉 add new css changes 2022-09-29 12:45:41 +02:00
Andrey Antukh
4378d71b70 🐛 Fix error message on login when profile does not have password 2022-09-29 11:42:45 +02:00
Andrey Antukh
b7206d734b 📎 Minor devenv update 2022-09-29 10:32:18 +02:00
Andrey Antukh
886ab0e152 Improve iteration and add concat-all and fully lazy mapcat helper 2022-09-29 10:32:18 +02:00
Eva
055a870c1f 🐛 Fix view mode login size 2022-09-29 09:00:13 +02:00
Andrey Antukh
058727a44b 🐛 Fix wrong spec on config 2022-09-29 08:38:49 +02:00
Andrey Antukh
a6e14846c7 🐛 Fix inconsistencies on config related to semaphores 2022-09-28 15:33:29 +02:00
Andrey Antukh
c2fec03fc7 Increase the auth token timeout 2022-09-28 15:33:07 +02:00
Alejandro Alonso
14788846a5 Merge remote-tracking branch 'origin/main' into staging 2022-09-28 14:25:27 +02:00
Alejandro
94a9bc844a Merge pull request #2372 from penpot/eva-empty-viewer
🐛 Fix alignment of 'no boards' message in viewer
2022-09-28 14:22:08 +02:00
Eva
745aa17d8a 🐛 Fix alignment of 'no boards' message in viewer 2022-09-28 14:20:43 +02:00
Alejandro
012315f207 Merge pull request #2367 from penpot/eva-fix-typos
🐛 Fix some typos
2022-09-28 14:12:00 +02:00
Eva
ba37168a84 🐛 Fix some typos 2022-09-28 14:10:34 +02:00
Alejandro
c68a6cbc10 Merge pull request #2366 from penpot/eva-hotfix-pdf-exportation
🐛 Fix PDF exportation order
2022-09-28 14:09:16 +02:00
Alejandro
35a72be4f2 Merge pull request #2346 from penpot/niwinz-hot-improvements
Improvements & Fixes (part 2)
2022-09-28 12:04:55 +02:00
Andrey Antukh
53c358cfd7 🐛 Fix ssl support on email sending module 2022-09-28 11:30:48 +02:00
Eva
c2ccdd5680 🐛 Fix PDF exportation order 2022-09-28 10:35:26 +02:00
Lazalatin
3a4563d755 🐛 Fix doubled quotes in frontend config
Docker parses environment variables literally, delivering quoted flags in the $PENPOT_FLAGS variable. This in turn leads to doubled quotes in the resulting config.js in front and after the flags, omitting them completely.

This commit fixes this behaviour.
2022-09-28 09:50:20 +02:00
Andrey Antukh
ab22909b6c Merge pull request #2364 from penpot/superalex-fix-resend-invitation
🐛 Fix Internal error when resending invitation email
2022-09-28 09:47:55 +02:00
Andrey Antukh
89e64236b0 Don't log exception on health check fail 2022-09-28 09:47:13 +02:00
Andrey Antukh
748499a26f 🎉 Add lazy loading of thumbnails on dashboard 2022-09-28 09:47:13 +02:00
Alejandro Alonso
8fec5af55e 🐛 Fix cannot take out an element from a group at layers panel by drag 2022-09-28 09:47:11 +02:00
Andrey Antukh
84655c0fa3 🐛 Fix content-length handling on exporter 2022-09-28 09:45:43 +02:00
Andrey Antukh
1dc493c2d5 🔥 Remove assets handler code for :db backend 2022-09-28 09:45:43 +02:00
Andrey Antukh
2753a934aa 📎 Add service result wrapper
Allows attach metadata to values that does not implement the IObj
clojure interface.
2022-09-28 09:45:43 +02:00
Pablo Alba
47363d96f1 Improve invitation token validation 2022-09-28 09:45:43 +02:00
Andrey Antukh
b74631bf4a ⬆️ Update shadow-cljs on exporter 2022-09-28 09:45:43 +02:00
Andrey Antukh
99a718e407 🎉 Add openjdk19 on devenv 2022-09-28 09:45:43 +02:00
Andrey Antukh
8bdfd188d8 ⬆️ Upgrade shadow-cljs and rumext dependency 2022-09-28 09:45:43 +02:00
Andrey Antukh
278f6685b6 Improve object deletion process on profile deletion 2022-09-28 09:45:43 +02:00
Andrey Antukh
06bce92cdc 📎 Fix linter issues on backend 2022-09-28 09:45:43 +02:00
Andrey Antukh
757cee67fb 🎉 Add the ability to completly block access to a profile 2022-09-28 09:45:43 +02:00
Andrey Antukh
37e2fe5c65 Allow repeated registers after small delay
Helps users with expired tokens proceed with a new register
2022-09-28 09:45:43 +02:00
Andrey Antukh
395a7096bf Minor improvements on error report template 2022-09-28 09:45:43 +02:00
Andrey Antukh
65afa2a833 ⬆️ Update dependencies 2022-09-28 09:45:43 +02:00
Andrey Antukh
041ecf67fe 🔥 Remove sentry from codebase 2022-09-28 09:45:43 +02:00
Alejandro Alonso
6ac1d47de1 🐛 Fix Internal error when resending invitation email 2022-09-28 08:43:48 +02:00
Alejandro
dfae7d30a1 Merge pull request #2362 from penpot/superalex-fix-sort-anidated-objects
🐛 Fix cannot take out an element from a group at layers panel by …
2022-09-27 14:10:24 +02:00
Alejandro Alonso
d8f1df0142 🐛 Fix cannot take out an element from a group at layers panel by drag 2022-09-27 14:04:48 +02:00
Alejandro
a471d96b53 Merge pull request #2361 from penpot/niwinz-hotfix-exporter-content-length
🐛 Fix content-lenght calculation on exporter response
2022-09-27 13:22:31 +02:00
Andrey Antukh
51307cdf8d 🐛 Fix content-lenght calculation on exporter response 2022-09-27 13:11:17 +02:00
Alejandro Alonso
bc55268a17 📎 Update CHANGES.md file 2022-09-27 12:57:36 +02:00
Alejandro Alonso
0bc24bb6eb Merge remote-tracking branch 'origin/staging' into develop 2022-09-27 12:42:08 +02:00
Alejandro
1be1e94869 Merge pull request #2344 from penpot/eva-fix-layers
🐛 Fix delete layers in bulk
2022-09-27 12:30:15 +02:00
Eva
3ea3ca3bd9 🐛 Fix delete layers in bulk 2022-09-27 12:24:32 +02:00
Eva Marco
6c09ecbef5 Merge pull request #2329 from penpot/superalex-fix-bounding-box-mask
🐛 Fix bounding box mask
2022-09-27 12:17:28 +02:00
Alejandro Alonso
e888b06ec4 Merge remote-tracking branch 'origin/staging' into develop 2022-09-27 09:50:37 +02:00
Alejandro Alonso
c1a4ae9d36 Merge remote-tracking branch 'origin/staging' 2022-09-27 09:50:23 +02:00
Alejandro
1a9fbee412 Merge pull request #2352 from penpot/niwinz-mtype-fix
 Ignore S3 object metadata and use our own content-type header
2022-09-27 09:49:10 +02:00
Andrey Antukh
4909d6574e 📎 Remove unnecesary logging on ws io exception 2022-09-27 07:28:29 +02:00
Andrey Antukh
577db35777 Merge pull request #2349 from penpot/palba-add-thankyou
📎 Add THANKYOU file
2022-09-27 07:17:12 +02:00
Pablo Alba
f9187cd202 📎 Add THANKYOU file 2022-09-27 07:12:39 +02:00
Andrey Antukh
a868840132 Ignore S3 object metadata and use our own content-type header 2022-09-27 00:47:09 +02:00
Alejandro Alonso
33a8c47f6e Merge remote-tracking branch 'origin/staging' into develop 2022-09-26 16:50:41 +02:00
Alejandro Alonso
e80ad112b8 Merge remote-tracking branch 'origin/staging' 2022-09-26 16:50:29 +02:00
Alejandro
07601975ac Merge pull request #2347 from penpot/superalex-fix-remove-woff2-from-template
🐛 Fix woff2 reference in font template
2022-09-26 16:50:06 +02:00
Alejandro Alonso
c709505733 🐛 Fix woff2 reference in font template 2022-09-26 16:45:52 +02:00
Alejandro Alonso
c3f0657652 Merge remote-tracking branch 'origin/staging' into develop 2022-09-26 15:56:21 +02:00
Alejandro Alonso
853b78613d Merge remote-tracking branch 'origin/staging' 2022-09-26 15:31:40 +02:00
Andrey Antukh
0b4a1553b9 Merge pull request #2342 from penpot/superalex-disable-woff2-compress
🐛 Fix woff2 generation
2022-09-26 15:30:32 +02:00
Alejandro Alonso
f67c4ddca0 🐛 Fix woff2 generation 2022-09-26 15:25:42 +02:00
Alejandro
bc693ad1bb Merge pull request #2345 from penpot/niwinz-hash-and-dblock
🎉 Add better hashing approach for database locking on Uuids
2022-09-26 15:24:13 +02:00
Andrey Antukh
fad2e51cbe 🎉 Add better hashing approach for database locking on Uuids 2022-09-26 14:22:52 +02:00
Eva Marco
61d1a3a77b Merge pull request #2343 from penpot/palba-fix-ungroup-typography
🐛 Fix ungroup does not work for typographies
2022-09-26 13:13:05 +02:00
Pablo Alba
d060ddaeae 🐛 Fix ungroup does not work for typographies 2022-09-26 09:45:33 +02:00
Alejandro Alonso
d2d7803186 🐛 Fix export bounding box mask 2022-09-23 14:07:04 +02:00
Eva Marco
38468d7584 Merge pull request #2327 from penpot/superalex-hotfixes
🐛 Hotfixes
2022-09-23 12:09:41 +02:00
Alejandro Alonso
a9e8f4eb67 🐛 Fix Terms and Privacy links overlapping 2022-09-23 09:46:39 +02:00
Alejandro Alonso
944cfd0fc4 🐛 Fix font search works only with lowercase letters 2022-09-23 09:46:39 +02:00
Alejandro Alonso
1ef4d42b28 🐛 Fix error after user drags layers in search functionality 2022-09-23 09:46:39 +02:00
Alejandro Alonso
441e9627b5 🐛 Fix Wrong shortcut button tip of Delete function 2022-09-23 09:46:39 +02:00
Alejandro Alonso
5d01a0e24c 🐛 Fix import files with unexpected format or invalid content 2022-09-23 09:46:39 +02:00
Alejandro
b2d0f3cac2 Merge pull request #2324 from penpot/eva-fix-library-colors
🐛 Fix library color selector
2022-09-22 16:48:48 +02:00
Eva
79e35e2608 🐛 Fix precision 2022-09-22 15:32:15 +02:00
Eva
6e33d5b311 🐛 Fix library color selector 2022-09-22 15:32:15 +02:00
Andrey Antukh
e1b62805e5 Merge pull request #2323 from 5idereal/patch-1
🐛 Fix language name
2022-09-22 10:59:09 +02:00
5idereal
5cff6eb592 🐛 Fix language name 2022-09-22 16:54:21 +08:00
Eva
f3115f8f3a 🐛 Remove gitter information from feedback page 2022-09-21 15:21:17 +02:00
Alejandro Alonso
f7cfb5708f Merge remote-tracking branch 'origin/staging' 2022-09-21 14:06:38 +02:00
Alejandro
e75c9df17e Merge pull request #2316 from penpot/superalex-fix-colors
🐛 Fix several issues in colors #2303
2022-09-21 14:06:18 +02:00
Alejandro Alonso
dfc1b03a60 📎 Update CHANGES.md file 2022-09-21 13:59:12 +02:00
Alejandro Alonso
726baefa25 🐛 Fix add to recent colors when changing fill or opacity manually 2022-09-21 13:54:34 +02:00
Alejandro Alonso
3063725a62 🐛 Fix color type icon doesn't change 2022-09-21 13:54:25 +02:00
Alejandro Alonso
aed065eec1 🐛 Fix using gradient for shadow fill 2022-09-21 13:54:25 +02:00
Eva
4961991e18 🐛 Fix gradient colors in recents 2022-09-21 13:54:25 +02:00
Eva
199142045f 🐛 Remove bugged colors from recents 2022-09-21 13:54:25 +02:00
Eva
f444d3d01d 🐛 Fix opacity in color picker 2022-09-21 13:54:25 +02:00
Eva
bea96cb586 🐛 Fix recent colors 2022-09-21 13:54:25 +02:00
Alejandro
cc18f84d62 Merge pull request #2302 from penpot/niwinz-hot-improvements
Enhancements
2022-09-21 10:01:31 +02:00
Alejandro
ac75d0cc1b Merge pull request #2310 from penpot/niwinz-onboarding-form-load-improvements
 Improve error handling on onboarding questions form
2022-09-21 09:46:58 +02:00
Andrey Antukh
21683be07b Improve error handling on onboarding questions form 2022-09-21 09:44:08 +02:00
Andrey Antukh
5ac123dc4b Improve error handling on onboarding questions form 2022-09-21 09:35:29 +02:00
Andrey Antukh
ec53288b66 Improve test runner 2022-09-21 09:29:48 +02:00
Andrey Antukh
2348146f00 🎉 Add 'email-verification' flag enabled by default
The main idea is deprecating the `insecure-register` flag with the more
general `email-verification` flag.
2022-09-21 09:29:48 +02:00
Andrey Antukh
41134f22e9 📎 Update license header 2022-09-20 23:23:22 +02:00
Andrey Antukh
9bfdcc6277 Make the task retry algorithm use better backoff values 2022-09-20 23:04:37 +02:00
Andrey Antukh
d3347a1be0 Allow floats on db/interval constructor 2022-09-20 23:04:37 +02:00
Andrey Antukh
ef2918a115 Minor change on how service middleware are applied 2022-09-20 23:04:37 +02:00
Andrey Antukh
92d3015d24 Reset the recovery request form on submit 2022-09-20 23:04:37 +02:00
Andrey Antukh
c4aba025c4 Add some srepl helpers for resend email verification 2022-09-20 23:04:37 +02:00
Andrey Antukh
3aac620276 Merge pull request #2298 from penpot/Waishnav-button-gap-fix
 UI improvements in Project section
2022-09-20 17:12:47 +02:00
Andrey Antukh
3c2ba92f6c 🎉 Add srepl helper for sending test email 2022-09-20 13:41:40 +02:00
Andrey Antukh
58319d84ad ♻️ Refactor email namespaces 2022-09-20 13:41:18 +02:00
Andrey Antukh
87691499d7 🐛 Add missing enable-smtp flag on devenv scripts 2022-09-20 13:40:32 +02:00
Andrey Antukh
e0112ac3a3 🐛 Fix worker startup on dev REPL 2022-09-20 13:39:47 +02:00
Eva Marco
ee8b5cc1c5 Merge pull request #2300 from penpot/superalex-fix-change-multiple-colors-with-svg
🐛 Fix change multiple colors with svg
2022-09-20 12:11:05 +02:00
Alejandro Alonso
c638ab459f Merge remote-tracking branch 'origin/staging' 2022-09-20 12:05:00 +02:00
Alejandro Alonso
345ae020d6 📎 Update version.txt file 2022-09-20 11:52:36 +02:00
Alejandro Alonso
b6b800a8e2 📎 Update CHANGES.md file 2022-09-20 11:44:56 +02:00
Waishnav
eeb8d284cc UI improvements in Project section div 2022-09-20 11:44:56 +02:00
Alejandro Alonso
00222499cc 🐛 Fix files translation 2022-09-20 11:43:41 +02:00
Andrés Moya
c6067ce336 🐛 Only dangling reset is unavailable, other cases do work 2022-09-20 11:36:32 +02:00
Andrey Antukh
6f42f4ec45 ♻️ Refactor semaphore and executors 2022-09-20 11:32:45 +02:00
Andrey Antukh
12b98c22bc Increase the default db pool size to 60 2022-09-20 11:32:45 +02:00
Andrey Antukh
435c627afd Make the audit log gc more agressive 2022-09-20 11:32:45 +02:00
Alejandro Alonso
4de579f861 🐛 Fix change multiple colors with svg 2022-09-20 11:20:38 +02:00
Andrey Antukh
978b309b04 🐛 Fix issues introduced in previous merge 2022-09-20 08:58:57 +02:00
Andrey Antukh
32b8c17dad Merge remote-tracking branch 'origin/staging' into develop 2022-09-20 08:48:55 +02:00
Andrey Antukh
c6e33fa9bc Merge pull request #2260 from penpot/eva-layout-effect
🐛 Fix layout effects in viewer
2022-09-20 07:43:56 +02:00
Alejandro
69c2d95768 Merge pull request #2258 from penpot/alotor-hotfixes
Alotor hotfixes
2022-09-20 07:37:36 +02:00
Wojciech Maj
80cfe6df9d 🎉 Add microsoft variant of woff mimetype on the fonts selection dialog 2022-09-20 07:37:12 +02:00
Martial Garchery
29550add6c 📚 Fix readme typo 2022-09-20 07:34:27 +02:00
Alejandro
780b833a67 Merge pull request #2290 from penpot/superalex-fix-instagram-link
🐛 Fix instagram link
2022-09-19 08:49:37 +02:00
Alejandro Alonso
879041b0bc 🐛 Fix instagram link 2022-09-19 08:44:02 +02:00
Pablo Ruiz Múzquiz
48118a0ff4 Readme 2.0 (#2262)
* Change nav and new intro text

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* minor updates

Co-authored-by: elhombretecla <delacruzgarciajuan@gmail.com>
2022-09-15 17:06:26 +02:00
Eva
843e1e91c2 " Add new context menu options for components" 2022-09-14 15:05:46 +02:00
alonso.torres
ddf8aaf68f 🐛 Fix problem when moving shapes inside nested frames 2022-09-14 13:12:16 +02:00
Eva
ffaf5d835d 🐛 Fix layout effects in viewer 2022-09-14 13:09:43 +02:00
Andrey Antukh
f94571b3b4 Merge pull request #2256 from penpot/palba-newsletter-subscription
🎉 Newsletter Opt-in options for subscription categories
2022-09-14 12:47:19 +02:00
Andrey Antukh
7832a80f82 Merge pull request #2243 from penpot/add-shortcut-for-clear-undo
🐛 Change shortcut for "Clear undo"
2022-09-14 12:46:27 +02:00
Andrey Antukh
16a0af802a Merge branch 'staging' into add-shortcut-for-clear-undo 2022-09-14 12:45:42 +02:00
alonso.torres
9cb6e71258 🐛 Fix issue when scaling to value 0 2022-09-14 12:28:10 +02:00
alonso.torres
c9c2f9e40f Revert "🐛 Fix weird text align"
This reverts commit 726f55bd04.
2022-09-14 12:28:10 +02:00
alonso.torres
bdd487adc0 🐛 Fix problem with snap to grids 2022-09-14 12:28:10 +02:00
Andrey Antukh
8e2ccfb4b0 Revert some changes from previous merge 2022-09-14 11:15:29 +02:00
Andrey Antukh
6067498570 ⬆️ Update aws/s3 dependency 2022-09-14 09:26:36 +02:00
Andrey Antukh
3cd9a3254d 🎉 Add /readyz http endpoint for backend & exporter 2022-09-14 09:26:26 +02:00
Andrey Antukh
4af851d4c6 Merge branch 'staging' into develop 2022-09-14 09:16:00 +02:00
Andrey Antukh
8fa49eada8 Merge pull request #2225 from penpot/superalex-viwer-performance-degradation-because-of-fixed-position
🎉 Improve viewer performance degradation because of fixed position
2022-09-14 08:30:53 +02:00
Pablo Alba
f921085c72 🎉 Newsletter Opt-in options for subscription categories 2022-09-13 17:10:23 +02:00
Andrey Antukh
e58c943f41 ⬆️ Update aws/s3 dependency 2022-09-13 13:25:22 +02:00
Alejandro
c43e8bda3c Merge pull request #2255 from penpot/niwinz-healtz-endpoint
🎉 Add /readyz http endpoint for backend & exporter
2022-09-13 11:22:25 +02:00
Andrey Antukh
028e0c5b70 🎉 Add /readyz http endpoint for backend & exporter 2022-09-13 11:03:41 +02:00
Alejandro Alonso
108cdcecbb 🎉 Improve viewer performance degradation because of fixed position 2022-09-13 10:10:50 +02:00
Alejandro
36f30c611e Merge pull request #2244 from penpot/niwinz-docker-devenv-aarch64
🎉 Make the devenv docker image multiplatform
2022-09-12 15:49:25 +02:00
Andrey Antukh
172a39c2e2 Improve storage sharding replacing uuid/next with uuid/random 2022-09-12 15:31:14 +02:00
Andrés Moya
f736ec813e 🐛 Fix origin-frame calculation 2022-09-12 11:52:32 +02:00
Eva
7618fcade0 🐛 Fix several transitions on same frame 2022-09-12 11:52:32 +02:00
Eva
7599b7abc6 🐛 Fix overlay close when click 2022-09-12 11:52:32 +02:00
Eva
67cbfc631d 🐛 Fix toggle overlay position 2022-09-12 11:52:32 +02:00
Eva
dc6afb46bf 🐛 Fix overlay remain open on frame change 2022-09-12 11:52:32 +02:00
Alejandro
f98512242a Merge pull request #2247 from penpot/palba-select-children-on-ungrouping
🐛 Fix when ungrouping, the items previously grouped should ALWAYS…
2022-09-12 08:27:14 +02:00
Pablo Alba
8b29767932 🐛 Fix items selected while ungrouping 2022-09-12 08:26:32 +02:00
Alejandro
b5b042e6e4 Merge pull request #2242 from penpot/palba-fix-move-layers
🐛 Fix Move layers fails (sometimes)
2022-09-12 06:50:29 +02:00
Alejandro Alonso
726f55bd04 🐛 Fix weird text align 2022-09-09 12:30:23 +02:00
Andrey Antukh
f86f93deea Make the rate limit configuration automatically reloadable 2022-09-09 12:19:49 +02:00
Andrey Antukh
e657c1bbfa Start use datoteka.io ns 2022-09-09 12:19:49 +02:00
Andrey Antukh
b5e26fe615 🎉 Make the devenv docker image multiplatform
Add support for aarch64
2022-09-08 16:27:40 +02:00
Pablo Alba
73d2aad4db 🐛 Change shortcut for "Clear undo" 2022-09-08 16:24:37 +02:00
Pablo Alba
27aa20f00b 🐛 Fix Move layers fails (sometimes) 2022-09-08 10:27:43 +02:00
Alejandro
643e58c61b Merge pull request #2233 from penpot/palba-fix-grouping-typographies
🐛 Fix Grouping typographies by drag & drop does not work (again)
2022-09-08 08:27:26 +02:00
Pablo Alba
f7aba14f76 bug: Fix Grouping typographies by drag & drop does not work (again) 2022-09-08 08:05:06 +02:00
Alejandro
19a9440f11 Merge pull request #2240 from penpot/palba-fix-undo-move-layers
🐛 Fix undo after moving layers will wrongly order the layers
2022-09-08 08:01:27 +02:00
Alejandro
1e2d100c81 Merge pull request #2236 from penpot/niwinz-minor-improvements
Minor improvements
2022-09-08 07:21:06 +02:00
Andrey Antukh
675a07bac6 🔥 Remove internal file data blob format v2 2022-09-08 07:16:44 +02:00
Pablo Alba
21ec8bfdac 🐛 Fix undo after moving layers will wrongly order the layers 2022-09-07 16:57:22 +02:00
Alejandro Alonso
de57300fe3 Merge remote-tracking branch 'origin/staging' into develop 2022-09-07 12:00:56 +02:00
Alejandro Alonso
2da6732aba Merge remote-tracking branch 'origin/staging' 2022-09-07 11:58:57 +02:00
Alejandro Alonso
be18defcb1 📎 Update version.txt file 2022-09-07 11:58:47 +02:00
Alejandro Alonso
52344fdb18 Merge remote-tracking branch 'origin/staging' into develop 2022-09-07 11:39:01 +02:00
Alejandro Alonso
d632ca3114 Merge remote-tracking branch 'origin/staging' 2022-09-07 11:38:43 +02:00
Alejandro
c19237b45a Merge pull request #2238 from penpot/alotor-bug-texts
🐛 Fix problem with texts for non existing fonts
2022-09-07 11:28:21 +02:00
alonso.torres
c47f5ca186 🐛 Fix problem with texts for non existing fonts 2022-09-07 11:12:30 +02:00
Alejandro Alonso
fb8543c4e4 Merge remote-tracking branch 'origin/staging' into develop 2022-09-07 06:32:29 +02:00
Andrey Antukh
e0ac583aba 📎 Improve analyze-files helper on srepl ns 2022-09-06 16:53:28 +02:00
Andrey Antukh
5cd0079e7f 📎 Add some utilities on user ns 2022-09-06 16:52:51 +02:00
Andrey Antukh
00a7760c0f Change default task schedule 2022-09-06 12:30:23 +02:00
Alejandro
89732d911b Merge pull request #2234 from penpot/niwinz-tasks-schedule-change
 Change default task schedule
2022-09-06 12:28:58 +02:00
Andrey Antukh
c184ab58a3 Change default task schedule 2022-09-06 12:17:28 +02:00
Andrey Antukh
182b572550 Merge pull request #2232 from penpot/alotor-ungroup-frames
 Ungroup frames
2022-09-06 11:59:37 +02:00
Andrey Antukh
f394e8dba3 📎 Minor fix on i18n ns 2022-09-06 11:13:01 +02:00
Andrey Antukh
5bcf5ff4bc 🎉 Add it and eu languages to the i18n subsystem 2022-09-06 11:11:30 +02:00
Alejandro
d41c2388c1 Merge pull request #2229 from penpot/niwinz-msgbus-improvements
 Improve msgbus internal API
2022-09-06 10:50:42 +02:00
Alejandro
0155ef80b2 Merge pull request #2222 from penpot/niwinz-uuidv8-improvements
 Improve UUIDv8 implementation
2022-09-06 10:44:36 +02:00
Alejandro
ad32512980 Merge pull request #2223 from penpot/niwinz-public-uri-improvements
 Improve public-uri handling
2022-09-06 10:43:30 +02:00
Andrey Antukh
d082ff0a2b Improve UUIDv8 implementation 2022-09-06 10:37:23 +02:00
alonso.torres
c0fc68b9f0 Ungroup frames 2022-09-06 09:31:01 +02:00
Andrey Antukh
82032bedf5 Sort & validate translation files 2022-09-06 08:40:18 +02:00
Andrey Antukh
915d4249a0 Merge branch 'translations' into develop 2022-09-06 08:15:50 +02:00
Andrey Antukh
004334a7c8 Merge remote-tracking branch 'weblate/develop' into translations 2022-09-06 08:15:24 +02:00
Valentina Chapellu
97d5f48ab5 🌐 Add translations for: Italian.
Currently translated at 33.2% (388 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2022-09-06 08:05:28 +02:00
Stas Haas
0155c6c5c4 🌐 Add translations for: German.
Currently translated at 93.9% (1095 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-09-06 08:05:27 +02:00
Andrey Antukh
45adc8a61d Merge pull request #2228 from penpot/hiru-check-v2-viewer
 Disallow to view a file with components-v2
2022-09-05 19:09:01 +02:00
Andrey Antukh
a555e13b6a Improve msgbus internal API 2022-09-05 19:08:26 +02:00
Andrés Moya
f1b536034a Show error instead of dialog 2022-09-05 16:16:24 +02:00
Andrey Antukh
6018df480e Merge pull request #2227 from penpot/palba-fix-colorpicker
🐛 Fix color-picker recent colors
2022-09-05 15:54:55 +02:00
Andrés Moya
3a6876eeec Disallow to view a file with components-v2 2022-09-05 15:53:19 +02:00
Pablo Alba
7e4b7424a5 🐛 Fix color-picker recent colors 2022-09-05 15:38:37 +02:00
Andrey Antukh
ce7eed5ea0 🐛 Fix issues on deleting library which is in use by deleted files 2022-09-05 14:58:05 +02:00
Andrey Antukh
11018581ed Merge pull request #2201 from penpot/hiru-undelete-components
🎉 Allow to restore deleted components
2022-09-05 14:26:14 +02:00
Andrey Antukh
3aa25e7a90 Merge pull request #2224 from penpot/eva-fix-grow-type
🐛 Fix default grow type in texts
2022-09-05 14:19:06 +02:00
Rubén
302c135d51 🌐 Add translations for: Catalan.
Currently translated at 99.4% (1160 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2022-09-05 14:17:52 +02:00
Alejandro Alonso
9c68432936 Merge remote-tracking branch 'origin/staging' into develop 2022-09-05 12:13:08 +02:00
Alejandro Alonso
851092fc9e Merge remote-tracking branch 'origin/staging' 2022-09-05 11:57:43 +02:00
Eva
c69cb20be1 🐛 Fix default grow type in texts 2022-09-05 11:17:51 +02:00
Andrey Antukh
a1fccd46ff Improve public-uri handling
This enables use penpot under subdirectory
2022-09-05 09:41:19 +02:00
Valentina Chapellu
d75648e6b0 🌐 Add translations for: Italian.
Currently translated at 22.8% (266 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2022-09-03 10:15:37 +02:00
liimee
179a77eb05 🌐 Add translations for: Indonesian.
Currently translated at 6.7% (79 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2022-09-03 10:15:36 +02:00
Stas Haas
352c044aad 🌐 Add translations for: German.
Currently translated at 92.7% (1081 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-09-03 10:15:35 +02:00
Aimee
5503e371aa 🌐 Add translations for: French.
Currently translated at 82.5% (962 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-09-03 10:15:34 +02:00
Andrés Moya
46053b6bbf 🎉 Import & export new components 2022-09-02 13:43:01 +02:00
Andrés Moya
251e7eada2 🎉 Allow to restore deleted components 2022-09-02 13:43:00 +02:00
Andrey Antukh
7e58e2f5eb Merge pull request #2217 from penpot/alotor-fix-nested-boards
Fixes on nested artboards and texts
2022-09-01 13:40:54 +02:00
alonso.torres
04d6e76c6c 🐛 Fix problem editing rotated texts 2022-09-01 12:56:01 +02:00
Alejandro
ee1058950e Merge pull request #2214 from penpot/palba-fix-undo-delete-page
🐛 Fix undo on delete page does not preserve its order
2022-09-01 08:16:11 +02:00
Pablo Alba
be656bb4ef 🐛 Fix undo on delete page does not preserve its order 2022-09-01 07:57:59 +02:00
alonso.torres
d6317297d7 🐛 Fix error when moving nested frames outside 2022-08-31 16:30:30 +02:00
alonso.torres
5820f73b6e 🐛 Fix problem when hovering over nested frames 2022-08-31 16:29:38 +02:00
Andrey Antukh
89e5607d7f Merge pull request #2211 from penpot/niwinz-rate-limit
Rate Limit for RPC methods
2022-08-31 13:19:34 +02:00
Pablo Alba
2ecf33d7bb Merge pull request #2213 from penpot/superalex-fix-export-simple-path
🐛 Fix export simple path
2022-08-31 13:17:58 +02:00
Andrey Antukh
2f21560fe3 🎉 Add improved approach for async flow time measurements 2022-08-31 12:55:48 +02:00
Andrey Antukh
fd973d87fd ♻️ Refactor metrics namespace 2022-08-31 12:55:48 +02:00
Andrey Antukh
ec3651d85b 🎉 Add optional rate limit support for RPC calls 2022-08-31 12:55:48 +02:00
Alejandro Alonso
8f98b81829 🐛 Fix export simple path 2022-08-31 12:47:46 +02:00
Andrey Antukh
469704def6 Merge pull request #2212 from penpot/palba-fix-move-artboards-on-comments
🐛 Fix artboards moving with comment tool selected
2022-08-31 12:07:18 +02:00
Pablo Alba
3cbb2defb3 🐛 Fix artboards moving with comment tool selected 2022-08-31 11:59:17 +02:00
Andrey Antukh
47b745592b ⬆️ Update shadow-cljs on frontend 2022-08-31 11:01:07 +02:00
Andrey Antukh
819492f453 Remove release build warnings 2022-08-31 11:01:07 +02:00
Andrey Antukh
83905c2f56 📎 Minor change on common repl script 2022-08-31 11:01:07 +02:00
Andrey Antukh
d6d9d25fce ♻️ Refactor token generation API 2022-08-31 11:01:07 +02:00
Alejandro Alonso
44f4d9c50c Merge remote-tracking branch 'origin/staging' into develop 2022-08-31 08:58:27 +02:00
Alejandro Alonso
2cb8e7b986 Merge remote-tracking branch 'origin/staging' 2022-08-31 08:56:54 +02:00
Alejandro Alonso
8314e6c17b 📎 Update version.txt file 2022-08-31 08:56:38 +02:00
Alejandro
84d85edc0b Merge pull request #2208 from penpot/alotor-bugfixes
🐛 Fix problem with multi-user text editing
2022-08-31 08:40:13 +02:00
Andrey Antukh
1884a8abe6 Merge pull request #2209 from penpot/palba-protect-profile-url
🐛 Fix opening profile URL while signed out takes to "your account" section
2022-08-30 18:48:30 +02:00
Pablo Alba
c54354f143 🐛 Fix opening profile URL while signed out takes to "your account" section 2022-08-30 17:51:41 +02:00
alonso.torres
29f1c8bb4d 🐛 Fix frame titles deforming when resize 2022-08-30 17:12:43 +02:00
alonso.torres
a301856c0d 🐛 Fix path tools blocking elements underneath 2022-08-30 15:47:45 +02:00
alonso.torres
4e6a5ffa69 🐛 Fix problem with multi-user text editing 2022-08-30 15:08:55 +02:00
Alejandro Alonso
199541aeee Merge remote-tracking branch 'origin/staging' into develop 2022-08-30 13:51:08 +02:00
Alejandro Alonso
0268e8594d Merge remote-tracking branch 'origin/staging' 2022-08-30 13:50:35 +02:00
Alejandro Alonso
9f1540cd00 📎 Update version.txt file 2022-08-30 13:50:16 +02:00
Alejandro Alonso
28a721ce9c Merge remote-tracking branch 'origin/staging' into develop 2022-08-30 13:31:11 +02:00
Alejandro Alonso
1e62b72769 Merge remote-tracking branch 'origin/staging' 2022-08-30 13:29:51 +02:00
Alejandro
ab94968648 Merge pull request #2206 from penpot/palba-fix-color-picker
🐛 Fix on color-picker, click+drag adds lots of recent colors
2022-08-30 13:29:23 +02:00
Alejandro
4ba5be4450 Merge pull request #2193 from penpot/palba-avoid-bring-file-libraries-on-export
🐛 Fix bringing complete file data when launching the export dialog
2022-08-30 13:28:57 +02:00
Pablo Alba
1bb83b3019 🐛 Fix bringing complete file data when launching the export dialog 2022-08-30 13:18:40 +02:00
Pablo Alba
d175c96871 🐛 Fix on color-picker, click+drag adds lots of recent colors 2022-08-30 13:14:57 +02:00
Alejandro Alonso
24a56f029a Merge remote-tracking branch 'origin/staging' into develop 2022-08-30 13:09:02 +02:00
Alejandro Alonso
a2d368636b Merge remote-tracking branch 'origin/staging' 2022-08-30 13:03:30 +02:00
Andrey Antukh
ecfc20f514 Merge pull request #2205 from penpot/superalex-fix-jittering-on-firefox-scroll-fixed-elements
🐛 Fix jittering on firefox scroll fixed elements
2022-08-30 12:32:42 +02:00
Alejandro Alonso
c69bf9fd35 🐛 Fix jittering on firefox scroll fixed elements 2022-08-30 12:26:56 +02:00
Alejandro
77118a3cc7 Merge pull request #2204 from penpot/alotor-bugfixes
🐛 Fix problems with texts
2022-08-30 12:23:16 +02:00
alonso.torres
282941d284 🐛 Fix problems with texts 2022-08-30 12:12:32 +02:00
Alejandro
d034b61318 Merge pull request #2199 from penpot/palba-shadow-nested-artboard
🐛 Fix Shadows doesn't work on nested artboards
2022-08-30 11:58:04 +02:00
Alejandro
02f3809b89 Merge pull request #2175 from penpot/hiru-dashboard-libraries
🎉 Add new dashboard libraries screen
2022-08-30 11:32:44 +02:00
Pablo Alba
1c033fd9f6 Merge pull request #2200 from penpot/alotor-fix-selection
🐛 Fix problems with double-click and selection
2022-08-29 13:45:31 +02:00
alonso.torres
40130d1ca7 🐛 Fix problems with double-click and selection 2022-08-29 12:27:43 +02:00
Pablo Alba
5376c4aa23 🐛 Fix Shadows doesn't work on nested artboards 2022-08-29 12:18:56 +02:00
Stas Haas
3c759a46ec 🌐 Add translations for: German.
Currently translated at 80.5% (939 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-08-29 10:20:04 +02:00
Andrés Moya
1675d945d9 Add some small enhancements 2022-08-29 09:50:04 +02:00
Andrey Antukh
12ba46642c Merge pull request #2198 from Windfarer/fix-devenv-nginx
🐛 Fix nginx user for devenv
2022-08-29 07:24:09 +02:00
Qizhao Yang
acb9432f61 🐛 Fix nginx user for devenv
Signed-off-by: Qizhao Yang <windfarer@gmail.com>
2022-08-29 09:32:06 +08:00
Mikel Larreategi
ff3b6fc0c8 🌐 Add translations for: Basque.
Currently translated at 100.0% (1166 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2022-08-28 23:15:03 +02:00
Amine Gdoura
60d8486f24 🌐 Add translations for: Arabic.
Currently translated at 47.2% (551 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2022-08-28 23:15:01 +02:00
Stas Haas
99050af903 🌐 Add translations for: German.
Currently translated at 80.2% (936 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-08-28 23:15:00 +02:00
Rubén
c488efa515 🌐 Add translations for: Catalan.
Currently translated at 98.3% (1147 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2022-08-28 23:15:00 +02:00
Alejandro
396d35840e Merge pull request #2189 from penpot/palba-componentsv2-publish-from-popup
🎉 Publish file library from libraries popup
2022-08-26 13:30:25 +02:00
Pablo Alba
d1550ebb2a 🎉 Publish file library from libraries popup 2022-08-26 13:14:27 +02:00
Alejandro
edc88458d3 Merge pull request #2191 from penpot/palba-componentsv2-library-name-assets-panel
🎉 Library name in assets panel
2022-08-26 13:06:39 +02:00
Mikel Larreategi
517d47f016 🌐 Add translations for: Basque.
Currently translated at 60.8% (709 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2022-08-25 17:42:32 +02:00
Mikel Larreategi
70f6a6cecc 🌐 Add translations for: Basque.
Currently translated at 4.3% (51 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2022-08-25 15:21:25 +02:00
Ahmad HosseinBor
07ce252d60 🌐 Add translations for: Persian.
Currently translated at 55.8% (651 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-08-25 15:21:24 +02:00
Andrey Antukh
a055a31286 Merge remote-tracking branch 'origin/staging' into develop 2022-08-25 14:51:58 +02:00
Alejandro Alonso
1b33b0dcef Merge remote-tracking branch 'origin/staging' 2022-08-25 13:55:07 +02:00
Pablo Alba
01d99222e0 Merge pull request #2192 from penpot/superalex-fix-viewer-scroll-problems
🐛 Fix viewer scroll problems
2022-08-25 12:09:11 +02:00
Andrey Antukh
85ec1668f3 🐛 Add missing rpc-command definition on metrics 2022-08-25 11:47:14 +02:00
Alejandro Alonso
a1654aeb0e 🐛 Fix viewer scroll problems 2022-08-25 09:12:50 +02:00
Pablo Alba
00e9195af8 🎉 Library name in assets panel 2022-08-24 14:41:39 +02:00
Alejandro Alonso
f7186fa781 Merge remote-tracking branch 'origin/staging' 2022-08-24 14:27:02 +02:00
Ahmad HosseinBor
2974125e8f 🌐 Add translations for: Persian.
Currently translated at 51.5% (601 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-08-24 14:18:24 +02:00
Andrey Antukh
e705a333a9 Merge pull request #2190 from penpot/superalex-fix-drag-and-drop-boards
🐛 Fix drag and drop boards
2022-08-24 14:14:17 +02:00
Alejandro Alonso
08ccd7be70 🐛 Fix drag and drop boards 2022-08-24 14:03:38 +02:00
Pablo Alba
aa4344a76f 🐛 Fix drag and drop graphic assets in groups 2022-08-24 13:15:59 +02:00
Alejandro Alonso
581843f99b Merge remote-tracking branch 'origin/staging' 2022-08-24 12:18:41 +02:00
Andrey Antukh
02efffceb4 Merge pull request #2188 from penpot/superalex-fix-permissions-when-moving-comments
🐛 Fix permissions when moving comments
2022-08-24 12:17:57 +02:00
Alejandro Alonso
44330ffb3b 🐛 Fix permissions when moving comments 2022-08-24 12:16:54 +02:00
Alejandro Alonso
8a33a63f91 🐛 Fix permissions when moving comments 2022-08-24 12:08:38 +02:00
Pablo Alba
3c492f03d1 Merge pull request #2181 from penpot/hiru-select-instance
🎉 Select main instance when selecting a component
2022-08-24 11:18:21 +02:00
Andrés Moya
e8990caefb 🎉 Select main instance when selecting a component 2022-08-24 11:17:26 +02:00
Alejandro Alonso
d2cd29bf76 Merge remote-tracking branch 'origin/staging' 2022-08-24 11:10:08 +02:00
Andrés Moya
c1942ef408 💄 Change libraries label 2022-08-24 11:07:53 +02:00
Alejandro
35c1008b37 Merge pull request #2187 from penpot/niwinz-viewer-comments-positioning-bug
Viewer comments positioning regression
2022-08-24 11:07:42 +02:00
Andrés Moya
b072c1d1d1 🐛 Fix delete library color 2022-08-24 11:00:26 +02:00
Andrés Moya
5a0ec9525b 🎉 Add new dashboard libraries screen 2022-08-24 11:00:26 +02:00
Andrey Antukh
8ce8b3fdef 📎 Update docker images related files 2022-08-24 10:59:56 +02:00
Andrés Moya
f0e521b8d5 🐛 Fix compilation of exporter 2022-08-24 10:59:53 +02:00
Andrey Antukh
be1c19e718 🐛 Fix comments positioning on viewer (regression) 2022-08-24 10:59:38 +02:00
Andrey Antukh
1e9fb6e391 Merge pull request #2186 from penpot/superalex-fix-permissions-when-moving-comments
🐛 fix permissions when moving comments
2022-08-24 10:42:00 +02:00
Eva
8dfd74547a 💄 Change some styles in viewer mode 2022-08-24 10:36:38 +02:00
Alejandro Alonso
cb064358f8 🐛 Fix permissions when moving comments 2022-08-24 10:26:08 +02:00
andy
a6210be63a 🌐 Added translation for: Basque. 2022-08-24 09:18:04 +02:00
Alejandro Alonso
1fdd3b85ab 🐛 Fix typo in releases 1.15 2022-08-24 09:00:28 +02:00
Alejandro Alonso
8d8e4c5e22 Merge remote-tracking branch 'origin/staging' 2022-08-24 08:11:22 +02:00
Alejandro
595700f8b3 Merge pull request #2184 from penpot/palba-fix-multiselection-assets
🐛 Fix multiselection with shift not working inside a library group
2022-08-24 08:00:30 +02:00
Pablo Alba
29223e8db8 🐛 Fix multiselection with shift not working inside a library group 2022-08-23 17:29:36 +02:00
Andrés Moya
4e319fd9ef Merge pull request #2182 from penpot/niwinz-viewer-performance
Viewer performance issues
2022-08-23 14:01:25 +02:00
Andrey Antukh
c1348189d4 🐛 Fix path with images on binfile importation 2022-08-23 13:57:31 +02:00
Andrey Antukh
1b42e324a2 Avoid recursive rerender and react warning 2022-08-23 13:57:31 +02:00
Andrey Antukh
7af914eef0 📎 Properly print on console UI related errors 2022-08-23 13:57:31 +02:00
Andrey Antukh
1649ca4ff7 📎 Fix linter issues 2022-08-23 13:57:31 +02:00
Andrey Antukh
f9b44ccc5c Refactor viewer shape-container component
Still need a rething for the fixed position shapes
feature because watching scroll position on all shapes
is killing viewer performance.
2022-08-23 13:57:31 +02:00
Andrey Antukh
b9f767a614 Rename active-frames-ctx to active-frames 2022-08-23 13:57:31 +02:00
Andrey Antukh
3e3a10b5dd Rename render-ctx to render-id 2022-08-23 13:57:31 +02:00
Andrey Antukh
082bcd2bde 🔥 Remove unused def-ctx react context var 2022-08-23 13:57:31 +02:00
Andrey Antukh
10bb75c1a1 🔥 Remove unused code related to remap colors of fo-text component 2022-08-23 13:57:31 +02:00
Andrey Antukh
a37c1f7fca ♻️ Refactor viewer comments related components 2022-08-23 13:57:31 +02:00
Andrey Antukh
50d371c14b ♻️ Refactor viewer state management (partial) 2022-08-23 13:57:31 +02:00
Andrey Antukh
48de242a2d 🐛 Fix z-index on viewer sidebar 2022-08-23 13:57:31 +02:00
Andrey Antukh
9722e6ea97 📎 Update gitignore file 2022-08-23 13:57:30 +02:00
Andrey Antukh
f9502315ec Remove duplicate helper from page helpers 2022-08-23 13:57:30 +02:00
Alejandro
dbd2b8527a Merge pull request #2180 from penpot/palba-carousel-misaligment
🐛 Fix Libraries & Templates carousel misalingments
2022-08-23 10:50:46 +02:00
Alejandro Alonso
eb797f37a7 🐛 Fix hide html options on handoff 2022-08-23 09:34:13 +02:00
Alejandro Alonso
36af303850 🐛 Fix share prototypes overlay and stroke 2022-08-23 09:34:13 +02:00
Alejandro Alonso
d16761772b 🐛 Fix border radious on boolean operations 2022-08-23 09:34:13 +02:00
Andrey Antukh
b3d6b4b402 Merge remote-tracking branch 'origin/staging' into develop 2022-08-23 08:11:58 +02:00
Pablo Alba
b6e17a0f09 🐛 Fix Libraries & Templates carousel misalingments 2022-08-22 12:00:30 +02:00
Alejandro Alonso
7325322ebf 🐛 Fix text alignment undefined after paste text 2022-08-19 15:15:29 +02:00
Alejandro Alonso
a5975864fb 🎉 Update login methods translations 2022-08-19 15:02:54 +02:00
Alejandro Alonso
4a655c863a 🐛 Fix typo in manage.sh 2022-08-19 07:21:51 +02:00
andy
9b642b6055 🌐 Add translations for: Romanian.
Currently translated at 54.4% (635 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2022-08-18 18:16:49 +02:00
andy
17c5eeb740 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 43.8% (511 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2022-08-18 18:16:49 +02:00
Oğuz Ersen
54f19564d4 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1166 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2022-08-18 18:16:48 +02:00
andy
c3219d1de5 🌐 Add translations for: Russian.
Currently translated at 45.3% (529 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2022-08-18 18:16:48 +02:00
andy
f279e54f2f 🌐 Add translations for: Greek.
Currently translated at 48.5% (566 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/el/
2022-08-18 18:16:48 +02:00
andy
da18314e37 🌐 Add translations for: French.
Currently translated at 65.9% (769 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-08-18 18:16:47 +02:00
andy
f689d2f84f 🌐 Add translations for: Catalan.
Currently translated at 92.0% (1073 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2022-08-18 18:16:47 +02:00
andy
a43f76bb3f 🌐 Add translations for: English.
Currently translated at 100.0% (1166 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/en/
2022-08-18 18:16:47 +02:00
Andrés Moya
0c717c579b 🐛 Some small fixes
* Remove an unneeded condition in sync.
* Correctly duplicate main instances.
* Slightly enhance sync logging.
2022-08-17 16:22:04 +02:00
Eva
bf63e9da95 🎉 Add new hero projects 2022-08-17 15:50:24 +02:00
Alejandro Alonso
7f7032aaa5 🐛 Fix inconsistent representation of rectangles 2022-08-17 13:03:03 +02:00
Alejandro
61cb43f2f0 Merge pull request #2169 from penpot/hiru-create-nested-component
🎉 Allow to create a nested component in one step
2022-08-17 12:24:38 +02:00
andy
de8d693292 🌐 Add translations for: German.
Currently translated at 78.5% (916 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-08-17 12:18:31 +02:00
Alejandro
5c5ec8ef56 Merge pull request #2152 from penpot/niwinz-colorpicker-state-management-refactor
♻️ Refactor state management on colorpicker and gradients
2022-08-17 11:12:22 +02:00
Andrés Moya
9725dd5fff 🎉 Allow to create a nested component in one step 2022-08-16 15:15:02 +02:00
Andrey Antukh
d6faf68dce 📎 Add entry to the changelog 2022-08-12 15:11:20 +02:00
Andrey Antukh
9950c5dc0f 🎉 Add shared state hook and broadcast channel api 2022-08-12 15:11:20 +02:00
Andrey Antukh
756b6d4fbd 💄 Minor cosmetic changes on resize and thumbnail render 2022-08-12 15:11:20 +02:00
Andrey Antukh
8d06227d1e ♻️ Refactor state management of colorpicker & gradients 2022-08-12 15:11:20 +02:00
Andrey Antukh
4ad27c3fca Merge remote-tracking branch 'origin/staging' into develop 2022-08-12 12:49:36 +02:00
Andrey Antukh
4cc88bf84f Merge pull request #2162 from penpot/release-1.15
🎉 Add new release info
2022-08-12 12:48:25 +02:00
Andrey Antukh
d8332e62d1 Merge pull request #2161 from penpot/superalex-fix-text-edit-when-using-certain-fonts
🐛 Fix text edition when using certain fonts
2022-08-12 12:48:08 +02:00
Andrey Antukh
86389256a9 📎 Fix linter issues 2022-08-12 09:50:33 +02:00
Andrey Antukh
4f0cc3d0d8 Merge remote-tracking branch 'origin/staging' into develop 2022-08-12 09:45:58 +02:00
Alejandro
95335e64b1 Merge pull request #2160 from penpot/niwinz-enhancements
Enhancements on tasks
2022-08-12 09:37:21 +02:00
Alejandro Alonso
c219d1cc89 🐛 Fix text edition when using certain fonts 2022-08-12 09:30:12 +02:00
Andrey Antukh
7fa609d5f4 Allow disable worker 2022-08-12 08:52:36 +02:00
Andrey Antukh
95bb3f31af Fix all tasks related tests 2022-08-12 08:35:04 +02:00
Andrey Antukh
8d7baa75de Improve tasks-gc task 2022-08-12 08:35:04 +02:00
Andrey Antukh
5867e64d36 Improve objects-gc task 2022-08-12 08:34:57 +02:00
Andrey Antukh
fee264007f Merge pull request #2157 from penpot/superalex-add-audit-log-for-team-up-hero
🎉 Add audit log for team up hero
2022-08-12 08:21:03 +02:00
Andrey Antukh
df00760ffa Improve file-xlog-gc task 2022-08-11 17:31:32 +02:00
Andrey Antukh
ac8ef1d622 🔥 Remove completly unused file-offload task 2022-08-11 17:31:32 +02:00
Andrey Antukh
ec2a3c0de1 Improve the file-gc task logging and params 2022-08-11 17:31:32 +02:00
Andrey Antukh
d533e37ae0 Improve logging on gc-deleted storage task 2022-08-11 17:31:32 +02:00
Andrey Antukh
6ee6e5e23e Improve logging on gc-touched storage task 2022-08-11 17:31:32 +02:00
Andrey Antukh
7626d912b9 🎉 Add srepl helpers for run and print available tasks 2022-08-11 17:31:32 +02:00
Andrey Antukh
ada0938e27 Remove key warning on import dialog 2022-08-11 17:31:32 +02:00
Andrey Antukh
918d2ab4a9 🎉 Add more helpers on srepl ns 2022-08-11 17:31:32 +02:00
Andrey Antukh
b3623ed14c 🎉 Add migration for remove on cascade action on file-media-object table 2022-08-11 17:31:32 +02:00
Alejandro
94c3dfbfe8 Merge pull request #2158 from penpot/eva-modal-animations
💄 Improve onboarding modal animations
2022-08-11 12:05:17 +02:00
Alejandro Alonso
f360958c66 🎉 Add audit log for team up hero 2022-08-11 11:59:07 +02:00
Eva
c9885d757a 💄 Improve onboarding modal animations 2022-08-11 11:17:28 +02:00
Andrey Antukh
4a054dec25 Merge remote-tracking branch 'origin/staging' into develop 2022-08-11 09:41:41 +02:00
Alejandro
0b9546c541 Merge pull request #2155 from penpot/eva-onboarding-team-up
🎉 Eva onboarding team up
2022-08-11 09:24:29 +02:00
Eva
0e513a4a25 🎉 Add new team hero 2022-08-11 08:55:52 +02:00
Andrey Antukh
a77f9eae7c 🎉 Backport binfile improvements from develop 2022-08-11 07:44:47 +02:00
Alejandro
678a163b01 Merge pull request #2156 from penpot/niwinz-binfile-improvements
♻️ Refactor binfile implementation
2022-08-11 06:44:31 +02:00
Tatsuto Yamamoto
3b57e7a583 🌐 Add translations for: Japanese (jpn_JP).
Currently translated at 22.5% (263 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/jpn_JP/
2022-08-11 04:17:13 +02:00
Andrey Antukh
dd73152afd ♻️ Refactor binfile implementation 2022-08-10 20:07:24 +02:00
Andrey Antukh
99ed610dde Merge remote-tracking branch 'origin/staging' into develop 2022-08-10 14:10:57 +02:00
elhombretecla
b38f99b2f6 🎉 Add new release info 2022-08-10 12:26:14 +02:00
Alejandro
6df2089a60 Merge pull request #2154 from penpot/niwinz-upload-size-config
 Make the upload media size configurable
2022-08-10 12:22:13 +02:00
Andrey Antukh
b9b53258c1 Make the upload media size configurable 2022-08-10 12:10:45 +02:00
Eva Marco
5b5fe8ebbc Merge pull request #2145 from penpot/palba-templates-carousel
🎉 Add Libraries & Templates carousel
2022-08-10 11:52:21 +02:00
Pablo Alba
edb46b2080 🎉 Add Libraries & Templates carousel 2022-08-10 11:44:51 +02:00
Alejandro
2d24529165 Merge pull request #2148 from penpot/eva-onboarding-zoom-url
🎉 Add zoom style to share link url
2022-08-10 11:24:23 +02:00
Eva
0a939185d2 🎉 Add zoom style to share link url 2022-08-10 11:00:37 +02:00
Andrey Antukh
2211fea976 🚑 Fix tests in develop
Caused of bad merge conflict resolution from mergin
staging to develop.
2022-08-10 09:14:11 +02:00
Alejandro
b404246f8a Merge pull request #2153 from penpot/eva-fix-shared-placeholder
 Fix shared libraries placehoder
2022-08-10 09:09:23 +02:00
Eva
6f415cc046 Fix shared libraries placehoder 2022-08-10 08:54:34 +02:00
Andrey Antukh
84ee6555a3 Merge remote-tracking branch 'origin/staging' into develop 2022-08-10 08:27:25 +02:00
Alejandro
0471df36ef Merge pull request #2142 from penpot/niwinz-session-management-refactor
♻️ Refactor session management
2022-08-10 08:06:49 +02:00
Alejandro
37f5b41486 Merge pull request #2147 from penpot/niwinz-enhancements-20220808
Enhancements & Fixes
2022-08-10 07:46:02 +02:00
andy
29ff06dc6a 🌐 Added translation for: Japanese (jpn_JP). 2022-08-09 16:04:34 +02:00
Alejandro
42dd38b4ee Merge pull request #2151 from penpot/eva-show-next-btn-viewer
🎉 Make prev and next buttons fixed in viewer
2022-08-09 12:50:50 +02:00
Eva
5791ddda49 🎉 Make prev and next buttons fixed in viewer 2022-08-09 12:35:48 +02:00
Andrey Antukh
36def65c87 Merge pull request #2150 from penpot/eva-fix-recent-fonts
🐛 Fix recent fonts info
2022-08-09 12:25:32 +02:00
Eva
763877b713 🐛 Fix recent fonts info 2022-08-09 12:07:16 +02:00
Eva Marco
b560c07243 Merge pull request #2149 from penpot/superalex-review-onboarding-questions
🎉 Review onboarding questions
2022-08-09 11:55:45 +02:00
Alejandro Alonso
e6dcfec90c 🎉 Review onboarding questions 2022-08-09 11:39:55 +02:00
Andrey Antukh
58a06b8cf3 🐛 Ignore invalid file references on importing file-media-object 2022-08-08 12:16:31 +02:00
Andrey Antukh
c30d4d313c 🐛 Force file-id association with file-media-object on exportation
This is needed because we may have situation when a file
is using a file-media-object reference from other file (probably
a library that is not included in the exportation); in this case
we need to forcely embed it.
2022-08-08 12:09:16 +02:00
Andrey Antukh
183e0bf985 Simplify select all implementation 2022-08-08 10:51:08 +02:00
Andrey Antukh
aceefc0485 ♻️ Move comments mutations to commands 2022-08-08 10:36:15 +02:00
Andrey Antukh
0b3d25a890 Make frontend use new cmd based repo methods for comments queries 2022-08-08 09:51:11 +02:00
Andrey Antukh
173f0d68bb 📎 Properly deprecate comments related queries 2022-08-08 09:42:45 +02:00
Andrey Antukh
61f2799e49 🐛 Fix unexpected response truncation on viewer 2022-08-08 09:28:31 +02:00
Andrey Antukh
7611aec4c6 🐛 Fix unexpected response truncation on viewer 2022-08-08 09:00:27 +02:00
Andrey Antukh
adbadc8743 ♻️ Refactor session management 2022-08-08 07:54:15 +02:00
Alejandro
6d61f75db6 Merge pull request #2144 from penpot/eva-improve-team-icon
💄 Improve team icon
2022-08-05 12:56:38 +02:00
Eva
efa382c906 💄 Improve team icon 2022-08-05 11:24:34 +02:00
Eva Marco
a54e0900d0 Merge pull request #2137 from penpot/superalex-fix-clipped-elements-affect-artboards-centering
🐛 Fix clipped elements affect artboards centering
2022-08-05 08:25:01 +02:00
Eva
e1972692ab 👷 fix CI 2022-08-04 16:00:20 +02:00
Alejandro Alonso
9ffd00d821 🐛 Fix clipped elements affect artboards centering 2022-08-04 15:12:33 +02:00
Eva
33706e0bda 🎉 Add ellipsis to text 2022-08-04 13:54:35 +02:00
Alejandro
57ec9f8218 Merge pull request #2139 from penpot/eva-review-onboarding
🎉 Improve dashboard and onboarding css
2022-08-04 12:33:09 +02:00
Eva
e863ef7dbf 🎉 Improve dashboard and onboarding css 2022-08-04 12:28:53 +02:00
Andrey Antukh
390ad34b13 Merge remote-tracking branch 'origin/staging' into develop 2022-08-04 10:43:12 +02:00
Andrey Antukh
29fa36ad2d Merge pull request #2140 from penpot/superalex-link-to-binary-versions-for-files-used-in-libraries-and-templates-carousel
🎉 Add binary file links to onboarding.edn
2022-08-04 09:24:14 +02:00
Alejandro Alonso
fe7c01323a 🎉 Add binary file links to onboarding.edn 2022-08-04 09:22:36 +02:00
Andrew Zhurov
5febd35cfe 🐛 Fix layers get out of their group when moved
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-08-04 07:11:43 +02:00
Andrey Antukh
1df9f0b29e Merge pull request #2131 from penpot/andrewzhurov-3932-layers-get-out-of-the-group-when-moved
🐛 Fix layers get out of the group when moved
2022-08-04 07:09:29 +02:00
Andrey Antukh
74c6556ad6 ♻️ Refactor some page helpers usage 2022-08-04 07:08:18 +02:00
Andrew Zhurov
d270c9670e 🐛 Fix layers get out of their group when moved
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-08-04 07:08:18 +02:00
Alejandro
b926409fa2 Merge pull request #2135 from penpot/eva-bugfixes-1.15
🐛 Bugfixes Eva
2022-08-04 06:52:33 +02:00
Pablo Alba
87419d63a5 Merge pull request #2138 from penpot/niwinz-builtin-templates
Builtin Templates Load & RPC command for clone
2022-08-03 17:44:53 +02:00
Andrey Antukh
53d7c4332d 🎉 Add prefetch builtin templates script 2022-08-03 17:06:53 +02:00
Andrey Antukh
6981d92b11 🎉 Add RPC method for retrieve the list of builtin templates 2022-08-03 14:46:51 +02:00
Andrey Antukh
052404b1b4 🎉 Add complete list of onboarding builtin files 2022-08-03 14:41:32 +02:00
Andrey Antukh
b8b60d9208 🎉 Add RPC command for clone builtin template 2022-08-03 14:28:33 +02:00
Andrey Antukh
ed701fd9c5 ♻️ Move management mutations to commands 2022-08-03 14:28:33 +02:00
Andrey Antukh
d832482dae 🎉 Add builtin template loading mechanism 2022-08-03 14:28:33 +02:00
Andrey Antukh
fd08511514 Merge pull request #2129 from penpot/palba/select-all-group
 Select all inside a group select only the objects at this …
2022-08-03 13:37:42 +02:00
Pablo Alba
812131fdbc Add new image to onboarding welcome 2022-08-03 13:35:44 +02:00
Pablo Alba
52cc91f4c4 Select all inside a group select only the objects at this group level 2022-08-03 11:37:33 +02:00
Andrey Antukh
f455580cf7 Merge pull request #2124 from penpot/3799-dashboard-rework
3799 dashboard rework
2022-08-03 11:06:52 +02:00
Andrey Antukh
15d7b94940 🎉 Add convenience helper for http client 2022-08-03 09:49:24 +02:00
Andrey Antukh
d30b6ac5b9 Reorganize resources directory 2022-08-03 09:49:24 +02:00
Alejandro
39fb391128 Merge pull request #2130 from penpot/niwinz-uuid-improvements
🎉 Replace current uuidv1 with custom v8
2022-08-03 09:38:23 +02:00
Andrey Antukh
77b1ebfcc6 Merge pull request #2132 from penpot/eva-modify-design-onboarding-modal
💄 Modify label in input
2022-08-03 09:34:42 +02:00
Andrey Antukh
f5df0eacef Merge remote-tracking branch 'origin/staging' into develop 2022-08-03 09:33:48 +02:00
Andrey Antukh
fdc01cfed5 Merge branch 'andrew-fixes-backports' into staging 2022-08-03 09:27:09 +02:00
Andrey Antukh
0cc51db533 📎 Update changelog 2022-08-03 09:26:29 +02:00
Andrey Antukh
4266d9be83 📎 Add missing entry on changelog 2022-08-03 09:25:38 +02:00
Eva
8795e134c1 🐛 Fix intro action in multi input 2022-08-03 09:23:36 +02:00
Andrew Zhurov
732755066e 🐛 Fix text alignment becoming undefined on pasting text from clipboard
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-08-03 09:23:07 +02:00
Andrew Zhurov
424e9faa8e 🐛 Fix paste frame removes all guides
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-08-03 09:22:57 +02:00
Andrey Antukh
8fe98b1f7a Merge pull request #2133 from penpot/superalex-add-audit-log-for-new-onboarding
🎉 Add audit log for new onboarding
2022-08-03 09:05:51 +02:00
Andrey Antukh
5c6212d7a2 📎 Comment not passing test of experimental code of new components
It should be revisited by @andres.moya
2022-08-03 09:05:28 +02:00
Andrey Antukh
ed5ce777b9 📎 Uncomment frontend tests on common module 2022-08-03 09:05:28 +02:00
Andrey Antukh
d477f74d13 📎 Change output feature set to :es2020 on test compiler options 2022-08-03 09:05:28 +02:00
Andrey Antukh
fbfcb827ed 🎉 Replace current uuidv1 with custom v8 2022-08-03 09:05:28 +02:00
Alejandro Alonso
b4d5ff3452 🎉 Add audit log for new onboarding 2022-08-03 08:55:34 +02:00
elhombretecla
4c03450b88 🎉 Add new CSS dashboard viewpoints 2022-08-03 08:32:24 +02:00
Eva
10e0a662e4 💄 Modify label in input 2022-08-02 11:00:43 +02:00
Andrey Antukh
c62427501e Merge pull request #2128 from penpot/palba-copy-paste-layers-order
🐛 Fix copy and paste layers order
2022-08-02 10:27:55 +02:00
Pablo Alba
64217b34ca 🐛 Fix copy and paste layers order 2022-08-02 10:23:25 +02:00
Eva
140731cf34 🐛 Change default team image in config 2022-08-02 08:30:07 +02:00
Andrew Zhurov
39ae2ed98d 🐛 Fix svg upload
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-08-01 16:43:59 +02:00
Andrey Antukh
5336db4456 Merge pull request #2123 from andrewzhurov/3885-assets-library-not-showing-graphics
🐛 Fix svg upload
2022-08-01 16:42:17 +02:00
Andrey Antukh
f301ec5d2f Merge pull request #2121 from andrewzhurov/3629-text-alignment-becomes-undefined-after-pasting-text-from-clipboard
🐛 Fix text alignment becoming undefined on pasting text from clipboard
2022-08-01 15:30:45 +02:00
Andrey Antukh
6237829445 📎 Add additional reformating to specs naming 2022-08-01 15:03:45 +02:00
Andrey Antukh
abfca5c89a 📎 Add additional reformating to specs naming 2022-08-01 15:01:03 +02:00
Andrey Antukh
d54ebaa0d7 Merge pull request #2125 from penpot/eva-component-delete-modal-plr
🎉 Add plurals to delete or unpublish lib modal
2022-08-01 14:42:49 +02:00
Andrey Antukh
5e57fb4023 📎 Fix linter issues introduced in the latest merges 2022-08-01 14:41:30 +02:00
Andrey Antukh
c1daa4a4c4 Merge remote-tracking branch 'origin/staging' into develop 2022-08-01 14:38:09 +02:00
Andrey Antukh
ddc7f412a4 📎 Mainly reformat specs code 2022-08-01 14:27:11 +02:00
Andrey Antukh
f25222e441 Merge remote-tracking branch 'origin/staging' into develop 2022-08-01 14:05:04 +02:00
Eva
ae20a06e97 🎉 Add plurals to delete or unpublish lib modal 2022-08-01 13:51:27 +02:00
Andrey Antukh
18970cb233 Merge pull request #2108 from penpot/hiru-main-instance
Components v2 (first PR)
2022-08-01 13:25:06 +02:00
Andrey Antukh
5cd12ac710 ⬆️ Update shadow-cljs to 2.19.8 2022-08-01 13:11:44 +02:00
Andrey Antukh
91baae3580 📎 Minor change on session internal timestamp handling 2022-08-01 13:10:01 +02:00
Andrew Zhurov
a6ee1617ab 🐛 Fix svg upload
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-08-01 11:23:08 +03:00
Pablo Alba
01306841a9 Merge pull request #2084 from penpot/eva-alex-move-comments
❇️ Comments positioning
2022-08-01 10:03:03 +02:00
Eva
1c446a011e Move comments 2022-08-01 09:53:55 +02:00
Andrew Zhurov
0aa60b22b0 🐛 Fix text alignment becoming undefined on pasting text from clipboard
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-07-31 10:33:10 +03:00
Pablo Alba
bcc7be16ad Merge pull request #2119 from penpot/eva-onboaring-teams
🎉  Improve team up invitation flow
2022-07-29 14:34:02 +02:00
Andrés Moya
f4482eb5a7 Allow to set features by config file 2022-07-29 14:00:36 +02:00
Andrés Moya
0667089833 🔧 Some style enhancements and mini bug fix 2022-07-29 14:00:36 +02:00
Eva
c108974ad2 Add info in modal 2022-07-29 14:00:36 +02:00
Eva
dd5a6f7f50 🎉 Improve team up invitation flow 2022-07-29 13:02:07 +02:00
Andrés Moya
a5bf1c03e7 🎉 Make components-v2 an optional feature 2022-07-29 09:29:25 +02:00
Andrés Moya
1ef37281e6 🎉 Auto sync when changing main instance 2022-07-29 09:29:24 +02:00
Andrés Moya
eebd596fca ♻️ Use main-instance? attribute 2022-07-29 09:29:24 +02:00
Andrés Moya
dcf18b3aee 🔧 Refactor sync-file for performance 2022-07-29 09:29:24 +02:00
Andrés Moya
43e0b5cfa5 🎉 Absorb colors and typographies 2022-07-29 09:29:24 +02:00
Andrés Moya
7da159d52a 🎉 Absorb components when deleting or unpublishing a library 2022-07-29 09:29:24 +02:00
Andrés Moya
54e0071c9c 🎉 Scaffolding to write unit tests of common types 2022-07-29 09:29:24 +02:00
Andrés Moya
165cdd871f 🎉 Allow to duplicate components with main instance 2022-07-29 09:29:24 +02:00
Andrés Moya
ce09ea6eb5 🎉 Add library page for components on migration 2022-07-29 09:29:24 +02:00
Andrés Moya
bdcbe46d0d ♻️ Move component instantiation to new types module 2022-07-29 09:29:24 +02:00
Andrés Moya
5dc7bc213f 🎉 Add the concept of 'main instance' 2022-07-29 09:29:22 +02:00
Andrey Antukh
758d0d8943 📎 Update changelog 2022-07-29 08:22:33 +02:00
Andrey Antukh
f8fbb7abba Merge pull request #2113 from andrewzhurov/3887-paste-frame-removes-all-guides
🐛 Fix paste frame removes all guides
2022-07-29 08:21:04 +02:00
Andrey Antukh
c6f74692ba Merge pull request #2111 from andrewzhurov/1962-double-click-icon-in-navigation-pane-to-focus-on-object-or-artboard
🎉 Add zoom to shape on double click upon its icon in navigation pane
2022-07-29 08:17:30 +02:00
Andrew Zhurov
98402ae1db 🎉 Add zoom to shape on double click upon its icon in navigation pane
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-07-28 20:40:49 +03:00
Andrew Zhurov
902c746dbb 🐛 Fix paste frame removes all guides
Signed-off-by: Andrei Zhurau <zhurov.andrew@gmail.com>
2022-07-28 19:59:12 +03:00
Andrey Antukh
26fd1a261c Merge pull request #2116 from penpot/palba-review-info-slides
🎉 New style and content for onboarding slides
2022-07-28 15:24:30 +02:00
Pablo Alba
b93b8a8966 🎉 Add new style and content for onboarding slides 2022-07-28 15:17:24 +02:00
Andrey Antukh
4e2dbdbebe Merge remote-tracking branch 'origin/staging' into develop 2022-07-28 11:58:30 +02:00
Alejandro
8379cc3625 Merge pull request #2094 from penpot/niwinz-enhancements-20220713
Bugfixes & Enhancements
2022-07-28 11:56:08 +02:00
Alejandro
aa95114860 Merge pull request #2114 from penpot/andrewzhurov-2645-hovering-layers-bounding-box
Layer outlines hightlight on hovering
2022-07-28 11:43:43 +02:00
Andrey Antukh
d084f17430 Add ssh client to devenv dockerfile 2022-07-28 11:14:59 +02:00
Andrey Antukh
e3f878ef2f ♻️ Move doc ns from http to rpc ns 2022-07-28 11:14:59 +02:00
Andrey Antukh
7a3f1a36e9 📎 Fix linter issues 2022-07-28 10:53:23 +02:00
Andrey Antukh
b3415d0d52 📎 Update changelog 2022-07-28 10:53:21 +02:00
Andrey Antukh
10f8d1365c 📎 Add todo about equality checks on refs ns 2022-07-28 10:52:40 +02:00
Andrey Antukh
a48db277b9 Avoid recursive rerender and react warning 2022-07-28 10:52:40 +02:00
Andrey Antukh
9263f70d6a Use properly the react hook
We can't use hooks in a condition, the condition should be inside
the hook.
2022-07-28 10:52:40 +02:00
Andrey Antukh
d2aa985714 Properly use use-fn hook on layer-item component on sidebar 2022-07-28 10:52:40 +02:00
Andrey Antukh
b5796b4cdb Efficiency improvements on outlines component 2022-07-28 10:52:40 +02:00
Andrey Antukh
c3f67e6358 💄 Many cosmetic and indentation changes on outlines component 2022-07-28 10:52:40 +02:00
Andrey Antukh
1477837cbf 💄 Use concat-vec helper instead of set + into 2022-07-28 10:52:40 +02:00
Andrey Antukh
5834e29b39 🔥 Remove unnecesary ref, viewport already has access to all local data 2022-07-28 10:52:40 +02:00
Andrew Zhurov
1fa25060a0 🎉 Add shape outline on hover upon layers in workspace contextual menu
Signed-off-by: Andrew Zhurov <zhurov.andrew@gmail.com>
2022-07-28 10:52:40 +02:00
Andrew Zhurov
c354c560d4 🎉 Add shape outline on hover upon layers in the left sidebar of workspace
Signed-off-by: Andrew Zhurov <zhurov.andrew@gmail.com>
2022-07-28 10:52:40 +02:00
Alejandro
4b2729b041 Merge pull request #2112 from penpot/eva-viewer-full-screen
💄 Change some styles in viewer mode
2022-07-28 10:31:34 +02:00
Eva
5b658c2f8a 💄 Change some styles in viewer mode 2022-07-28 09:47:52 +02:00
Andrey Antukh
05a86581a5 Reorganize comments related rpc methods
Mutations becomes deprecated and queries moved to commands. The
old queries still maintained with deprecated flag.
2022-07-27 21:41:38 +02:00
Andrey Antukh
8237805cf5 🐛 Fix minor issues on page helpers 2022-07-27 21:41:38 +02:00
Andrey Antukh
8fd908a59f 💄 Add mainly cosmetic improvements to delete-shapes event impl 2022-07-27 21:41:38 +02:00
Andrey Antukh
07eab923f0 Improve doc endpoint
Add changes, added and deprecation notices
2022-07-27 21:41:38 +02:00
Andrey Antukh
2e077e3ea9 🐛 Fix awsns endpoint 2022-07-27 21:41:38 +02:00
Andrey Antukh
99dea51eea ⬆️ Update yetti to v9.3 (bugfixing) 2022-07-27 21:41:38 +02:00
Andrey Antukh
e7ae8f5c58 🐛 Fix unexpected null pointer exception on decoding pgarray 2022-07-27 21:41:38 +02:00
Andrey Antukh
ee51e8c719 Always assoc :iat claim to tokens for better traceability 2022-07-27 21:41:38 +02:00
Andrey Antukh
b4ad907c73 📎 Improve clj-kondo hook impl for defservice 2022-07-27 21:41:38 +02:00
Andrey Antukh
8285cb8f62 🐛 Fix unexpected text positioning 2022-07-27 15:28:50 +02:00
Andrey Antukh
7f611c89e1 Merge branch 'staging' into develop 2022-07-27 15:10:51 +02:00
Andrey Antukh
00b6d76164 Merge remote-tracking branch 'origin/eva-new-file-placeholder' into develop 2022-07-27 14:36:41 +02:00
Andrey Antukh
333e1d32a2 Merge pull request #2097 from penpot/palba-fix-drag-drop-fonts
🐛 Fix drag and drop font assets in groups
2022-07-27 14:17:28 +02:00
Pablo Alba
58f93d2177 🐛 Fix drag and drop font assets in groups 2022-07-27 14:17:02 +02:00
Andrey Antukh
08c0070f22 Merge branch 'niwinz-scripts-and-fixes' into staging 2022-07-27 13:16:48 +02:00
Pablo Alba
6408689d4c Merge pull request #2110 from penpot/niwinz-scripts-and-fixes
Scripts & Fixes
2022-07-27 13:13:34 +02:00
Alejandro Alonso
bfe54fe5e1 Improve audit log for create files 2022-07-27 13:07:28 +02:00
Andrey Antukh
14c28ccce7 Merge pull request #2095 from penpot/alotor-bugfixes
Alotor bugfixes
2022-07-27 12:59:31 +02:00
Andrey Antukh
dece149c9e 🎉 Add migration for fix legacy storage object backend names 2022-07-27 12:55:43 +02:00
Andrey Antukh
9275f5e5ce Reorganize migrations directory 2022-07-27 12:55:43 +02:00
Andrey Antukh
483da5248f 🎉 Add internal script for move some legacy files stored on fs backend to s3 2022-07-27 12:55:43 +02:00
Andrey Antukh
4bf05c8a42 Minor reorganization of srepl namespace 2022-07-27 12:55:43 +02:00
Eva
1eede8442d Add new file creation button on placeholder 2022-07-27 10:54:05 +02:00
Ahmad HosseinBor
3778bb4b1d 🌐 Add translations for: Persian.
Currently translated at 50.6% (590 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-07-27 10:17:11 +02:00
Andrey Antukh
1174502cb8 Merge remote-tracking branch 'origin/staging' into develop 2022-07-26 11:54:16 +02:00
Andrey Antukh
cd8578480f 🐛 Fix unexpected exception on i18n autodetect code 2022-07-26 11:52:43 +02:00
Andrey Antukh
e5ebe0a295 Merge branch 'staging' into develop 2022-07-26 08:26:34 +02:00
Yaron Shahrabani
d3dd2644ae 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1166 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2022-07-19 08:21:57 +02:00
alonso.torres
d2a5344407 🐛 Fix problem with snap-pixel on resize 2022-07-15 14:48:05 +02:00
alonso.torres
48615ca5b2 🐛 Round coordinates in viewport and paths 2022-07-15 14:48:05 +02:00
Yaron Shahrabani
b49348ff86 🌐 Add translations for: Hebrew.
Currently translated at 93.3% (1088 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2022-07-15 13:18:50 +02:00
Oğuz Ersen
d9cc76f8ba 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1166 of 1166 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2022-07-15 13:18:50 +02:00
alonso.torres
f89ccac567 🐛 Fix problems with nested boards 2022-07-15 11:11:12 +02:00
alonso.torres
b57ddf9dca 🐛 Fix problem with 180 degree rotations 2022-07-15 11:11:12 +02:00
alonso.torres
8e9ab32a9f 🐛 Fix moving frame-guides outside frames 2022-07-15 11:11:12 +02:00
alonso.torres
fdbcf977f5 🐛 Fix problem with line-height and texts 2022-07-15 11:11:12 +02:00
alonso.torres
cc6b3dcec6 🐛 Fix problem with group coordinates 2022-07-15 11:11:12 +02:00
alonso.torres
7abbcdf226 Move text position calculation outside foreign object 2022-07-15 11:11:12 +02:00
alonso.torres
4088e55c9f 🐛 Fix problem with span overflow 2022-07-15 11:03:13 +02:00
Andrey Antukh
54d9b02b4d Add specific font for persian and arabic locales
And remove deprecated and not used font files, simplifying
the font-face mixin.
2022-07-15 11:03:13 +02:00
Andrey Antukh
b2da41720e ⬆️ Update npm dependencies on frontend submodule 2022-07-15 09:52:25 +02:00
Andrey Antukh
2b70331630 🐛 Fix custom fonts loading on dashboard thumbnails 2022-07-15 09:52:25 +02:00
Andrey Antukh
05c8ad8bf9 💄 Minor cosmetic changes on fonts loading code 2022-07-15 09:52:25 +02:00
Andrey Antukh
18ca2aca15 🐛 Fix font loading when text is only rendered as svg 2022-07-15 09:52:25 +02:00
Andrey Antukh
51023396bc 🎉 Add new version of gfonts reference file 2022-07-15 09:52:25 +02:00
Andrey Antukh
3e7b9805c9 Merge pull request #2099 from penpot/superalex-fix-worker-synchronize-cron-entries
🐛 Fix worker synchronize cron entries
2022-07-15 09:42:55 +02:00
Alejandro Alonso
be0c810c5f 🐛 Fix worker synchronize cron entries 2022-07-15 08:03:06 +02:00
Andrey Antukh
dd180d93f4 Merge pull request #2088 from penpot/alotor-autolayout
Autolayout basics
2022-07-14 09:54:49 +02:00
alonso.torres
3ac1760141 🎉 Add layout items options and feature toggle 2022-07-14 09:53:59 +02:00
alonso.torres
28abe785e8 Allow for rotated auto-layout 2022-07-14 09:53:59 +02:00
alonso.torres
5e5355230c 🎉 Add support for wrap layout 2022-07-14 09:53:59 +02:00
alonso.torres
6e5a23c190 Allows groups to reflow the layout on transform 2022-07-14 09:53:59 +02:00
alonso.torres
84c0825893 🐛 Fix problems with nested groups 2022-07-14 09:53:53 +02:00
Eva
51e8eea795 Changes in the layout UI 2022-07-14 09:53:53 +02:00
alonso.torres
7176bb6f1a 🎉 Add support for nested layouts 2022-07-14 09:53:53 +02:00
alonso.torres
1c8aef6fa8 🎉 Add packed basic layout positions 2022-07-14 09:53:33 +02:00
alonso.torres
aeb8fa1896 🎉 Add basic left-right layout 2022-07-14 09:48:51 +02:00
alonso.torres
3c3664535e Store layout info in shape 2022-07-14 09:29:21 +02:00
Alejandro
a958aed058 Merge pull request #2093 from penpot/niwinz-minor-release-1.14.2
Prepare the 1.14.2 minor release
2022-07-14 07:26:58 +02:00
Andrey Antukh
e662a7090f Merge branch 'staging' into develop 2022-07-14 07:11:01 +02:00
Andrey Antukh
2e2b05a7a4 📎 Sort translations files 2022-07-14 07:10:05 +02:00
Andrey Antukh
4e5146c210 Merge remote-tracking branch 'weblate/develop' into translations 2022-07-14 07:08:42 +02:00
Andrey Antukh
4bac2f15a2 ⬆️ Use correct version of im4java (fixes tests) 2022-07-13 11:39:36 +02:00
Andrey Antukh
1c09328d0e 📎 Update version.txt file 2022-07-13 11:22:06 +02:00
alonso.torres
06905d5fa6 🐛 Fix SVG texts positioning inconsistencies 2022-07-13 11:22:06 +02:00
Andrey Antukh
46c9fc1c5f 🐛 Normalize return value from parse-client-ip function 2022-07-13 11:18:33 +02:00
Andrey Antukh
b901a10aaa 🐛 Fix typographies grouping 2022-07-13 11:17:55 +02:00
Pablo Alba
9022520334 Merge pull request #2091 from penpot/eva-bugfix-share
🐛 Fix overlay and shadows of modal body
2022-07-13 11:10:32 +02:00
Andrey Antukh
c4bdb84d70 Merge pull request #2089 from penpot/palba-create-shared-link-log
 Add audit log for create shared link
2022-07-13 11:03:18 +02:00
Eva
edad00ad95 🐛 Fix overlay and shadows of modal body 2022-07-13 08:15:03 +02:00
Pablo Alba
8ac32fc3c2 Add audit log for create shared link 2022-07-12 13:07:51 +02:00
Andrey Antukh
641ebf8b8e 📎 Increase version 2022-07-12 11:57:51 +02:00
Andrey Antukh
8c84cc7fa0 📎 Update changelog 2022-07-12 11:56:35 +02:00
Andrey Antukh
40415bb0d8 Merge branch 'develop' into staging 2022-07-12 11:55:13 +02:00
alonso.torres
f2bd6a552f Feature toggle 2022-07-11 11:45:26 +02:00
Josep Ponsà
62bb3d9087 🌐 Add translations for: Catalan.
Currently translated at 99.5% (1105 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2022-07-09 13:14:47 +02:00
Alejandro
374f52a819 Merge pull request #2080 from penpot/superalex-set-project-on-binary-file-import
🐛 Fix set project on binary file import
2022-07-08 08:00:54 +02:00
Alejandro Alonso
d140f15f37 🐛 Fix set project on binary file import 2022-07-08 07:43:44 +02:00
Alejandro
f32bb56b95 Merge pull request #2079 from penpot/superalex-set-project-on-binary-file-import
🐛 Fix set project on binary file import
2022-07-08 06:47:22 +02:00
Alejandro Alonso
37e9adc6b6 🐛 Fix set project on binary file import 2022-07-08 06:43:04 +02:00
Alejandro
602cead4ae Merge pull request #2077 from penpot/niwinz-asserts-improvements
Asserts & binfile cosmetic refactor
2022-07-07 13:19:02 +02:00
Andrey Antukh
aadb7cb1bf Don't call rp/command internal method 2022-07-07 13:12:38 +02:00
Andrey Antukh
d60f849089 💄 Cosmetic refactor of binfile internal API impl 2022-07-07 13:08:18 +02:00
Andrey Antukh
98190ed92d ♻️ Improve the asserts framework 2022-07-07 12:29:13 +02:00
Andrey Antukh
c02e8ff883 Print the spec error explain to logging message 2022-07-07 12:29:13 +02:00
Andrey Antukh
4d55ed4860 Ensure vector ids on export debug handler 2022-07-07 12:29:13 +02:00
Andrey Antukh
5e2c1fb4cd 🎉 Add missing predicate on util/bytes ns 2022-07-07 12:29:13 +02:00
Andrey Antukh
f9447029f3 🔥 Remove some deprecated config attrs 2022-07-07 12:28:13 +02:00
Andrey Antukh
2a9c8eb9af 📎 Print parsed flags on start 2022-07-07 12:28:13 +02:00
Andrey Antukh
cdcf3facd2 🐛 Fix flags parsing order 2022-07-07 12:28:13 +02:00
Alejandro Alonso
5c696851bf 📎 Update CHANGES.md file 2022-07-07 11:48:41 +02:00
Andrey Antukh
c8051633d9 Merge pull request #2076 from penpot/superalex-frontend-binary-file-support
  Frontend binary file support
2022-07-07 11:42:46 +02:00
Alejandro Alonso
17645bb2a7 Frontend support for binary files 2022-07-07 11:37:34 +02:00
Alejandro
2fe770e0bb Merge pull request #2075 from penpot/niwinz-export-embed-assets
Embed assets and multiple files support for binfile export
2022-07-07 07:31:05 +02:00
Andrey Antukh
d032953121 Enable exporte multiple files in binfile format 2022-07-06 16:05:10 +02:00
Andrey Antukh
f4f58bc163 Add parameters validation to binfile write-export! fn 2022-07-06 16:05:10 +02:00
Andrey Antukh
d90b4370fb 📎 Update default devenv logging configuration 2022-07-06 16:05:10 +02:00
Andrey Antukh
ade41f77f3 📎 Add some notes to assets ns in sidebar 2022-07-06 16:05:10 +02:00
Andrey Antukh
c405e9a7a3 🔥 Remove unused code 2022-07-06 16:05:10 +02:00
Andrey Antukh
50f30eb12f Add the ability to embed assets on export binfile 2022-07-06 16:01:21 +02:00
Alejandro
6b8ab7aa72 Merge pull request #2072 from penpot/niwinz-update-ubuntu-and-openjdk-on-docker-images
⬆️ Update docker images system dependencies
2022-07-06 11:18:10 +02:00
Alejandro
0dac3f7845 Merge pull request #2071 from penpot/niwinz-improve-api-documentation-output
 Improve _doc endpoint output format
2022-07-06 11:17:22 +02:00
Andrey Antukh
537fff4c80 ⬆️ Update docker images system dependencies 2022-07-05 11:51:36 +02:00
Andrey Antukh
dd130615a1 Improve _doc endpoint output format 2022-07-05 11:04:37 +02:00
Andrey Antukh
356ff4683d Revert "📎 Allow set statement timeout on db module"
This reverts commit 70028e1371.
2022-07-04 14:04:56 +02:00
Andrey Antukh
70028e1371 📎 Allow set statement timeout on db module 2022-07-04 13:34:17 +02:00
Andrey Antukh
a3580a5ab9 📎 Update log4j2 default configuration 2022-07-04 12:41:55 +02:00
Alejandro
6bb5fb0361 Merge pull request #2068 from penpot/niwinz-fix-worker-cron-locking-mechanism
🐛 Fix cron scheduler locking mechanism
2022-07-04 12:30:10 +02:00
Andrey Antukh
f2140a1421 🐛 Fix cron scheduler locking mechanism
And add improved logging to the worker/cron code
2022-07-04 11:32:36 +02:00
Alejandro
f7f9ba99f7 Merge pull request #2067 from penpot/niwinz-auth-improvements
♻️ Refactor auth code
2022-07-04 11:28:26 +02:00
Andrey Antukh
14d1cb90bd ♻️ Refactor auth code 2022-07-04 11:23:33 +02:00
Lucie Lesage
11f7efb850 🌐 Add translations for: French.
Currently translated at 71.3% (792 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-07-02 11:14:04 +02:00
Locness
a16606c8e3 🌐 Add translations for: French.
Currently translated at 71.3% (792 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-07-01 10:17:03 +02:00
Lucie Lesage
7fe7b234bf 🌐 Add translations for: French.
Currently translated at 71.3% (792 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-07-01 10:17:03 +02:00
Alejandro Alonso
ba4f558f62 Merge remote-tracking branch 'origin/staging' into develop 2022-07-01 08:21:02 +02:00
Alejandro
8446df2056 Merge pull request #2065 from penpot/eva-bugfix-selected
🐛 Fix color indicators from unlinked libraries
2022-07-01 08:20:21 +02:00
Alejandro
8f22c421de Merge pull request #2064 from penpot/palba-signin-register-from-shared-link
 Signin/Signup from shared link
2022-07-01 08:17:57 +02:00
Eva
2c0725a9d2 🐛 Fix color indicators from unlinked libraries 2022-07-01 08:05:27 +02:00
Pablo Alba
288dab3fe7 Signin/Signup from shared link 2022-07-01 07:39:57 +02:00
Eranot
672c52b369 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 47.9% (532 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2022-07-01 03:20:21 +02:00
Alejandro
e458e3adb7 Merge pull request #2063 from penpot/alotor-refactor-data
♻️ Refactor workspace common
2022-06-30 13:15:10 +02:00
alonso.torres
b38ffdcf30 ♻️ Refactor workspace common 2022-06-30 13:09:35 +02:00
Andrey Antukh
09a3cf4b58 Merge pull request #2062 from penpot/circleci-experiments
📎 Add additional CI step to circleci config
2022-06-30 07:55:24 +02:00
Alejandro
7406aac0c7 Merge pull request #2058 from penpot/niwinz-exporter-tmp-files
 Put all temporal files under the same directory
2022-06-30 07:31:14 +02:00
Andrey Antukh
e44fb2cdbf 📎 Add additional CI step to circleci config 2022-06-29 23:00:45 +02:00
Andrey Antukh
bfb0ba47f5 💄 Fix linter issues on exporter 2022-06-29 14:53:57 +02:00
Andrey Antukh
9c194ee3cb 🐛 Fix websocket unexpected exception on exportation module
A regression caused by the previous commit that refactos
the websockets API and its state management.
2022-06-29 14:39:56 +02:00
Andrey Antukh
ebe8fdcba8 ♻️ Refactor temporal files management on exporter 2022-06-29 14:39:40 +02:00
Andrey Antukh
d021ac0226 🐛 Fix share link migration for backward compatibilty 2022-06-29 12:30:17 +02:00
Alejandro Alonso
7256bdbcd5 Merge remote-tracking branch 'origin/staging' into develop 2022-06-29 12:00:52 +02:00
Alejandro
27d81ee47d Merge pull request #2060 from penpot/community-nav-link
🎉 Add new community link to dashboard and workspace menus
2022-06-29 12:00:13 +02:00
elhombretecla
be304811d5 🎉 Add new community link to dashboard and workspace menus 2022-06-29 11:52:24 +02:00
Alejandro
bd4548cd25 Merge pull request #2046 from penpot/niwinz-20220624-websockets-fixes
Websocket protocol diagnostic info & Some deps updates
2022-06-29 11:05:37 +02:00
Andrey Antukh
cbc5811290 Improve websockets impl
Make it more extensible and move all the websocket unrelated stuff
to the new hooks API. Also adds observability from repl.
2022-06-29 11:01:16 +02:00
Andrey Antukh
935639411c ⬆️ Update devenv to use latest ubuntu lts and jdk18 2022-06-29 10:59:50 +02:00
Andrey Antukh
6de78cabd4 ⬆️ Update shadow-cljs cljs compiler on frontend and common 2022-06-29 10:59:50 +02:00
Andrey Antukh
73f1418c95 🐛 Normalize return value from parse-client-ip function 2022-06-29 10:59:50 +02:00
Alejandro
cf2de3cfac Merge pull request #2030 from penpot/eva-palba-share-link
Eva palba share link
2022-06-29 10:55:16 +02:00
Alejandro Alonso
481c45ee60 Merge remote-tracking branch 'origin/staging' into develop 2022-06-29 10:38:35 +02:00
Alejandro
716b0639f2 Merge pull request #2057 from penpot/3565-community-access
🎉 Add new community links
2022-06-29 10:37:52 +02:00
elhombretecla
ced3830d7a 🎉 Add new coomunity info 2022-06-29 10:34:41 +02:00
Pablo Alba
115314e97c In view mode allow comment/inspect to non-team users (by shared link permissions) 2022-06-29 09:41:30 +02:00
Alejandro Alonso
d2250274f2 Merge remote-tracking branch 'origin/staging' into develop 2022-06-29 09:37:31 +02:00
Eva
0f04398e61 💄 Improve shared link modal 2022-06-29 09:31:41 +02:00
Eva Marco
72979e4535 Merge pull request #2056 from penpot/alotor-fix-resize
🐛 Fix problem with resize groups
2022-06-29 08:51:41 +02:00
alonso.torres
a271a285ad 🐛 Fix problem with resize groups 2022-06-29 08:48:00 +02:00
Andrey Antukh
b68407a6c0 Merge pull request #2054 from penpot/superalex-update-auth-urls-navigation
 Update auth urls navigation
2022-06-29 08:35:41 +02:00
Alejandro Alonso
5136eef4bc Update auth urls navigation 2022-06-29 08:05:22 +02:00
Alejandro
f132651175 Merge pull request #2055 from penpot/hiru-types
♻️ Rename specs -> types
2022-06-29 06:33:21 +02:00
Andrés Moya
6f94745aed ♻️ Rename specs -> types
NO FUNCTIONALITY IS CHANGED in this commit, only moving things around
2022-06-29 06:25:06 +02:00
Ahmad HosseinBor
7052f64547 🌐 Add translations for: Persian.
Currently translated at 53.4% (593 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-06-28 15:16:38 +02:00
Alejandro Alonso
29220cd0d3 Merge remote-tracking branch 'origin/staging' into develop 2022-06-28 12:24:42 +02:00
Eva Marco
ec55d64454 Merge pull request #2052 from penpot/superalex-fix-fill-information-not-complete-when-paste-plain-text
🐛 Fix fill information not complete when paste plain text
2022-06-28 12:21:54 +02:00
Alejandro Alonso
e4eb8004e2 🐛 Fix fill information not complete when paste plain text 2022-06-28 12:18:31 +02:00
Andrey Antukh
b1e6a8b1e9 Merge pull request #2051 from penpot/eva-component-name
 Show shape name in right toolbar
2022-06-28 11:37:09 +02:00
Eva
da2214379c Show shape name in right toolbar 2022-06-28 11:33:28 +02:00
Alejandro
4d19ceff8d Merge pull request #2016 from penpot/niwinz-experiments-custom-export-import
Experimental support for binary file format for exportation/importation of penpot files
2022-06-27 13:23:06 +02:00
Andrey Antukh
b944d977bb 🎉 Add binfile import/export internal functionality 2022-06-27 11:12:00 +02:00
Alejandro Alonso
07881eed65 Merge remote-tracking branch 'origin/staging' into develop 2022-06-27 09:28:54 +02:00
Alejandro
f2862b6c16 Merge pull request #2039 from penpot/niwinz-hotfix-exporter-uri-param
🐛 Remove unused setting on exporter
2022-06-27 08:44:39 +02:00
Andrey Antukh
ccae7cc2d4 📎 Clean and improve default docker config.env file 2022-06-27 07:40:06 +02:00
Pablo Alba
c6de41421e Merge pull request #2033 from penpot/circleci-changes
Circleci changes
2022-06-27 07:16:40 +02:00
Andrey Antukh
fa06da36ac 🐛 Remove unused setting on exporter
That causes many troubles on configuring exporter on the onpremise
instances but serves for nothing because it is completly unused.
2022-06-24 16:37:27 +02:00
Alejandro
03c019ded0 Merge pull request #2034 from wodin/wodin/fix-spelling-of-peek
📚 Fix spelling of 'sneak peek'
2022-06-24 13:31:21 +02:00
Alejandro
248ab953b2 Merge pull request #2038 from penpot/eva-bugfix-3
🐛 Fix color change in a row
2022-06-24 13:12:45 +02:00
Eva
14754aae05 🐛 Fix color change in a row 2022-06-24 12:35:23 +02:00
Alejandro
dc7464220d Merge pull request #2028 from penpot/alotor-frames
 Improved nested boards thumbnail handling
2022-06-24 11:49:58 +02:00
Alejandro
7396410267 Merge pull request #2037 from penpot/niwinz-fix-region-spec-on-s3-storage-backend
Fix spec on S3 storage backend region parameter
2022-06-24 11:07:19 +02:00
Alejandro
9bd3cba58c Merge pull request #2035 from penpot/eva-bugfix-shortcuts
🐛 Fix shortcut acction in main menu
2022-06-24 10:59:56 +02:00
Andrey Antukh
b08b1a546a 🐛 Fix region spec on s3 storage backend
This allows users use different region
2022-06-24 10:58:42 +02:00
alonso.torres
639eaa2458 Improved nested boards thumbnail handling 2022-06-24 10:47:33 +02:00
Eva
ab1405b79c 🐛 Fix shortcut acction in main menu 2022-06-24 09:55:29 +02:00
Michael Wood
ce14acac2c 📚 Fix spelling of 'sneak peek'
https://theoatmeal.com/comics/sneak_peek
2022-06-24 08:41:32 +02:00
Andrey Antukh
826bd29327 📎 Disable :non-arg-vec-return-type-hint linter on clj-kondo 2022-06-24 08:33:20 +02:00
Andrey Antukh
5151a7bd49 📎 Ignore linter issues on single function on frontend
Because it happens to the `new` function previuously defined
clash with the instance creation (probably linter bug).
2022-06-24 07:34:34 +02:00
Andrey Antukh
0ad0a65fa9 📎 Minor changes on circleci config 2022-06-24 07:29:14 +02:00
Alejandro
10a33fb102 Merge pull request #2027 from penpot/eva-bugfix-selected-colors
🐛 Fix modify colors in a row in selected colors
2022-06-23 15:57:34 +02:00
Alejandro
b0c0c6ed43 Merge pull request #2026 from penpot/niwinz-hotfix-20220623
Minor fixes
2022-06-23 15:53:54 +02:00
Eva
e31fbb5c5f 🐛 Fix modify colors in a row in selected colors 2022-06-23 15:07:40 +02:00
andy
e2bdf1a155 🌐 Add translations for: Spanish.
Currently translated at 100.0% (1110 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2022-06-23 14:19:39 +02:00
Andrey Antukh
5e2ff2cf6f 📎 Minor update on telemetry task 2022-06-23 14:17:26 +02:00
Andrey Antukh
c211e84498 🐛 Fix incorrect register-profile audit log handling 2022-06-23 14:17:26 +02:00
alonso.torres
75dc9e64a7 Merge remote-tracking branch 'origin/staging' into develop 2022-06-23 13:55:24 +02:00
Alejandro Alonso
69810750c5 📎 Tag new minor release 2022-06-23 13:47:27 +02:00
Alejandro
4549281b6c Merge pull request #2025 from penpot/alotor-fix-path-performance
 Improved performance when rendering paths
2022-06-23 13:46:59 +02:00
alonso.torres
90532b760a Improved performance when rendering paths 2022-06-23 13:24:03 +02:00
Alejandro
eb190296d7 Merge pull request #2021 from penpot/alotor-frames
 Fix shadows in frames for dashboard and viewer
2022-06-22 11:45:29 +02:00
Andrey Antukh
46d075611d ♻️ Adapt media & fonts handling to new tmp service
And storage backend changes
2022-06-22 11:39:57 +02:00
Andrey Antukh
ebcb385593 ♻️ Minor refactor on storages
Fix many issues on FS & S3 backend; removes the unused and broken
DB backend. Normalize operations on bytes and byte streams on a
separated namespace: app.util.bytes
2022-06-22 11:37:45 +02:00
alonso.torres
8e60834292 Fix shadows in frames for dashboard and viewer 2022-06-22 11:18:55 +02:00
Eva Marco
6469a543ba Merge pull request #2023 from penpot/niwinz-hotfix-20220622
🚑 Fix unexpected exception on typography asset context menu
2022-06-22 09:39:07 +02:00
Andrey Antukh
666b9fa4d4 🚑 Fix unexpected exception on typography asset context menu 2022-06-22 09:36:34 +02:00
Eva Marco
137c10f631 Merge pull request #2018 from penpot/eva-fix-double-click-viewer
🐛 Fix double click crash on viewer layers
2022-06-22 09:34:22 +02:00
Eva
ac1167d0c9 🐛 Fix double click crash on viewer layers 2022-06-22 09:31:13 +02:00
Eva Marco
e1d6cded62 Merge pull request #2019 from penpot/palba-view-mode-improvements-2
On view mode only show arrows on hover
2022-06-21 11:52:52 +02:00
Pablo Alba
53df0f7585 On view mode only show arrows on hover 2022-06-21 11:10:05 +02:00
alonso.torres
6d4e898f79 Merge remote-tracking branch 'origin/staging' into develop 2022-06-21 09:30:54 +02:00
Andrey Antukh
3a55f07f45 🐛 Remove duplicate work on storing already existing files in storage 2022-06-20 14:17:31 +02:00
Alejandro
408f73396f Merge pull request #2000 from penpot/alotor-frames
Nested/Rotated Boards
2022-06-20 11:51:03 +02:00
Ahmad HosseinBor
2d61497159 🌐 Add translations for: Persian.
Currently translated at 51.2% (569 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-06-19 06:19:08 +02:00
Wang Jiaxiang
c582ae667b 🌐 Add translations for: Chinese (Simplified).
Currently translated at 85.7% (952 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2022-06-19 06:19:06 +02:00
alonso.torres
e638475a67 Handoff handling nested frames 2022-06-17 14:26:32 +02:00
alonso.torres
1bde183c50 🐛 Fix thumbnails in viewer thumbnails 2022-06-17 14:24:37 +02:00
alonso.torres
45b690ed05 Fix shadows and thumbnails 2022-06-17 13:15:27 +02:00
alonso.torres
2799c09294 Fix interaction targets 2022-06-17 12:54:51 +02:00
alonso.torres
a774f4d4fa Fix guides, grids and constraints for nested frames 2022-06-17 12:54:51 +02:00
alonso.torres
2e3f443758 Fix problems with shadows and strokes for nested frames 2022-06-17 12:54:51 +02:00
alonso.torres
e0a1da6bca 🐛 Fix problems with thumbnails 2022-06-17 12:54:51 +02:00
alonso.torres
108291337d Improved frame indices 2022-06-17 12:54:51 +02:00
alonso.torres
ca326ac231 Fix dashboard thumbnails for nested frames 2022-06-17 12:54:51 +02:00
alonso.torres
566dde21a5 Fix viewer for new frames 2022-06-17 12:54:51 +02:00
alonso.torres
cab2b8469e Fix nested frames with thumbnails 2022-06-17 12:54:51 +02:00
alonso.torres
a37233be1e 🐛 Improved thumbnails rendering 2022-06-17 12:54:51 +02:00
alonso.torres
b4e218c13a Fix copy/paste for multiple frames 2022-06-17 12:54:51 +02:00
alonso.torres
9bd382f833 Fixed export/import for nested frames 2022-06-17 12:54:51 +02:00
alonso.torres
a4cc57886b Thumbnails for clipped and nested artboards 2022-06-17 12:54:51 +02:00
alonso.torres
0bb0063be4 Fix comments for nested frames 2022-06-17 12:54:51 +02:00
alonso.torres
79a46efa35 Create nested frames from selection 2022-06-17 12:54:51 +02:00
alonso.torres
c8ad379bf8 Adapted viewer for new frames 2022-06-17 12:54:50 +02:00
alonso.torres
8c5cc446b0 Improved hover behavior 2022-06-17 12:51:24 +02:00
alonso.torres
688ec2589a Changes in selection feedback 2022-06-17 12:51:24 +02:00
alonso.torres
aa584e6d35 ♻️ Refactor transform matrix 2022-06-17 12:51:24 +02:00
alonso.torres
a9303c37c4 Allow for nested frames 2022-06-17 12:51:24 +02:00
Alejandro
0bbd898173 Merge pull request #2002 from penpot/palba-improvements-view-mode
🎉 Improvements on view mode
2022-06-17 11:33:19 +02:00
Pablo Alba
ae468ecdf2 🎉 Improvements on view mode 2022-06-17 11:05:43 +02:00
Eva
8f7fd21454 New layout and layout item menur 2022-06-16 18:55:35 +02:00
Alejandro
65b6d1e07b Merge pull request #2001 from penpot/niwinz-telemetry-enhacements-2
Minor improvements
2022-06-15 12:29:04 +02:00
Andrey Antukh
adf2d82a52 🎉 Add proper logging reports on audit-log-archive task 2022-06-15 12:21:23 +02:00
Andrey Antukh
dce479bc4b Make the pool initialization process and defaults reusable
And add the ability to skip pool initialization if no enough data is
provided. Mainly for initialize pools based on configuration for not
essential/dynamic services.
2022-06-15 12:19:16 +02:00
Andrey Antukh
199360efa6 📎 Update default repl script 2022-06-15 12:18:39 +02:00
Alejandro Alonso
943fa880a7 Merge remote-tracking branch 'origin/staging' into develop 2022-06-15 12:00:31 +02:00
Locness
963df4b44f 🌐 Add translations for: French.
Currently translated at 70.2% (780 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-06-15 10:19:07 +02:00
Alexandre Pawlak
32b2b46df7 🌐 Add translations for: French.
Currently translated at 70.2% (780 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-06-15 10:19:07 +02:00
Alejandro
58a1060ed8 Merge pull request #1997 from penpot/niwinz-make-s3-storace-region-optional
 Make the region param optional on s3 storage backend
2022-06-14 13:33:10 +02:00
Alejandro
b3f8d98c34 Merge pull request #1996 from penpot/niwinz-im4java-fix
⬆️ Update im4java version to our internal fork version
2022-06-14 13:11:07 +02:00
Andrey Antukh
20f357d75d Make the region param optional on s3 storage backend
Defaulting to the eu-central-1
2022-06-14 12:13:47 +02:00
Andrey Antukh
9ae5528355 ⬆️ Update im4java version to our internal fork version
It fixes the v7 compatibility issues. Now, adding the -Dim4java.useV7=true
property to the java command when executing the penpot backend bundle it
switches to use the `magick` (ImageMagick v7 CLI) instead of `convert`
and `identify`.
2022-06-14 11:09:48 +02:00
Alexandre Pawlak
e7e231b719 🌐 Add translations for: French.
Currently translated at 67.5% (750 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-06-14 09:44:28 +02:00
Alejandro Alonso
69cb626cab Merge remote-tracking branch 'origin/staging' into develop 2022-06-14 09:14:07 +02:00
Alejandro
5870d25bec Merge pull request #1993 from penpot/niwinz-update-deps
⬆️ Update deps & linter fixes
2022-06-13 15:53:40 +02:00
Andrey Antukh
6190ce9b35 🐛 Add missing resolver to frontend docker image 2022-06-13 14:44:40 +02:00
Andrey Antukh
65753cdc17 ⬆️ Update yetti dep (fix multipart field size validation params handling) 2022-06-13 13:42:32 +02:00
Andrey Antukh
1174590af4 📎 Add hack for devtools unhandled rejection 2022-06-13 13:10:36 +02:00
Andrey Antukh
e5cb5860a8 ⬆️ Update cuerdas dep (fixes dm/str nil handling) 2022-06-13 13:01:31 +02:00
Andrey Antukh
65e99cabbf 📎 Fix linter issues
Related to the linter update on devenv
2022-06-13 11:18:35 +02:00
Andrey Antukh
97bf20dd4c ⬆️ Update dependencies 2022-06-13 11:18:02 +02:00
ascarida
c26273c9b3 🌐 Add translations for: Galician.
Currently translated at 12.7% (142 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/gl/
2022-06-11 11:14:34 +02:00
Alexandre Pawlak
7e1a771e24 🌐 Add translations for: French.
Currently translated at 67.3% (748 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-06-11 11:14:33 +02:00
Locness
fa7b0d3b35 🌐 Add translations for: French.
Currently translated at 67.3% (748 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-06-11 11:14:33 +02:00
Alejandro Alonso
d9420081c4 Merge remote-tracking branch 'origin/staging' into develop 2022-06-09 13:45:25 +02:00
Alejandro Alonso
e6b1c578d4 Merge remote-tracking branch 'origin/staging' into develop 2022-06-09 08:55:23 +02:00
andy
437e352bf4 🌐 Added translation for: Galician. 2022-06-08 08:05:32 +02:00
alonso.torres
288e6e1ea1 Merge remote-tracking branch 'origin/staging' into develop 2022-06-07 13:02:32 +02:00
alonso.torres
a8c3ac630d ⬆️ Update to version 1.15.0-beta 2022-06-06 15:27:55 +02:00
alonso.torres
da6675c91e 📚 Update changelog 2022-06-06 15:26:40 +02:00
Oğuz Ersen
3b26ec6b8c 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1110 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2022-06-05 15:15:47 +02:00
Ahmad HosseinBor
71ce0b66e0 🌐 Add translations for: Persian.
Currently translated at 28.4% (316 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-06-03 21:13:58 +02:00
Ahmad HosseinBor
784365f45c 🌐 Add translations for: Persian.
Currently translated at 27.4% (305 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-05-31 15:14:29 +02:00
Vincas Dundzys
05aee3507a 🌐 Add translations for: Lithuanian.
Currently translated at 8.9% (99 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lt/
2022-05-28 23:16:02 +02:00
Radek Sawicki
f651b7585d 🌐 Add translations for: Polish.
Currently translated at 100.0% (1110 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2022-05-28 23:16:02 +02:00
Yaron Shahrabani
68e603a86c 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1110 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2022-05-28 23:16:01 +02:00
Andrés Moya
52adf7eaf1 🌐 Add translations for: English.
Currently translated at 100.0% (1110 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/en/
2022-05-28 23:16:00 +02:00
Andrés Moya
8d51e32c5a 🌐 Add translations for: Spanish.
Currently translated at 100.0% (1110 of 1110 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2022-05-25 17:29:50 +02:00
851 changed files with 75399 additions and 471103 deletions

View File

@@ -11,6 +11,7 @@ jobs:
- image: cimg/redis:6.2.6
working_directory: ~/repo
resource_class: large
environment:
# Customize the JVM maximum heap limit
@@ -26,6 +27,8 @@ jobs:
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: cd .clj-kondo && cat config.edn
- run:
name: common lint
working_directory: "./common"
@@ -76,16 +79,16 @@ jobs:
environment:
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
# - run:
# working_directory: "./common"
# name: common tests (cljs)
# command: |
# yarn install
# yarn run compile-test
# node target/test.js
#
# environment:
# PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
- run:
working_directory: "./common"
name: common tests (cljs)
command: |
yarn install
yarn run compile-test
node target/test.js
environment:
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
- run:
working_directory: "./common"

View File

@@ -2,8 +2,8 @@
{promesa.core/let clojure.core/let
promesa.core/->> clojure.core/->>
promesa.core/-> clojure.core/->
rumext.alpha/defc clojure.core/defn
rumext.alpha/fnc clojure.core/fn
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
@@ -38,6 +38,9 @@
:single-key-in
{:level :warning}
:non-arg-vec-return-type-hint
{:level :off}
:redundant-do
{:level :off}

View File

@@ -53,24 +53,37 @@
[{:keys [:node]}]
(let [[rnode rtype ?meta & other] (:children node)
rsym (gensym (name (:k rtype)))
result (api/list-node
[(api/token-node (symbol "do"))
(api/list-node
[(api/token-node (symbol "declare"))
(api/token-node rsym)])
(if (= :map (:tag ?meta))
(api/list-node
[(api/token-node (symbol "reset-meta!"))
(api/token-node rsym)
?meta])
(api/list-node
[(api/token-node (symbol "comment"))
(api/token-node rsym)]))
(api/list-node
(into [(api/token-node (symbol "defmethod"))
(api/token-node rsym)
rtype]
(cons ?meta other)))])]
;; (prn "==============" rtype (into {} ?meta))
[?docs other] (if (api/string-node? ?meta)
[?meta other]
[nil (cons ?meta other)])
[?meta other] (let [?meta (first other)]
(if (api/map-node? ?meta)
[?meta (rest other)]
[nil other]))
nodes [(api/token-node (symbol "do"))
(api/list-node
[(api/token-node (symbol "declare"))
(api/token-node rsym)])
(when ?docs
(api/list-node
[(api/token-node (symbol "comment")) ?docs]))
(when ?meta
(api/list-node
[(api/token-node (symbol "reset-meta!"))
(api/token-node rsym)
?meta]))
(api/list-node
(into [(api/token-node (symbol "defmethod"))
(api/token-node rsym)
rtype]
other))]
result (api/list-node (filterv some? nodes))]
;; (prn "=====>" rtype)
;; (prn (api/sexpr result))
{:node result}))

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@
/backend/resources/public/assets
/backend/resources/public/media
/backend/target/
/backend/builtin-templates
/bundle*
/cd.md
/clj-profiler/

View File

@@ -1,5 +1,217 @@
# CHANGELOG
## 1.16.0-beta
### :boom: Breaking changes & Deprecations
- Removed the support for v2 internal file data blob format. This
version has never been documented nor set as default value so
technicaly this is not a breaking change because we are removing
a "private API".
### :sparkles: New features
- Improve interactions with nested boards [Taiga #4054](https://tree.taiga.io/project/penpot/us/4054)
- Add team hero in projects dashboard [Taiga #3863](https://tree.taiga.io/project/penpot/us/3863)
- Add zoom style to shared link [Taiga #3874](https://tree.taiga.io/project/penpot/us/3874)
- Add dashboard creation button as placeholder [Taiga #3861](https://tree.taiga.io/project/penpot/us/3861)
- Improve invitation flow on onboarding [Taiga #3241](https://tree.taiga.io/project/penpot/us/3241)
- Add new text to initial modals [Taiga #3458](https://tree.taiga.io/project/penpot/us/3458)
- Add new questions to onboarding [Taiga #3462](https://tree.taiga.io/project/penpot/us/3462)
- Add cosmetic changes in viewer mode [Taiga #3688](https://tree.taiga.io/project/penpot/us/3688)
- Outline highlights on layer hovering [Taiga #2645](https://tree.taiga.io/project/penpot/us/2645) by @andrewzhurov
- Add zoom to shape on double click up on its icon [Taiga #3929](https://tree.taiga.io/project/penpot/us/3929) by @andrewzhurov
- Add Libraries & Templates carousel [Taiga #3860](https://tree.taiga.io/project/penpot/us/3860)
- Ungroup frames [Taiga #4012](https://tree.taiga.io/project/penpot/us/4012)
- Newsletter Opt-in options for subscription categories [Taiga #3242](https://tree.taiga.io/project/penpot/us/3242)
- Print emails to console by default if smtp is disabled
- Add `email-verification` flag for enable/disable email verification
- Make graphics thumbnails load lazy [Taiga #4252](https://tree.taiga.io/project/penpot/issue/4252)
### :bug: Bugs fixed
- Fix unexpected removal of guides on copy&paste frames [Taiga #3887](https://tree.taiga.io/project/penpot/issue/3887) by @andrewzhurov
- Fix props preserving on copy&paste texts [Taiga #3629](https://tree.taiga.io/project/penpot/issue/3629) by @andrewzhurov
- Fix unexpected layers ungrouping on moving it [Taiga #3932](https://tree.taiga.io/project/penpot/issue/3932) by @andrewzhurov
- Fix artboards moving with comment tool selected [Taiga #3938](https://tree.taiga.io/project/penpot/issue/3938)
- Fix undo on delete page does not preserve its order [Taiga #3375](https://tree.taiga.io/project/penpot/issue/3375)
- Fix unexpected 404 on deleting library that is used by deleted files
- Fix inconsistent message on deleting library when a library is linked from deleted files
- Fix change multiple colors with SVG [Taiga #3889](https://tree.taiga.io/project/penpot/issue/3889)
- Fix ungroup does not work for typographies [Taiga #4195](https://tree.taiga.io/project/penpot/issue/4195)
- Fix inviting to non existing users can fail [Taiga #4108](https://tree.taiga.io/project/penpot/issue/4108)
- Fix components marked as touched when moved [Taiga #4061](https://tree.taiga.io/project/penpot/task/4061)
- Fix boards grouped shouldn't show the title [Taiga #4251](https://tree.taiga.io/project/penpot/issue/4251)
- Fix gradient handlers are under resize handlers[Taiga #4298](https://tree.taiga.io/project/penpot/issue/4298)
- Fix grid not syncing immediately in multiuser [Taiga #4339](https://tree.taiga.io/project/penpot/issue/4339)
- Fix custom font upload fails silently for unsupported formats [Taiga #4279](https://tree.taiga.io/project/penpot/issue/4280)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
- To @andrewzhurov for many code contributions on this release.
- UI improvements in Project section (by @Waishnav) [#2285](https://github.com/penpot/penpot/pull/2285)
## 1.15.5-beta
### :bug: Bugs fixed
- Fix artboard border radius [Taiga #4291](https://tree.taiga.io/project/penpot/issue/4291)
- Fix copied & pasted layer is not visible [Taiga #4283](https://tree.taiga.io/project/penpot/issue/4283)
- Fix notification to newsletter is shown in all cases [Taiga #4367](https://tree.taiga.io/project/penpot/issue/4367)
- Fix comments section is not scrolling by mouse wheel [Taiga #4305](https://tree.taiga.io/project/penpot/issue/4305)
## 1.15.4-beta
### :bug: Bugs fixed
- Fix social buttons in register form [Taiga #4320](https://tree.taiga.io/project/penpot/issue/4320)
- Remove gitter information from feedback page [Taiga #4157](https://tree.taiga.io/project/penpot/issue/4157)
- Fix overlay remains open on frame change [Taiga #4066](https://tree.taiga.io/project/penpot/issue/4066)
- Fix toggle overlay position [Taiga #4091](https://tree.taiga.io/project/penpot/issue/4091)
- Fix overlay closed on clicked outside [Taiga #4027](https://tree.taiga.io/project/penpot/issue/4027)
- Fix animate multiple overlays [Taiga #3993](https://tree.taiga.io/project/penpot/issue/3993)
- Fix problem with snap to grids [#2221](https://github.com/penpot/penpot/issues/2221)
- Fix issue when scaling to value 0 [#2252](https://github.com/penpot/penpot/issues/2252)
- Fix problem when moving shapes inside nested frames [Taiga #4113](https://tree.taiga.io/project/penpot/issue/4113)
- Fix color type icon does not change [Taiga #4133](https://tree.taiga.io/project/penpot/issue/4133)
- Fix recent colors are not working [Taiga #4153](https://tree.taiga.io/project/penpot/issue/4153)
- Fix change opacity in colorpicker cause bugged color [Taiga #4154](https://tree.taiga.io/project/penpot/issue/4154)
- Fix gradient colors don't arrive in recent colors palette (https://tree.taiga.io/project/penpot/issue/4155)
- Fix selected colors allow gradients in shadows [Taiga #4156](https://tree.taiga.io/project/penpot/issue/4156)
- Fix import files with unexpected format or invalid content [Taiga #4136](https://tree.taiga.io/project/penpot/issue/4136)
- Fix wrong shortcut button tip of "Delete" function [Taiga #4162](https://tree.taiga.io/project/penpot/issue/4162)
- Fix error after user drags any layer in search functionality [Taiga #4161](https://tree.taiga.io/project/penpot/issue/4161)
- Fix font search works only with lowercase letters [Taiga #4140](https://tree.taiga.io/project/penpot/issue/4140)
- Fix Terms and Privacy links overlapping [Taiga #4137](https://tree.taiga.io/project/penpot/issue/4137)
- Fix Export bounding box mask [Taiga #950](https://tree.taiga.io/project/penpot/issue/950)
- Fix delete layers in bulk [Taiga #4160](https://tree.taiga.io/project/penpot/issue/4160)
- Fix Cannot take out an element from a group at layers panel by drag [Taiga #4209](https://tree.taiga.io/project/penpot/issue/4209)
- Fix Internal error when resending invitation email [Taiga #4212](https://tree.taiga.io/project/penpot/issue/4212)
- Fix PDF exportation order [Taiga #4216](https://tree.taiga.io/project/penpot/issue/4216)
- Fix some typos [Taiga #4215](https://tree.taiga.io/project/penpot/issue/4215)
- Fix "no boards" message in viewer [Taiga #4243](https://tree.taiga.io/project/penpot/issue/4243)
- Fix view mode login size [Taiga #4210](https://tree.taiga.io/project/penpot/issue/4210)
## 1.15.3-beta
### :bug: Bugs fixed
- Fix default value of grow type in texts [Taiga #4034](https://tree.taiga.io/project/penpot/issue/4034)
- Fix error when moving nested frames outside [Taiga #4017](https://tree.taiga.io/project/penpot/issue/4017)
- Fix problem when hovering over nested frames [Taiga #4018](https://tree.taiga.io/project/penpot/issue/4018)
- Fix problem editing rotated texts [Taiga #4026](https://tree.taiga.io/project/penpot/issue/4026)
- Fix problem with texts for non existing fonts [Taiga #4087](https://tree.taiga.io/project/penpot/issue/4087)
- Fix undo after moving layers will wrongly order the layers [Taiga #3344](https://tree.taiga.io/project/penpot/issue/3344)
- Fix grouping typographies by drag & drop does not work (again) [#2203](https://github.com/penpot/penpot/issues/2203)
- Fix when ungrouping, the items previously grouped should ALWAYS remain selected [Taiga #4064](https://tree.taiga.io/project/penpot/issue/4064)
- Change shortcut for "Clear undo" [#2219](https://github.com/penpot/penpot/issues/2219)
## 1.15.2-beta
### :bug: Bugs fixed
- Fix problem with multi-user text editing [Taiga #3446](https://tree.taiga.io/project/penpot/issue/3446)
- Fix path tools blocking elements underneath [#2050](https://github.com/penpot/penpot/issues/2050)
- Fix frame titles deforming when resize [#2207](https://github.com/penpot/penpot/issues/2207)
- Fix export simple line path [#3890](https://tree.taiga.io/project/penpot/issue/3890)
- Fix color-picker recent colors [Taiga #4013](https://tree.taiga.io/project/penpot/issue/4013)
## 1.15.1-beta
### :bug: Bugs fixed
- Fix shadows doesn't work on nested artboards [Taiga #3886](https://tree.taiga.io/project/penpot/issue/3886)
- Fix problems with double-click and selection [Taiga #4005](https://tree.taiga.io/project/penpot/issue/4005)
- Fix mismatch between editor and displayed text in workspace [Taiga #3975](https://tree.taiga.io/project/penpot/issue/3975)
- Fix validation error on text position [Taiga #4010](https://tree.taiga.io/project/penpot/issue/4010)
- Fix objects jitter while scrolling [Github #2167](https://github.com/penpot/penpot/issues/2167)
- Fix on color-picker, click+drag adds lots of recent colors [Taiga #4013](https://tree.taiga.io/project/penpot/issue/4013)
- Fix opening profile URL while signed out takes to "your account" section[Taiga #3976](https://tree.taiga.io/project/penpot/issue/3976)
## 1.15.0-beta
### :boom: Breaking changes & Deprecations
- The `PENPOT_LOGIN_WITH_LDAP` environment variable is finally removed (after
many version with deprecation). It is replaced with the
`enable-login-with-ldap` flag.
- The `PENPOT_LDAP_ATTRS_PHOTO` finally removed, it was unused for many
versions.
- If you are using social login (google, github, gitlab or generic OIDC) you
will need to ensure to add the following flags respectivelly to let them
enabled: `enable-login-with-google`, `enable-login-with-github`,
`enable-login-with-gitlab` and `enable-login-with-oidc`. If not, they will
remain disabled after application start independently if you set the client-id
and client-sectet options.
- The `PENPOT_REGISTRATION_ENABLED` is finally removed in favour of
`<enable|disable>-registration` flag.
- The OIDC providers are now initialized synchronously, and if you are using the
discovery mechanism of the generic OIDC integration, the start time of the
application will depend on how fast the OIDC provider responds to the
discovery http request.
### :sparkles: New features
- Add some cosmetic changes in viewer mode [Taiga #3688](https://tree.taiga.io/project/penpot/us/3688)
- Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982)
- View mode improvements to enable access and use in different conditions [Taiga #3023](https://tree.taiga.io/project/penpot/us/3023)
- Improved share link options. Now you can allow non-team members to comment and/or inspect [Taiga #3056] (https://tree.taiga.io/project/penpot/us/3056)
- Signin/Signup from shared link [Taiga #3472](https://tree.taiga.io/project/penpot/us/3472)
- Support for import/export binary format [Taiga #2991](https://tree.taiga.io/project/penpot/us/2991)
- Comments positioning [Taiga #2007](https://tree.taiga.io/project/penpot/us/2007)
- Select all inside a group select only the objects at this group level [Taiga #2382](https://tree.taiga.io/project/penpot/issue/2382)
- Make the media maximum upload size configurable
### :bug: Bugs fixed
- Fix viewer scroll problems [Taiga 3403](https://tree.taiga.io/project/penpot/issue/3403)
- Fix hide html options on handoff [Taiga 3533](https://tree.taiga.io/project/penpot/issue/3533)
- Fix share prototypes overlay and stroke [Taiga #3994](https://tree.taiga.io/project/penpot/issue/3994)
- Fix border radious on boolean operations [Taiga #3959](https://tree.taiga.io/project/penpot/issue/3959)
- Fix inconsistent representation of rectangles [Taiga #3977](https://tree.taiga.io/project/penpot/issue/3977)
- Fix recent fonts info [Taiga #3953](https://tree.taiga.io/project/penpot/issue/3953)
- Fix clipped elements affect boards and centering [Taiga #3666](https://tree.taiga.io/project/penpot/issue/3666)
- Fix intro action in multi input [Taiga #3541](https://tree.taiga.io/project/penpot/issue/3541)
- Fix team default image [Taiga #3919](https://tree.taiga.io/project/penpot/issue/3919)
- Fix problem with group coordinates [#2008](https://github.com/penpot/penpot/issues/2008)
- Fix problem with line-height and texts [Taiga #3578](https://tree.taiga.io/project/penpot/issue/3578)
- Fix moving frame-guides outside frames [Taiga #3839](https://tree.taiga.io/project/penpot/issue/3839)
- Fix problem with 180 degree rotations [#2082](https://github.com/penpot/penpot/issues/2082)
- Fix font rendering on grid thumbnails [Taiga #3473](https://tree.taiga.io/project/penpot/issue/3473)
- Fix Drag and drop font assets in groups [Taiga #3763](https://tree.taiga.io/project/penpot/issue/3763)
- Fix copy and paste layers order [Taiga #1617](https://tree.taiga.io/project/penpot/issue/1617)
- Fix unexpected removal of guides on copy&paste frames [Taiga #3887](https://tree.taiga.io/project/penpot/issue/3887) by @andrewzhurov
- Fix props preserving on copy&paste texts [Taiga #3629](https://tree.taiga.io/project/penpot/issue/3629) by @andrewzhurov
- Fix unexpected layers ungrouping on moving it [Taiga #3932](https://tree.taiga.io/project/penpot/issue/3932) by @andrewzhurov
- Fix unexpected exception and behavior on colorpicker with gradients [Taiga #3448](https://tree.taiga.io/project/penpot/issue/3448)
- Fix multiselection with shift not working inside a library group [Taiga #3532](https://tree.taiga.io/project/penpot/issue/3532)
- Fix drag and drop graphic assets in groups [Taiga #4002](https://tree.taiga.io/project/penpot/issue/4002)
- Fix bringing complete file data when launching the export dialog [Taiga #4006](https://tree.taiga.io/project/penpot/issue/4006)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.14.2-beta
### :bug: Bugs fixed
- Fix colors from unlinked libs in color selected widget [Taiga #3712](https://tree.taiga.io/project/penpot/issue/3712)
- Fix fill information not complete when paste plain text [Taiga #3680](https://tree.taiga.io/project/penpot/issue/3680)
- Fix problem when resizing groups [Taiga #3702](https://tree.taiga.io/project/penpot/issue/3702)
- Fix issues on typographies assets grouping [#2073](https://github.com/penpot/penpot/issues/2073)
- Fix text positioning inconsistencies between browsers
## 1.14.1-beta
### :bug: Bugs fixed
- Fix shortcut access in main menu [Taiga #3672](https://tree.taiga.io/project/penpot/issue/3672)
- Fix modify colors in a row in selected colors [Taiga #3653](https://tree.taiga.io/project/penpot/issue/3653)
- Fix crash when double click on viewer assets [Taiga #3625](https://tree.taiga.io/project/penpot/issue/3625)
- Fix right click on typographies assets [Taiga #3638](https://tree.taiga.io/project/penpot/issue/3638)
## 1.14.0-beta
### :sparkles: New features
@@ -13,6 +225,7 @@
- Navigate to the original link after log in [Taiga #3624](https://tree.taiga.io/project/penpot/issue/3624)
### :bug: Bugs fixed
- Fix menu file not accessible in certain conditions [Taiga #3385](https://tree.taiga.io/project/penpot/issue/3385)
- Remove deprecated menu options [Taiga #3333](https://tree.taiga.io/project/penpot/issue/3333)
- Prototype connection should be under the rules [Taiga #3384](https://tree.taiga.io/project/penpot/issue/3384)

137
README.md
View File

@@ -12,76 +12,122 @@
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
![PENPOT](https://penpot.app/images/readme/home-ui.jpg)
<p align="center">
<a href="https://penpot.app/"><b>Website</b></a> •
<a href="https://help.penpot.app/technical-guide/getting-started/"><b>Getting Started</b></a> •
<a href="https://help.penpot.app/user-guide/"><b>User Guide</b></a> •
<a href="https://help.penpot.app/user-guide/introduction/info/"><b>Tutorials & Info</b></a> •
<a href="https://community.penpot.app/"><b>Community</b></a> •
<a href="https://twitter.com/penpotapp"><b>Twitter</b></a> •
<a href="https://instagram.com/penpot.app"><b>Instagram</b></a> •
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
<a href="https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g"><b>Youtube</b></a>
</p>
![feature-readme](https://user-images.githubusercontent.com/1045247/189871786-0b44f7cf-3a0a-4445-a87b-9919ec398bf7.gif)
## What is Penpot? ##
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.
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 web standards (SVG). For all and empowered by the community.
## Table of contents ##
- [How to use](#how-to-use)
- [Help center](#help-center)
- [Contributing](#contributing)
- [Give feedback](#give-feedback)
- [Tutorials](#tutorials)
- [Why Penpot](#why-penpot)
- [Getting Started](#getting-started)
- [Community](#community)
- [Resources](#resources)
- [License](#license)
## How to use ##
## Why Penpot ##
Login or Register on our Penpot cloud app. Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** by your own.
Penpot makes design and prototyping accessible to every team in the world.
✏️ [Start using Penpot](https://design.penpot.app)
### For cross-domain teams ###
We have a clear focus on design and code teams and our capabilities reflect exactly that. The less hand-off mindset, the more fun for everyone.
You can also install Penpot in a local environment. This section details everything you need to know to get Penpot up and running in production environments. Although it can be installed in many ways, the recommended approach is using **docker** and **docker-compose**.
### Multiplatform ###
Being web based, Penpot is not dependent on operating systems or local installations, you will only need to run a modern browser.
🐳 [Install docker](https://help.penpot.app/technical-guide/getting-started/)
## Help center ##
In this documentation you will find (almost) everything you need to know about how to work with Penpot. From the interface basics to advanced functionality.
📖 [User guide](https://help.penpot.app/user-guide/)
❓ [FAQs](https://help.penpot.app/faqs/)
🖥️ [Technical guide](https://help.penpot.app/technical-guide/)
❤️ [Contributing guide](https://help.penpot.app/contributing-guide/)
![User guide](https://penpot.app/images/readme/help-center.jpg)
## Contributing ##
### Open Standards ###
Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work.
<p align="center">
<img src="https://penpot.app/images/open-source.png" alt="Open Source">
</p>
**Open to you!**
We love the open source software community. Contributing is our
passion and because of this, we'll be glad if you want to participate
and improve Penpot. All your awesome ideas and code are welcome!
## Getting started ##
Please refer to the [Contributing Guide](./CONTRIBUTING.md)
### Install with Elestio ###
[Elestio](https://elest.io/) offers a fully managed service for on-premise instances of a selection of open-source software! This means you can deploy a dedicated instance of Penpot in just 3 minutes with no technical knowledge needed.
## Give feedback ##
You dont need to worry about DNS configuration, SMTP, backups, SSL certificates, OS & Penpot upgrades, and much more.
[Get started with Elestio.](https://help.penpot.app/technical-guide/getting-started/#install-with-elestio)
### Install with Docker ###
You can also get started with Penpot locally or self-host it with **docker** and **docker-compose**.
Heres a step-by-step guide on [getting started with Docker.](https://help.penpot.app/technical-guide/getting-started/#install-with-docker)
### Penpot cloud app ###
If you prefer not to install Penpot in a local environment, [login or register on our Penpot cloud app](https://design.penpot.app). Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** on your own.
<p align="center">
<img src="https://help.penpot.app/img/home-techguide.png" alt="Getting started">
</p>
## Community ##
We love the open source software community. Contributing is our passion and if its yours too, [participate](https://community.penpot.app/) and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your ideas and code are welcome!
If you need help or have any questions; if youd like to share your experience using Penpot or get inspired; if youd rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)!
You will find the following categories:
- [Ask the Community](https://community.penpot.app/c/ask-for-help-using-penpot/6)
- [Troubleshooting](https://community.penpot.app/c/technical/8)
- [Help us Improve Penpot](https://community.penpot.app/c/help-us-improve-penpot/7)
- [#MadeWithPenpot](https://community.penpot.app/c/madewithpenpot/9)
- [Events and Announcements](https://community.penpot.app/c/announcements/5)
- [Inside Penpot](https://community.penpot.app/c/inside-penpot/21)
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
<p align="center">
<img src="https://penpot.app/images/cross-teams.webp" alt="Community">
</p>
## Contributing ##
Every sort of contribution will be very helpful to enhance Penpot. How youll participate? All your ideas, designs and code are welcome:
- Invite your [team to join](https://design.penpot.app/#/auth/register)
- Star this repo and follow us on Social Media: [Twitter](https://twitter.com/penpotapp), [Instagram](https://instagram.com/penpot.app), [Youtube](https://www.youtube.com/c/Penpot) or [Mastodon](https://fosstodon.org/@penpot/).
- Participate in the [Community](https://community.penpot.app/) asking and answering questions, reacting to others articles or opening your own conversations.
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues)
- Create and [share Libraries & templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
- Become a [translator](https://help.penpot.app/contributing-guide/translations)
- Give feedback: [Mail us](mailto:support@penpot.app)
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/).
<p align="center">
<img src="https://help.penpot.app/img/home-contributing.png" alt="Contributing">
</p>
## Resources ##
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
✉️ [Mail us](mailto:info@penpot.app)
💾 [Documentation](https://help.penpot.app/technical-guide/)
💬 [GitHub discussions](https://github.com/penpot/penpot/discussions)
🚀 [Getting Started](https://help.penpot.app/technical-guide/getting-started/)
🐞 [GitHub issues](https://github.com/penpot/penpot/issues)
✏️ [Tutorials](https://www.youtube.com/playlist?list=PLgcCPfOv5v54WpXhHmNO7T-YC7AE-SRsr)
✍️ [Gitter](https://gitter.im/penpot/community)
🏘 [Architecture](https://help.penpot.app/technical-guide/architecture/)
## Tutorials ##
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
Would you like to know more about Penpot? We recommend you to visit our youtube channel and learn more about the functionalities and possibilities of Penpot with our video tutorials.
🎞️ [YouTube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g)
## License ##
@@ -90,5 +136,6 @@ 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) UXBOX Labs SL
Copyright (c) KALEIDOS INC
```
Penpot is a Kaleidos [open source project](https://kaleidos.net/products)

88
THANKYOU.md Normal file
View File

@@ -0,0 +1,88 @@
# THANK YOU
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/)
## 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)
* [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)
* [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)
* [devadarta](https://hosted.weblate.org/user/devadarta)
* [diacritica](https://hosted.weblate.org/user/diacritica)
* [dundzys.vincas](dundzys.https://hosted.weblate.org/user/vincas)
* [Eranot](https://hosted.weblate.org/user/Eranot)
* [erral](https://hosted.weblate.org/user/erral)
* [ersen](https://hosted.weblate.org/user/ersen)
* [filipepessanha](https://hosted.weblate.org/user/filipepessanha)
* [fortx](https://hosted.weblate.org/user/fortx)
* [foxbit](https://hosted.weblate.org/user/foxbit)
* [georgelemon](https://hosted.weblate.org/user/georgelemon)
* [girafic](https://hosted.weblate.org/user/girafic)
* [gizemb](https://hosted.weblate.org/user/gizemb)
* [greench](https://hosted.weblate.org/user/greench)
* [guidimas](https://hosted.weblate.org/user/guidimas)
* [hfigueira_1](https://hosted.weblate.org/user/hfigueira_1)
* [hifiaz](https://hosted.weblate.org/user/hifiaz)
* [httpsterio](https://hosted.weblate.org/user/httpsterio)
* [humteus](https://hosted.weblate.org/user/humteus)
* [iblueer](https://hosted.weblate.org/user/iblueer)
* [insan](https://hosted.weblate.org/user/insan)
* [Iphi](https://hosted.weblate.org/user/Iphi)
* [iWangJiaxiang](https://hosted.weblate.org/user/iWangJiaxiang)
* [jancborchardt](https://hosted.weblate.org/user/jancborchardt)
* [jazz](https://hosted.weblate.org/user/jazz)
* [johnterroa](https://hosted.weblate.org/user/johnterroa)
* [jponsa](https://hosted.weblate.org/user/jponsa)
* [kapler](https://hosted.weblate.org/user/kapler)
* [kingu](https://hosted.weblate.org/user/kingu)
* [KnahkAmath](https://hosted.weblate.org/user/KnahkAmath)
* [laminne](https://hosted.weblate.org/user/laminne)
* [lenildoleite](https://hosted.weblate.org/user/lenildoleite)
* [liimee](https://hosted.weblate.org/user/liimee)
* [lixeix](https://hosted.weblate.org/user/lixeix)
* [locness3](https://hosted.weblate.org/user/locness3)
* [maiwann](https://hosted.weblate.org/user/maiwann)
* [MidooDj](https://hosted.weblate.org/user/MidooDj)
* [Mohamed_amine_gdoura](https://hosted.weblate.org/user/Mohamed_amine_gdoura)
* [myfunnyandy](https://hosted.weblate.org/user/myfunnyandy)
* [NampoinaRal](https://hosted.weblate.org/user/NampoinaRal)
* [nautilusx](https://hosted.weblate.org/user/nautilusx)
* [niwinz](https://hosted.weblate.org/user/niwinz)
* [pablo.alba](pablo.https://hosted.weblate.org/user/alba)
* [PhilippeAccorsi](https://hosted.weblate.org/user/PhilippeAccorsi)
* [rnarius](https://hosted.weblate.org/user/rnarius)
* [rnd](https://hosted.weblate.org/user/rnd)
* [RuanAragao](https://hosted.weblate.org/user/RuanAragao)
* [ruben](https://hosted.weblate.org/user/ruben)
* [semonxue](https://hosted.weblate.org/user/semonxue)
* [shahab](https://hosted.weblate.org/user/shahab)
* [shuaib85](https://hosted.weblate.org/user/shuaib85)
* [SiderealArt](https://hosted.weblate.org/user/SiderealArt)
* [swapnil.cx](swapnil.https://hosted.weblate.org/user/cx)
* [syuza](https://hosted.weblate.org/user/syuza)
* [th3ph4nt0m](https://hosted.weblate.org/user/th3ph4nt0m)
* [tiwb](https://hosted.weblate.org/user/tiwb)
* [tommi](https://hosted.weblate.org/user/tommi)
* [val](https://hosted.weblate.org/user/val)
* [vikt](https://hosted.weblate.org/user/vikt)
* [VinLin](https://hosted.weblate.org/user/VinLin)
* [vintprox](https://hosted.weblate.org/user/vintprox)
* [Voxybuns](https://hosted.weblate.org/user/Voxybuns)
* [winie](https://hosted.weblate.org/user/winie)
* [Yaron](https://hosted.weblate.org/user/Yaron)
* [yrd](https://hosted.weblate.org/user/yrd)
* [YukiYuigishi](https://hosted.weblate.org/user/YukiYuigishi)
* [zcraber](https://hosted.weblate.org/user/zcraber)

View File

@@ -33,4 +33,4 @@
{:src-dirs ["dev/java"]
:class-dir class-dir
:basis basis
:javac-opts ["-source" "11" "-target" "11"]}))
:javac-opts ["-source" "17" "-target" "17"]}))

View File

@@ -1,53 +1,60 @@
{:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/core.async {:mvn/version "1.5.648"}
;; Logging
org.zeromq/jeromq {:mvn/version "0.5.2"}
com.taoensso/nippy {:mvn/version "3.1.1"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-2"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-4"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
io.prometheus/simpleclient {:mvn/version "0.15.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.15.0"}
io.prometheus/simpleclient_jetty {:mvn/version "0.15.0"
:exclusions [org.eclipse.jetty/jetty-server
org.eclipse.jetty/jetty-servlet]}
io.prometheus/simpleclient_httpserver {:mvn/version "0.15.0"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
io.prometheus/simpleclient_jetty
{:mvn/version "0.16.0"
:exclusions [org.eclipse.jetty/jetty-server
org.eclipse.jetty/jetty-servlet]}
io.lettuce/lettuce-core {:mvn/version "6.1.6.RELEASE"}
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.2.0.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti {:git/tag "v9.1" :git/sha "63f35d9"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
funcool/yetti
{:git/tag "v9.9"
:git/sha "f0a455d"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.2.772"}
metosin/reitit-core {:mvn/version "0.5.16"}
org.postgresql/postgresql {:mvn/version "42.3.3"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.828"}
metosin/reitit-core {:mvn/version "0.5.18"}
org.postgresql/postgresql {:mvn/version "42.5.0"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
funcool/datoteka {:mvn/version "2.0.0"}
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "1.8.158"}
buddy/buddy-sign {:mvn/version "3.4.333"}
org.jsoup/jsoup {:mvn/version "1.14.3"}
org.im4java/im4java {:mvn/version "1.4.0"}
org.jsoup/jsoup {:mvn/version "1.15.1"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
:git/url "https://github.com/penpot/im4java"}
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"}
io.sentry/sentry {:mvn/version "5.6.1"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.0"}
markdown-clj/markdown-clj {:mvn/version "1.11.3"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.17.136"}}
software.amazon.awssdk/s3 {:mvn/version "2.17.278"}}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -63,15 +70,15 @@
:extra-paths ["test" "dev"]}
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.7.7" :git/sha "1474ad6"}}
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
:ns-default build}
:test
{:extra-paths ["test"]
:extra-deps
{io.github.cognitect-labs/test-runner
{:git/tag "v0.5.0" :git/sha "b3fd0d2"}}
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}
:outdated

View File

@@ -0,0 +1,114 @@
;; 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
;; This is an example on how it can be executed:
;; clojure -Scp $(cat classpath) -M dev/script-fix-sobjects.clj
(require
'[app.common.logging :as l]
'[app.common.data :as d]
'[app.common.pprint]
'[app.db :as db]
'[app.storage :as sto]
'[app.storage.impl :as impl]
'[app.util.time :as dt]
'[integrant.core :as ig])
;; --- HELPERS
(l/info :hint "initializing script" :args *command-line-args*)
(def noop? (some #(= % "noop") *command-line-args*))
(def chunk-size 10)
(def sql:retrieve-sobjects-chunk
"SELECT * FROM storage_object
WHERE created_at < ? AND deleted_at is NULL
ORDER BY created_at desc LIMIT ?")
(defn get-chunk
[conn cursor]
(let [rows (db/exec! conn [sql:retrieve-sobjects-chunk cursor chunk-size])]
[(some->> rows peek :created-at) (seq rows)]))
(defn get-candidates
[conn]
(->> (d/iteration (partial get-chunk conn)
:vf second
:kf first
:initk (dt/now))
(sequence cat)))
(def modules
[:app.db/pool
:app.storage/storage
[:app.main/default :app.worker/executor]
[:app.main/assets :app.storage.s3/backend]
[:app.main/assets :app.storage.fs/backend]])
(def system
(let [config (select-keys app.main/system-config modules)
config (-> config
(assoc :app.migrations/all {})
(assoc :app.metrics/metrics nil))]
(ig/load-namespaces config)
(-> config ig/prep ig/init)))
(defn update-fn
[{:keys [conn] :as storage} {:keys [id backend] :as row}]
(cond
(= backend "s3")
(do
(l/info :hint "rename storage object backend"
:id id
:from-backend backend
:to-backend :assets-s3)
(assoc row :backend "assets-s3"))
(= backend "assets-s3")
(do
(l/info :hint "ignoring storage object" :id id :backend backend)
nil)
(or (= backend "fs")
(= backend "assets-fs"))
(let [sobj (sto/row->storage-object row)
path (-> (sto/get-object-path storage sobj) deref)]
(l/info :hint "change storage object backend"
:id id
:from-backend backend
:to-backend :assets-s3)
(when-not noop?
(-> (impl/resolve-backend storage :assets-s3)
(impl/put-object sobj (sto/content path))
(deref)))
(assoc row :backend "assets-s3"))
:else
(throw (IllegalArgumentException. "unexpected backend found"))))
(try
(db/with-atomic [conn (:app.db/pool system)]
(let [storage (:app.storage/storage system)
storage (assoc storage :conn conn)]
(loop [items (get-candidates conn)]
(when-let [item (first items)]
(when-let [{:keys [id] :as row} (update-fn storage item)]
(db/update! conn :storage-object (dissoc row :id) {:id (:id item)}))
(recur (rest items))))
(when noop?
(throw (ex-info "explicit rollback" {})))))
(catch Throwable cause
(cond
(= "explicit rollback" (ex-message cause))
(l/warn :hint "transaction aborted")
:else
(l/error :hint "unexpected exception" :cause cause))))
(ig/halt! system)
(System/exit 0)

View File

@@ -2,17 +2,20 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns user
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt]
[app.common.logging :as l]
[app.common.perf :as perf]
[app.common.pprint :as pp]
[app.common.transit :as t]
[app.config :as cfg]
[app.main :as main]
[app.srepl.main :as srepl]
[app.util.blob :as blob]
[app.util.fressian :as fres]
[app.util.json :as json]
@@ -35,6 +38,24 @@
(defonce system nil)
;; --- Benchmarking Tools
(defmacro run-quick-bench
[& exprs]
`(with-progress-reporting (quick-bench (do ~@exprs) :verbose)))
(defmacro run-quick-bench'
[& exprs]
`(quick-bench (do ~@exprs)))
(defmacro run-bench
[& exprs]
`(with-progress-reporting (bench (do ~@exprs) :verbose)))
(defmacro run-bench'
[& exprs]
`(bench (do ~@exprs)))
;; --- Development Stuff
(defn- run-tests
@@ -55,7 +76,7 @@
[]
(alter-var-root #'system (fn [sys]
(when sys (ig/halt! sys))
(-> main/system-config
(-> (merge main/system-config main/worker-config)
(ig/prep)
(ig/init))))
:started)

View File

@@ -1,80 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex,nofollow">
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Builtin API Documentation - Penpot</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style>
{% include "api-doc.css" %}
</style>
<script>
{% include "api-doc.js" %}
</script>
</head>
<body>
<main>
<header>
<h1>Penpot API Documentation</h1>
</header>
<section class="rpc-doc-content">
<h2>RPC QUERY METHODS:</h2>
<ul class="rpc-items">
{% for item in query-methods %}
<li class="rpc-item">
<div class="rpc-row-info">
{# <div class="type">{{item.type}}</div> #}
<div class="name">{{item.name}}</div>
<div class="tags">
<span class="tag">
<span>Auth:</span>
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
</span>
</div>
</div>
<div class="rpc-row-detail hidden">
{% if item.docs %}
<h3>DOCSTRING:</h3>
<p>{{item.docs}}</p>
{% endif %}
<h3>SPEC EXPLAIN:</h3>
<pre>{{item.spec}}</pre>
</div>
</li>
{% endfor %}
</ul>
<h2>RPC MUTATION METHODS:</h2>
<ul class="rpc-items">
{% for item in mutation-methods %}
<li class="rpc-item">
<div class="rpc-row-info">
{# <div class="type">{{item.type}}</div> #}
<div class="name">{{item.name}}</div>
<div class="tags">
<span class="tag">
<span>Auth:</span>
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
</span>
</div>
</div>
<div class="rpc-row-detail hidden">
{% if item.docs %}
<h3>DOCSTRING:</h3>
<p>{{item.docs}}</p>
{% endif %}
<h3>SPEC EXPLAIN:</h3>
<pre>{{item.spec}}</pre>
</div>
</li>
{% endfor %}
</ul>
</section>
</main>
</body>
</html>

View File

@@ -0,0 +1,32 @@
[{: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 "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 "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"}
{:id "material-design-baseline"
:name "Material Design (baseline)"
:thumbnail-uri "https://penpot.app/images/libraries/cover-material.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Material-Design-Kit.penpot"}]

View File

@@ -0,0 +1,54 @@
<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">
{% if item.deprecated %}
<span class="tag">
<span>Deprecated:</span>
<span>since v{{item.deprecated}}</span>,
</span>
{% endif %}
<span class="tag">
<span>Auth:</span>
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
</span>
</div>
</div>
<div class="rpc-row-detail hidden">
<h3>DOCSTRING:</h3>
<section class="padded-section">
{% if item.added %}
<p class="small"><strong>Added:</strong> on v{{item.added}}</p>
{% endif %}
{% if item.deprecated %}
<p class="small"><strong>Deprecated:</strong> since v{{item.deprecated}}</p>
{% endif %}
{% if item.docs %}
<p class="docstring"> {{item.docs}}</p>
{% endif %}
</section>
{% if item.changes %}
<h3>CHANGES:</h3>
<section class="padded-section">
<ul class="changes">
{% for change in item.changes %}
<li><strong>{{change.0}}</strong> - {{change.1}}</li>
{% endfor %}
</ul>
</section>
{% endif %}
<h3>SPEC EXPLAIN:</h3>
<section class="padded-section">
<pre class="spec-explain">{{item.spec}}</pre>
</section>
</div>
</li>

View File

@@ -53,7 +53,7 @@ header {
.rpc-item {
/* border: 1px solid red; */
cursor: pointer;
/* cursor: pointer; */
display: flex;
flex-direction: column;
}
@@ -85,6 +85,16 @@ header {
.rpc-row-info > .name {
width: 280px;
/* font-weight: bold; */
border-right: 1px dotted #777;
padding-right: 10px;
}
.rpc-row-info > .module {
width: 120px;
font-weight: bold;
border-right: 1px dotted #777;
text-align: right;
padding-right: 10px;
}
.rpc-row-info > .tags > .tag > span:first-child {
@@ -99,3 +109,37 @@ header {
padding: 5px 10px;
padding-bottom: 20px;
}
.rpc-row-detail p {
font-weight: 200;
}
.rpc-row-detail p.small {
margin-top: 2px;
margin-bottom: 2px;
font-size: 10px;
}
.rpc-row-detail p.small {
margin-top: 2px;
margin-bottom: 2px;
font-size: 10px;
}
.rpc-row-detail strong {
font-weight: 500;
}
.rpc-row-detail .changes {
font-weight: 200;
list-style: none;
padding: 0px;
}
.rpc-row-detail .padded-section {
padding: 0px 10px;
}
p.small strong {
font-size: 10px;
}

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex,nofollow">
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Builtin API Documentation - Penpot</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@200;300;400;500;700&display=swap" rel="stylesheet">
<style>
{% include "app/templates/api-doc.css" %}
</style>
<script>
{% include "app/templates/api-doc.js" %}
</script>
</head>
<body>
<main>
<header>
<h1>Penpot API Documentation (v{{version}})</h1>
</header>
<section class="rpc-doc-content">
<h2>RPC COMMAND METHODS:</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 %}
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
{% endfor %}
</ul>
</section>
</main>
</body>
</html>

View File

@@ -7,7 +7,7 @@
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style>
{% include "templates/styles.css" %}
{% include "app/templates/styles.css" %}
</style>
</head>
<body>

View File

@@ -0,0 +1,127 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Debug Main Page
{% endblock %}
{% block content %}
<nav>
<h1>Debug INDEX:</h1>
<div>[<a href="/dbg/error">ERRORS</a>]</div>
</nav>
<main class="index">
<section class="widget">
<fieldset>
<legend>Download file data:</legend>
<desc>Given an FILE-ID, downloads the file data as file. The file data is encoded using transit.</desc>
<form method="get" action="/dbg/file/data">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Upload File Data:</legend>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/data">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Import with same id?</label>
<input type="checkbox" name="reuseid" />
</div>
<input type="submit" value="Upload" />
</form>
</fieldset>
</section>
<section class="widget">
<fieldset>
<legend>Export binfile:</legend>
<desc>Given an FILE-ID, downloads the file and optionally all
the related libraries in a single custom formatted binary
file.</desc>
<form method="get" action="/dbg/file/export">
<div class="row set-of-inputs">
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
</div>
<div class="row">
<label>Include libraries?</label>
<input type="checkbox" name="includelibs" />
</div>
<div class="row">
<label>Embed assets?</label>
<input type="checkbox" name="embedassets" checked/>
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Import binfile:</legend>
<desc>Import penpot file in binary
format. If <strong>overwrite</strong> is checked, all files will
be overwriten using the same ids found in the file instead of
generating a new ones.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/import">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Overwrite?</label>
<input type="checkbox" name="overwrite" />
<br />
<small>
Instead of creating a new file with all relations remaped,
reuses all ids and updates/overwrites the objects that are
already exists on the database.
<strong>Warning, this operation should be used with caution.</strong>
</small>
</div>
<div class="row">
<label>Migrate?</label>
<input type="checkbox" name="migrate" />
<br />
<small>
Applies the file migrations on the importation process.
</small>
</div>
<div class="row">
<label>Ignore index errors?</label>
<input type="checkbox" name="ignore-index-errors" checked/>
<br />
<small>
Do not break on index lookup erros (remap operation).
Useful when importing a broken file that has broken
relations or missing pieces.
</small>
</div>
<div class="row">
<input type="submit" name="upload" value="Upload" />
</div>
</form>
</fieldset>
</section>
</main>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends "templates/base.tmpl" %}
{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error list

View File

@@ -1,4 +1,4 @@
{% extends "templates/base.tmpl" %}
{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error report {{id}}

View File

@@ -14,7 +14,6 @@ pre {
}
desc {
display: flex;
margin-bottom: 10px;
font-size: 10px;
color: #666;
@@ -28,6 +27,15 @@ main {
margin: 20px;
}
small {
font-size: 9px;
color: #888;
}
small > strong {
font-size: 9px;
}
nav {
position: fixed;
width: 100vw;
@@ -95,17 +103,25 @@ nav > div:not(:last-child) {
.index {
margin-top: 40px;
display: flex;
}
.index > section {
padding: 10px;
background-color: #e3e3e3;
max-width: 400px;
margin: 5px;
height: fit-content;
}
.index > section:not(:last-child) {
margin-bottom: 10px;
.index fieldset:not(:first-child) {
margin-top: 15px;
}
/* .index > section:not(:last-child) { */
/* margin-bottom: 10px; */
/* } */
.index > section > h2 {
margin-top: 0px;
@@ -148,3 +164,16 @@ nav > div:not(:last-child) {
color: inherit;
}
form .row {
padding: 5px 0;
}
.set-of-inputs {
flex-direction: column;
display: flex;
}
.set-of-inputs input:not(:last-child) {
margin-bottom: 3px;
}

View File

@@ -20,11 +20,19 @@
</Appenders>
<Loggers>
<Logger name="com.zaxxer.hikari" level="error"/>
<Logger name="io.lettuce" level="error" />
<Logger name="org.eclipse.jetty" level="error" />
<Logger name="com.zaxxer.hikari" level="error"/>
<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.msgbus" level="info" />
<Logger name="app.http.websocket" level="info" />
<Logger name="app.util.websocket" level="info" />
<Logger name="app.redis" level="info" />
<Logger name="app.rpc.rlimit" level="info" />
<Logger name="app.cli" level="debug" additivity="false">
<AppenderRef ref="console"/>
</Logger>
@@ -38,11 +46,6 @@
<AppenderRef ref="zmq" level="debug" />
</Logger>
<Logger name="penpot" level="debug" additivity="false">
<AppenderRef ref="main" level="debug" />
<AppenderRef ref="zmq" level="debug" />
</Logger>
<Logger name="user" level="trace" additivity="false">
<AppenderRef ref="main" level="trace" />
</Logger>

View File

@@ -7,14 +7,11 @@
</Appenders>
<Loggers>
<Logger name="io.lettuce" level="error" />
<Logger name="com.zaxxer.hikari" level="error" />
<Logger name="org.eclipse.jetty" level="error" />
<Logger name="org.postgresql" level="error" />
<Logger name="app" level="debug" additivity="false">
<AppenderRef ref="console" />
</Logger>
<Logger name="penpot" level="fatal" additivity="false">
<Logger name="app" level="info" additivity="false">
<AppenderRef ref="console" />
</Logger>

View File

@@ -0,0 +1,6 @@
^{:refresh "30s"}
{:default
[[:default :window "200000/h"]]
#{:query/profile}
[[:burst :bucket "100/60/1m"]]}

View File

@@ -1,32 +0,0 @@
{% extends "templates/base.tmpl" %}
{% block title %}
Debug Main Page
{% endblock %}
{% block content %}
<nav>
<h1>Debug INDEX:</h1>
<div>[<a href="/dbg/error">ERRORS</a>]</div>
</nav>
<main class="index">
<section>
<h2>Download file data:</h2>
<desc>Given an FILE-ID, downloads the file data as file. The file data is encoded using transit.</desc>
<form method="get" action="/dbg/file/data">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
<input type="hidden" name="download" value="1" />
<input type="submit" value="Download" />
</form>
</section>
<section>
<h2>Upload File Data:</h2>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/data">
<input type="file" name="file" value="" />
<input type="submit" value="Upload" />
</form>
</section>
</main>
{% endblock %}

View File

@@ -17,5 +17,6 @@ cp scripts/manage.template.sh target/dist/manage.sh;
chmod +x target/dist/run.sh;
chmod +x target/dist/manage.sh;
# Prefetch
bb ./scripts/prefetch-templates.clj resources/app/onboarding.edn builtin-templates/
cp -r builtin-templates target/dist/

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bb
(require '[babashka.curl :as curl]
'[babashka.fs :as fs])
(defn download-if-needed!
[dest data]
(doseq [{:keys [id file-uri] :as item} data]
(let [file (fs/file dest id)
rsp (curl/get file-uri {:as :stream})]
(when (not= 200 (:status rsp))
(println (format "unable to download %s (uri: %s)" id file-uri))
(System/exit -1))
(when-not (fs/exists? (str file))
(println (format "=> downloading %s" id))
(with-open [output (io/output-stream file)]
(io/copy (:body rsp) output))))))
(defn read-defs-file
[path]
(with-open [content (io/reader path)]
(edn/read-string (slurp content))))
(let [[path dest] *command-line-args*]
(when (or (nil? path)
(nil? dest))
(println "invalid arguments")
(System/exit -1))
(when-not (fs/exists? path)
(println (format "file %s does not exists" path))
(System/exit -1))
(when-not (fs/exists? dest)
(fs/create-dirs dest))
(let [data (read-defs-file path)]
(download-if-needed! dest data)))

View File

@@ -2,7 +2,7 @@
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"
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-rpc-rate-limit enable-warn-rpc-rate-limits enable-smtp"
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
# export PENPOT_DATABASE_USERNAME="penpot"
@@ -16,6 +16,8 @@ export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enabl
# export PENPOT_LOGGERS_LOKI_URI="http://172.17.0.1:3100/loki/api/v1/push"
# export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit"
export PENPOT_DEFAULT_RATE_LIMIT="default,window,10000/h"
# Initialize MINIO config
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
mc admin user add penpot-s3 penpot-devenv penpot-devenv
@@ -24,9 +26,8 @@ mc mb penpot-s3/penpot -p
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_ASSETS_STORAGE_BACKEND=assets-fs
export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
export PENPOT_STORAGE_ASSETS_S3_REGION=eu-central-1
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
export OPTIONS="
@@ -40,6 +41,9 @@ export OPTIONS="
-J-XX:+UnlockDiagnosticVMOptions \
-J-XX:+DebugNonSafepoints";
# Uncomment for use the ImageMagick v7.x
# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
export OPTIONS_EVAL="nil"
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"

View File

@@ -2,7 +2,7 @@
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"
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp"
set -ex

View File

@@ -0,0 +1,137 @@
;; 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.auth.ldap
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[clj-ldap.client :as ldap]
[clojure.spec.alpha :as s]
[clojure.string]
[integrant.core :as ig]))
(defn- prepare-params
[cfg]
{:ssl? (:ssl cfg)
:startTLS? (:tls cfg)
:bind-dn (:bind-dn cfg)
:password (:bind-password cfg)
:host {:address (:host cfg)
:port (:port cfg)}})
(defn- connect
"Connects to the LDAP provider and returns a connection. An
exception is raised if no connection is possible."
^java.lang.AutoCloseable
[cfg]
(try
(-> cfg prepare-params ldap/connect)
(catch Throwable cause
(ex/raise :type :restriction
:code :unable-to-connect-to-ldap
:hint "unable to connect to ldap server"
:cause cause))))
(defn- replace-several [s & {:as replacements}]
(reduce-kv clojure.string/replace s replacements))
(defn- search-user
[{:keys [conn attrs base-dn] :as cfg} email]
(let [query (replace-several (:query cfg) ":username" email)
params {:filter query
:sizelimit 1
:attributes attrs}]
(first (ldap/search conn base-dn params))))
(defn- retrieve-user
[{:keys [conn] :as cfg} {:keys [email password]}]
(when-let [{:keys [dn] :as user} (search-user cfg email)]
(when (ldap/bind? conn dn password)
{:fullname (get user (-> cfg :attrs-fullname keyword))
:email email
:backend "ldap"})))
(s/def ::fullname ::us/not-empty-string)
(s/def ::email ::us/email)
(s/def ::backend ::us/not-empty-string)
(s/def ::info-data
(s/keys :req-un [::fullname ::email ::backend]))
(defn authenticate
[cfg params]
(with-open [conn (connect cfg)]
(when-let [user (-> (assoc cfg :conn conn)
(retrieve-user params))]
(when-not (s/valid? ::info-data user)
(let [explain (s/explain-str ::info-data user)]
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
(ex/raise :type :restriction
:code :wrong-ldap-response
:explain explain)))
user)))
(defn- try-connectivity
[cfg]
;; If we have ldap parameters, try to establish connection
(when (and (:bind-dn cfg)
(:bind-password cfg)
(:host cfg)
(:port cfg))
(try
(with-open [_ (connect cfg)]
(l/info :hint "provider initialized"
:provider "ldap"
:host (:host cfg)
:port (:port cfg)
:tls? (:tls cfg)
:ssl? (:ssl cfg)
:bind-dn (:bind-dn cfg)
:base-dn (:base-dn cfg)
:query (:query cfg))
cfg)
(catch Throwable cause
(l/error :hint "unable to connect to LDAP server (LDAP auth provider disabled)"
:host (:host cfg) :port (:port cfg) :cause cause)
nil))))
(defn- prepare-attributes
[cfg]
(assoc cfg :attrs [(:attrs-username cfg)
(:attrs-email cfg)
(:attrs-fullname cfg)]))
(defmethod ig/init-key ::provider
[_ cfg]
(when (:enabled? cfg)
(some-> cfg try-connectivity prepare-attributes)))
(s/def ::enabled? ::us/boolean)
(s/def ::host ::cf/ldap-host)
(s/def ::port ::cf/ldap-port)
(s/def ::ssl ::cf/ldap-ssl)
(s/def ::tls ::cf/ldap-starttls)
(s/def ::query ::cf/ldap-user-query)
(s/def ::base-dn ::cf/ldap-base-dn)
(s/def ::bind-dn ::cf/ldap-bind-dn)
(s/def ::bind-password ::cf/ldap-bind-password)
(s/def ::attrs-email ::cf/ldap-attrs-email)
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
(s/def ::attrs-username ::cf/ldap-attrs-username)
(defmethod ig/pre-init-spec ::provider
[_]
(s/keys :opt-un [::host ::port
::ssl ::tls
::enabled?
::bind-dn
::bind-password
::query
::attrs-email
::attrs-username
::attrs-fullname]))

View File

@@ -0,0 +1,543 @@
;; 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.auth.oidc
"OIDC client implementation."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uri :as u]
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.http.middleware :as hmw]
[app.loggers.audit :as audit]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.util.json :as json]
[app.util.time :as dt]
[app.worker :as wrk]
[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]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OIDC PROVIDER (GENERIC)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- discover-oidc-config
[{:keys [http-client]} {:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (ex/try (http/req! http-client {: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/read (:body response))]
{:token-uri (get data :token_endpoint)
:auth-uri (get data :authorization_endpoint)
:user-uri (get data :userinfo_endpoint)})
:else
(do
(l/warn :hint "unable to discover OIDC configuration"
:uri (str discovery-uri)
:response-status-code (:status response))
nil))))
(defn- prepare-oidc-opts
[cfg]
(let [opts {:base-uri (:base-uri cfg)
:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:token-uri (:token-uri cfg)
:auth-uri (:auth-uri cfg)
:user-uri (:user-uri cfg)
:scopes (:scopes cfg #{"openid" "profile" "email"})
:roles-attr (:roles-attr cfg)
:roles (:roles cfg)
:name "oidc"}
opts (d/without-nils opts)]
(when (and (string? (:base-uri opts))
(string? (:client-id opts))
(string? (:client-secret opts)))
(if (and (string? (:token-uri opts))
(string? (:user-uri opts))
(string? (:auth-uri opts)))
opts
(some-> (discover-oidc-config cfg opts)
(merge opts {:discover? true}))))))
(defmethod ig/prep-key ::generic-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::generic-provider
[_ cfg]
(when (:enabled? cfg)
(if-let [opts (prepare-oidc-opts cfg)]
(do
(l/info :hint "provider initialized"
:provider :oidc
:method (if (:discover? opts) "discover" "manual")
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts))
:scopes (str/join "," (:scopes opts))
:auth-uri (:auth-uri opts)
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :oidc)
nil))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GOOGLE AUTH PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/prep-key ::google-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::google-provider
[_ cfg]
(let [opts {:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:scopes #{"openid" "email" "profile"}
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
:token-uri "https://oauth2.googleapis.com/token"
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
:name "google"}]
(when (:enabled? cfg)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :google
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :google)
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GITHUB AUTH PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- retrieve-github-email
[{:keys [http-client]} tdata info]
(or (some-> info :email p/resolved)
(-> (http/req! http-client {:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get})
(p/then (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/read (filter :primary) first :email))))))
(defmethod ig/prep-key ::github-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::github-provider
[_ cfg]
(let [opts {:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:scopes #{"read:user" "user:email"}
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:user-uri "https://api.github.com/user"
:name "github"
;; Additional hooks for provider specific way of
;; retrieve emails.
:get-email-fn (partial retrieve-github-email cfg)}]
(when (:enabled? cfg)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :github
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :github)
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GITLAB AUTH PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/prep-key ::gitlab-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::gitlab-provider
[_ cfg]
(let [base (:base-uri cfg "https://gitlab.com")
opts {:base-uri base
:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:scopes #{"openid" "profile" "email"}
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/oauth/userinfo")
:name "gitlab"}]
(when (:enabled? cfg)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :gitlab
:base-uri base
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :gitlab)
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HANDLERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
(let [public (u/uri (:public-uri cfg))]
(str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback")))))
(defn- build-auth-uri
[{:keys [provider] :as cfg} state]
(let [params {:client_id (:client-id provider)
:redirect_uri (build-redirect-uri cfg)
:response_type "code"
:state state
:scope (str/join " " (:scopes provider []))}
query (u/map->query-string params)]
(-> (u/uri (:auth-uri provider))
(assoc :query query)
(str))))
(defn- qualify-props
[provider props]
(reduce-kv (fn [result k v]
(assoc result (keyword (:name provider) (name k)) v))
{}
props))
(defn retrieve-access-token
[{:keys [provider http-client] :as cfg} code]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code code
:grant_type "authorization_code"
:redirect_uri (build-redirect-uri cfg)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"
"accept" "application/json"}
:uri (:token-uri provider)
:body (u/map->query-string params)}]
(p/then
(http/req! http-client req)
(fn [{:keys [status body] :as res}]
(if (= status 200)
(let [data (json/read 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))))))
(defn- retrieve-user-info
[{:keys [provider http-client] :as cfg} tdata]
(letfn [(retrieve []
(http/req! http-client {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}))
(validate-response [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 [info]
;; Allow providers hook into this for custom email
;; retrieval method.
(if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata info)
(let [attr-kw (cf/get :oidc-email-attr :email)]
(get info attr-kw))))
(get-name [info]
(let [attr-kw (cf/get :oidc-name-attr :name)]
(get info attr-kw)))
(process-response [response]
(p/let [info (-> response :body json/read)
email (get-email info)]
{:backend (:name provider)
:email email
:fullname (or (get-name info) email)
:props (->> (dissoc info :name :email)
(qualify-props provider))}))
(validate-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)]
(-> (retrieve)
(p/then validate-response)
(p/then process-response)
(p/then validate-info))))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
(s/def ::fullname ::us/not-empty-string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::info
(s/keys :req-un [::backend
::email
::fullname
::props]))
(defn retrieve-info
[{:keys [sprops provider] :as cfg} {:keys [params] :as request}]
(letfn [(validate-oidc [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 [provider-roles (into #{} (:roles provider))
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
roles (get info attr)]
(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? provider-roles profile-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enough permissions"))))
info)
(post-process [state info]
(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))))]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
(let [state (get params :state)
code (get params :code)
state (tokens/verify sprops {: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))))))
(defn- retrieve-profile
[{:keys [pool executor] :as cfg} info]
(px/with-dispatch executor
(with-open [conn (db/open pool)]
(some->> (:email info)
(profile/retrieve-profile-data-by-email conn)
(profile/populate-additional-data conn)
(profile/decode-profile-row)))))
(defn- redirect-response
[uri]
(yrs/response :status 302 :headers {"location" (str uri)}))
(defn- generate-error-redirect
[cfg error]
(let [uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
(redirect-response uri)))
(defn- generate-redirect
[{:keys [sprops session audit] :as cfg} request info profile]
(if profile
(let [sxf ((:create session) (:id profile))
token (or (:invitation-token info)
(tokens/generate sprops {:iss :auth
:exp (dt/in-future "15m")
:profile-id (:id profile)}))
params {:token token}
uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string params)))]
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(when (fn? audit)
(audit :cmd :submit
:type "command"
:name "login"
:profile-id (:id profile)
:ip-addr (audit/parse-client-ip request)
: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 sprops info)
params (d/without-nils
{:token token
:fullname (:fullname info)})
uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))))
(defn- auth-handler
[{:keys [sprops] :as cfg} {:keys [params] :as request}]
(let [props (audit/extract-utm-params params)
state (tokens/generate sprops
{:iss :oauth
:invitation-token (:invitation-token params)
:props props
:exp (dt/in-future "4h")})
uri (build-auth-uri cfg state)]
(yrs/response 200 {:redirect-uri uri})))
(defn- callback-handler
[cfg request]
(letfn [(process-request []
(p/let [info (retrieve-info cfg request)
profile (retrieve-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))))
(def provider-lookup
{:compile
(fn [& _]
(fn [handler]
(fn [{:keys [providers] :as cfg} request]
(let [provider (some-> request :path-params :provider keyword)]
(if-let [provider (get providers provider)]
(handler (assoc cfg :provider provider) request)
(ex/raise :type :restriction
:code :provider-not-configured
:provider provider
:hint "provider not configured"))))))})
(s/def ::public-uri ::us/not-empty-string)
(s/def ::http-client ::http/client)
(s/def ::session map?)
(s/def ::sprops map?)
(s/def ::providers map?)
(defmethod ig/pre-init-spec ::routes
[_]
(s/keys :req-un [::public-uri
::session
::sprops
::http-client
::providers
::db/pool
::wrk/executor]))
(defmethod ig/init-key ::routes
[_ {:keys [executor session] :as cfg}]
(let [cfg (update cfg :provider d/without-nils)]
["" {:middleware [[(:middleware session)]
[hmw/with-dispatch executor]
[hmw/with-config cfg]
[provider-lookup]
]}
;; We maintain the both URI prefixes for backward compatibility.
["/auth/oauth"
["/:provider"
{:handler auth-handler
:allowed-methods #{:post}}]
["/:provider/callback"
{:handler callback-handler
:allowed-methods #{:get}}]]
["/auth/oidc"
["/:provider"
{:handler auth-handler
:allowed-methods #{:post}}]
["/:provider/callback"
{:handler callback-handler
:allowed-methods #{:get}}]]]))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.cli.manage
"A manage cli api."
@@ -10,6 +10,7 @@
[app.common.logging :as l]
[app.db :as db]
[app.main :as main]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.mutations.profile :as profile]
[app.rpc.queries.profile :refer [retrieve-profile-data-by-email]]
[clojure.string :as str]
@@ -54,13 +55,13 @@
:type :password}))]
(try
(db/with-atomic [conn (:app.db/pool system)]
(->> (profile/create-profile conn
(->> (cmd.auth/create-profile conn
{:fullname fullname
:email email
:password password
:is-active true
:is-demo false})
(profile/create-profile-relations conn)))
(cmd.auth/create-profile-relations conn)))
(when (pos? (:verbosity options))
(println "User created successfully."))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.config
"A configuration management."
@@ -19,6 +19,7 @@
[clojure.pprint :as pprint]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[environ.core :refer [env]]
[integrant.core :as ig]))
@@ -41,13 +42,13 @@
data))
(def defaults
{
:database-uri "postgresql://postgres/penpot"
{:database-uri "postgresql://postgres/penpot"
:database-username "penpot"
:database-password "penpot"
:default-blob-version 4
:loggers-zmq-uri "tcp://localhost:45556"
:rpc-rlimit-config (fs/path "resources/rlimit.edn")
:file-change-snapshot-every 5
:file-change-snapshot-timeout "3h"
@@ -79,35 +80,35 @@
:ldap-attrs-username "uid"
:ldap-attrs-email "mail"
:ldap-attrs-fullname "cn"
:ldap-attrs-photo "jpegPhoto"
;; a server prop key where initial project is stored.
:initial-project-skey "initial-project"})
(s/def ::flags ::us/set-of-keywords)
(s/def ::default-rpc-rlimit ::us/vector-of-strings)
(s/def ::rpc-rlimit-config ::fs/path)
;; DEPRECATED PROPERTIES
(s/def ::registration-enabled ::us/boolean)
(s/def ::smtp-enabled ::us/boolean)
(s/def ::media-max-file-size ::us/integer)
(s/def ::flags ::us/vector-of-keywords)
(s/def ::telemetry-enabled ::us/boolean)
(s/def ::asserts-enabled ::us/boolean)
;; END DEPRECATED
(s/def ::audit-log-archive-uri ::us/string)
(s/def ::audit-log-gc-max-age ::dt/duration)
(s/def ::admins ::us/set-of-str)
(s/def ::admins ::us/set-of-strings)
(s/def ::file-change-snapshot-every ::us/integer)
(s/def ::file-change-snapshot-timeout ::dt/duration)
(s/def ::default-executor-parallelism ::us/integer)
(s/def ::blocking-executor-parallelism ::us/integer)
(s/def ::worker-executor-parallelism ::us/integer)
(s/def ::authenticated-cookie-domain ::us/string)
(s/def ::authenticated-cookie-name ::us/string)
(s/def ::auth-token-cookie-name ::us/string)
(s/def ::auth-token-cookie-max-age ::dt/duration)
(s/def ::secret-key ::us/string)
(s/def ::allow-demo-users ::us/boolean)
(s/def ::assets-path ::us/string)
(s/def ::authenticated-cookie-domain ::us/string)
(s/def ::database-password (s/nilable ::us/string))
(s/def ::database-uri ::us/string)
(s/def ::database-username (s/nilable ::us/string))
@@ -131,8 +132,8 @@
(s/def ::oidc-token-uri ::us/string)
(s/def ::oidc-auth-uri ::us/string)
(s/def ::oidc-user-uri ::us/string)
(s/def ::oidc-scopes ::us/set-of-str)
(s/def ::oidc-roles ::us/set-of-str)
(s/def ::oidc-scopes ::us/set-of-strings)
(s/def ::oidc-roles ::us/set-of-strings)
(s/def ::oidc-roles-attr ::us/keyword)
(s/def ::oidc-email-attr ::us/keyword)
(s/def ::oidc-name-attr ::us/keyword)
@@ -143,13 +144,9 @@
(s/def ::http-server-max-multipart-body-size ::us/integer)
(s/def ::http-server-io-threads ::us/integer)
(s/def ::http-server-worker-threads ::us/integer)
(s/def ::http-session-idle-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-size ::us/integer)
(s/def ::initial-project-skey ::us/string)
(s/def ::ldap-attrs-email ::us/string)
(s/def ::ldap-attrs-fullname ::us/string)
(s/def ::ldap-attrs-photo ::us/string)
(s/def ::ldap-attrs-username ::us/string)
(s/def ::ldap-base-dn ::us/string)
(s/def ::ldap-bind-dn ::us/string)
@@ -169,11 +166,13 @@
(s/def ::profile-complaint-threshold ::us/integer)
(s/def ::public-uri ::us/string)
(s/def ::redis-uri ::us/string)
(s/def ::registration-domain-whitelist ::us/set-of-str)
(s/def ::rlimit-font ::us/integer)
(s/def ::rlimit-file-update ::us/integer)
(s/def ::rlimit-image ::us/integer)
(s/def ::rlimit-password ::us/integer)
(s/def ::registration-domain-whitelist ::us/set-of-strings)
(s/def ::semaphore-process-font ::us/integer)
(s/def ::semaphore-process-image ::us/integer)
(s/def ::semaphore-update-file ::us/integer)
(s/def ::semaphore-auth ::us/integer)
(s/def ::smtp-default-from ::us/string)
(s/def ::smtp-default-reply-to ::us/string)
(s/def ::smtp-host ::us/string)
@@ -198,18 +197,15 @@
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
(s/def ::sentry-trace-sample-rate ::us/number)
(s/def ::sentry-attach-stack-trace ::us/boolean)
(s/def ::sentry-debug ::us/boolean)
(s/def ::sentry-dsn ::us/string)
(s/def ::config
(s/keys :opt-un [::secret-key
::flags
::admins
::allow-demo-users
::audit-log-archive-uri
::audit-log-gc-max-age
::auth-token-cookie-name
::auth-token-cookie-max-age
::authenticated-cookie-name
::authenticated-cookie-domain
::database-password
::database-uri
@@ -218,9 +214,9 @@
::database-min-pool-size
::database-max-pool-size
::default-blob-version
::default-rpc-rlimit
::error-report-webhook
::default-executor-parallelism
::blocking-executor-parallelism
::worker-executor-parallelism
::file-change-snapshot-every
::file-change-snapshot-timeout
@@ -250,13 +246,9 @@
::http-server-max-multipart-body-size
::http-server-io-threads
::http-server-worker-threads
::http-session-idle-max-age
::http-session-updater-batch-max-age
::http-session-updater-batch-max-size
::initial-project-skey
::ldap-attrs-email
::ldap-attrs-fullname
::ldap-attrs-photo
::ldap-attrs-username
::ldap-base-dn
::ldap-bind-dn
@@ -269,6 +261,7 @@
::local-assets-uri
::loggers-loki-uri
::loggers-zmq-uri
::media-max-file-size
::profile-bounce-max-age
::profile-bounce-threshold
::profile-complaint-max-age
@@ -276,26 +269,25 @@
::public-uri
::redis-uri
::registration-domain-whitelist
::registration-enabled
::rlimit-font
::rlimit-file-update
::rlimit-image
::rlimit-password
::sentry-dsn
::sentry-debug
::sentry-attach-stack-trace
::sentry-trace-sample-rate
::rpc-rlimit-config
::semaphore-process-font
::semaphore-process-image
::semaphore-update-file
::semaphore-auth
::smtp-default-from
::smtp-default-reply-to
::smtp-enabled
::smtp-host
::smtp-password
::smtp-port
::smtp-ssl
::smtp-tls
::smtp-username
::srepl-host
::srepl-port
::assets-storage-backend
::storage-assets-fs-directory
::storage-assets-s3-bucket
@@ -314,7 +306,9 @@
(def default-flags
[:enable-backend-api-doc
:enable-secure-session-cookies])
:enable-backend-worker
:enable-secure-session-cookies
:enable-email-verification])
(defn- parse-flags
[config]
@@ -354,8 +348,8 @@
(str/trim))
"%version%")))
(def ^:dynamic config (read-config))
(def ^:dynamic flags (parse-flags config))
(defonce ^:dynamic config (read-config))
(defonce ^:dynamic flags (parse-flags config))
(def deletion-delay
(dt/duration {:days 7}))

View File

@@ -2,9 +2,10 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.db
(:refer-clojure :exclude [get])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
@@ -27,6 +28,8 @@
com.zaxxer.hikari.HikariConfig
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
io.whitfin.siphash.SipHasher
io.whitfin.siphash.SipHasherContainer
java.io.InputStream
java.io.OutputStream
java.lang.AutoCloseable
@@ -55,54 +58,66 @@
(s/def ::migrations map?)
(s/def ::name keyword?)
(s/def ::password ::us/string)
(s/def ::read-only ::us/boolean)
(s/def ::uri ::us/not-empty-string)
(s/def ::username ::us/string)
(s/def ::validation-timeout ::us/integer)
(s/def ::read-only? ::us/boolean)
(defmethod ig/pre-init-spec ::pool [_]
(s/keys :req-un [::uri ::name
(s/def ::pool-options
(s/keys :opt-un [::uri ::name
::min-size
::max-size
::connection-timeout
::validation-timeout]
:opt-un [::migrations
::validation-timeout
::migrations
::username
::password
::mtx/metrics
::read-only]))
::read-only?]))
(def defaults
{:name :main
:min-size 0
:max-size 60
:connection-timeout 10000
:validation-timeout 10000
:idle-timeout 120000 ; 2min
:max-lifetime 1800000 ; 30m
:read-only? false})
(defmethod ig/prep-key ::pool
[_ cfg]
(merge {:name :main
:min-size 0
:max-size 30
:connection-timeout 10000
:validation-timeout 10000
:idle-timeout 120000 ; 2min
:max-lifetime 1800000 ; 30m
:read-only false}
(d/without-nils cfg)))
(merge defaults (d/without-nils cfg)))
;; Don't validate here, just validate that a map is received.
(defmethod ig/pre-init-spec ::pool [_] ::pool-options)
(defmethod ig/init-key ::pool
[_ {:keys [migrations name read-only] :as cfg}]
(l/info :hint "initialize connection pool"
:name (d/name name)
:uri (:uri cfg)
:read-only read-only
:with-credentials (and (contains? cfg :username)
(contains? cfg :password))
:min-size (:min-size cfg)
:max-size (:max-size cfg))
[_ {:keys [migrations read-only? uri] :as cfg}]
(if uri
(let [pool (create-pool cfg)]
(l/info :hint "initialize connection pool"
:name (d/name (:name cfg))
:uri uri
:read-only read-only?
:with-credentials (and (contains? cfg :username)
(contains? cfg :password))
:min-size (:min-size cfg)
:max-size (:max-size cfg))
(when-not read-only?
(some->> (seq migrations) (apply-migrations! pool)))
pool)
(let [pool (create-pool cfg)]
(when-not read-only
(some->> (seq migrations) (apply-migrations! pool)))
pool))
(do
(l/warn :hint "unable to initialize pool, missing url"
:name (d/name (:name cfg))
:read-only read-only?)
nil)))
(defmethod ig/halt-key! ::pool
[_ pool]
(.close ^HikariDataSource pool))
(when pool
(.close ^HikariDataSource pool)))
(defn- apply-migrations!
[pool migrations]
@@ -126,7 +141,7 @@
(.setJdbcUrl (str "jdbc:" uri))
(.setPoolName (d/name (:name cfg)))
(.setAutoCommit true)
(.setReadOnly (:read-only cfg))
(.setReadOnly (:read-only? cfg))
(.setConnectionTimeout (:connection-timeout cfg))
(.setValidationTimeout (:validation-timeout cfg))
(.setIdleTimeout (:idle-timeout cfg))
@@ -138,7 +153,7 @@
;; When metrics namespace is provided
(when metrics
(->> (:registry metrics)
(->> (::mtx/registry metrics)
(PrometheusMetricsTrackerFactory.)
(.setMetricsTrackerFactory config)))
@@ -213,7 +228,7 @@
[& args]
`(jdbc/with-transaction ~@args))
(defn ^Connection open
(defn open
[pool]
(jdbc/get-connection pool))
@@ -256,28 +271,55 @@
(sql/delete table params opts)
(assoc opts :return-keys true))))
(defn- is-deleted?
(defn is-row-deleted?
[{:keys [deleted-at]}]
(and (dt/instant? deleted-at)
(< (inst-ms deleted-at)
(inst-ms (dt/now)))))
(defn get-by-params
(defn get*
"Internal function for retrieve a single row from database that
matches a simple filters."
([ds table params]
(get-by-params ds table params nil))
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
(let [res (exec-one! ds (sql/select table params opts))]
(when (and check-not-found (or (not res) (is-deleted? res)))
(get* ds table params nil))
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
(let [rows (exec! ds (sql/select table params opts))
rows (cond->> rows
check-deleted?
(remove is-row-deleted?))]
(first rows))))
(defn get
([ds table params]
(get ds table params nil))
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
(let [row (get* ds table params opts)]
(when (and (not row) check-deleted?)
(ex/raise :type :not-found
:table table
:hint "database object not found"))
res)))
row)))
(defn get-by-params
"DEPRECATED"
([ds table params]
(get-by-params ds table params nil))
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
(let [row (get* ds table params (assoc opts :check-deleted? check-not-found))]
(when (and (not row) check-not-found)
(ex/raise :type :not-found
:table table
:hint "database object not found"))
row)))
(defn get-by-id
([ds table id]
(get-by-params ds table {:id id} nil))
(get ds table {:id id} nil))
([ds table id opts]
(get-by-params ds table {:id id} opts)))
(let [opts (cond-> opts
(contains? opts :check-not-found)
(assoc :check-deleted? (:check-not-found opts)))]
(get ds table {:id id} opts))))
(defn query
([ds table params]
@@ -311,9 +353,9 @@
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
(defn decode-pgarray
([v] (into [] (.getArray ^PgArray v)))
([v in] (into in (.getArray ^PgArray v)))
([v in xf] (into in xf (.getArray ^PgArray v))))
([v] (some->> ^PgArray v .getArray vec))
([v in] (some->> ^PgArray v .getArray (into in)))
([v in xf] (some->> ^PgArray v .getArray (into in xf))))
(defn pgarray->set
[v]
@@ -355,23 +397,23 @@
(.rollback conn sp)))
(defn interval
[data]
[o]
(cond
(integer? data)
(->> (/ data 1000.0)
(or (integer? o)
(float? o))
(->> (/ o 1000.0)
(format "%s seconds")
(pginterval))
(string? data)
(pginterval data)
(string? o)
(pginterval o)
(dt/duration? data)
(->> (/ (.toMillis ^java.time.Duration data) 1000.0)
(format "%s seconds")
(pginterval))
(dt/duration? o)
(interval (inst-ms o))
:else
(ex/raise :type :not-implemented)))
(ex/raise :type :not-implemented
:hint (format "no implementation found for value %s" (pr-str o)))))
(defn decode-json-pgobject
[^PGobject o]
@@ -419,10 +461,19 @@
;; --- Locks
(def ^:private siphash-state
(SipHasher/container
(uuid/get-bytes uuid/zero)))
(defn uuid->hash-code
[o]
(.hash ^SipHasherContainer siphash-state
^bytes (uuid/get-bytes o)))
(defn- xact-check-param
[n]
(cond
(uuid? n) (uuid/get-word-high n)
(uuid? n) (uuid->hash-code n)
(int? n) n
:else (throw (IllegalArgumentException. "uuid or number allowed"))))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.db.sql
(:refer-clojure :exclude [update])

View File

@@ -2,30 +2,260 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.emails
"Main api for send emails."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.util.emails :as emails]
[app.emails.invite-to-team :as-alias emails.invite-to-team]
[app.metrics :as mtx]
[app.util.template :as tmpl]
[app.worker :as wrk]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
[cuerdas.core :as str]
[integrant.core :as ig])
(:import
jakarta.mail.Message$RecipientType
jakarta.mail.Session
jakarta.mail.Transport
jakarta.mail.internet.InternetAddress
jakarta.mail.internet.MimeBodyPart
jakarta.mail.internet.MimeMessage
jakarta.mail.internet.MimeMultipart
java.util.Properties))
;; --- PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EMAIL IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- parse-address
[v]
(InternetAddress/parse ^String v))
(defn- resolve-recipient-type
^Message$RecipientType
[type]
(case type
:to Message$RecipientType/TO
:cc Message$RecipientType/CC
:bcc Message$RecipientType/BCC))
(defn- assign-recipient
[^MimeMessage mmsg type address]
(if (sequential? address)
(reduce #(assign-recipient %1 type %2) mmsg address)
(let [address (parse-address address)
type (resolve-recipient-type type)]
(.addRecipients mmsg type address)
mmsg)))
(defn- assign-recipients
[mmsg {:keys [to cc bcc] :as params}]
(cond-> mmsg
(some? to) (assign-recipient :to to)
(some? cc) (assign-recipient :cc cc)
(some? bcc) (assign-recipient :bcc bcc)))
(defn- assign-from
[mmsg {:keys [default-from]} {:keys [from] :as props}]
(let [from (or from default-from)]
(when from
(let [from (parse-address from)]
(.addFrom ^MimeMessage mmsg from)))))
(defn- assign-reply-to
[mmsg {:keys [default-reply-to] :as cfg} {:keys [reply-to] :as params}]
(let [reply-to (or reply-to default-reply-to)]
(when reply-to
(let [reply-to (parse-address reply-to)]
(.setReplyTo ^MimeMessage mmsg reply-to)))))
(defn- assign-subject
[mmsg {:keys [subject charset] :or {charset "utf-8"} :as params}]
(assert (string? subject) "subject is mandatory")
(.setSubject ^MimeMessage mmsg
^String subject
^String charset))
(defn- assign-extra-headers
[^MimeMessage mmsg {:keys [headers extra-data] :as params}]
(let [headers (assoc headers "X-Penpot-Data" extra-data)]
(reduce-kv (fn [^MimeMessage mmsg k v]
(doto mmsg
(.addHeader (name k) (str v))))
mmsg
headers)))
(defn- assign-body
[^MimeMessage mmsg {:keys [body charset] :or {charset "utf-8"}}]
(let [mpart (MimeMultipart. "mixed")]
(cond
(string? body)
(let [bpart (MimeBodyPart.)]
(.setContent bpart ^String body (str "text/plain; charset=" charset))
(.addBodyPart mpart bpart))
(vector? body)
(let [mmp (MimeMultipart. "alternative")
mbp (MimeBodyPart.)]
(.addBodyPart mpart mbp)
(.setContent mbp mmp)
(doseq [item body]
(let [mbp (MimeBodyPart.)]
(.setContent mbp
^String (:content item)
^String (str (:type item "text/plain") "; charset=" charset))
(.addBodyPart mmp mbp))))
(map? body)
(let [bpart (MimeBodyPart.)]
(.setContent bpart
^String (:content body)
^String (str (:type body "text/plain") "; charset=" charset))
(.addBodyPart mpart bpart))
:else
(throw (ex-info "Unsupported type" {:body body})))
(.setContent mmsg mpart)
mmsg))
(defn- opts->props
[{:keys [username tls host port timeout default-from]
:or {timeout 30000}
:as opts}]
(reduce-kv
(fn [^Properties props k v]
(if (nil? v)
props
(doto props (.put ^String k ^String (str v)))))
(Properties.)
{"mail.user" username
"mail.host" host
"mail.debug" (contains? cf/flags :smtp-debug)
"mail.from" default-from
"mail.smtp.auth" (boolean username)
"mail.smtp.starttls.enable" tls
"mail.smtp.starttls.required" tls
"mail.smtp.host" host
"mail.smtp.port" port
"mail.smtp.user" username
"mail.smtp.timeout" timeout
"mail.smtp.connectiontimeout" timeout}))
(defn- create-smtp-session
[opts]
(let [props (opts->props opts)]
(Session/getInstance props)))
(defn- create-smtp-message
^MimeMessage
[cfg session params]
(let [mmsg (MimeMessage. ^Session session)]
(assign-recipients mmsg params)
(assign-from mmsg cfg params)
(assign-reply-to mmsg cfg params)
(assign-subject mmsg params)
(assign-extra-headers mmsg params)
(assign-body mmsg params)
(.saveChanges ^MimeMessage mmsg)
mmsg))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TEMPLATE EMAIL IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private email-path "app/emails/%(id)s/%(lang)s.%(type)s")
(defn- render-email-template-part
[type id context]
(let [lang (:lang context :en)
path (str/format email-path {:id (name id)
:lang (name lang)
:type (name type)})]
(some-> (io/resource path)
(tmpl/render context))))
(defn- build-email-template
[id context]
(let [subj (render-email-template-part :subj id context)
text (render-email-template-part :txt id context)
html (render-email-template-part :html id context)]
(when (or (not subj)
(and (not text)
(not html)))
(ex/raise :type :internal
:code :missing-email-templates))
{:subject subj
:body (into
[{:type "text/plain"
:content text}]
(when html
[{:type "text/html"
:content html}]))}))
(s/def ::priority #{:high :low})
(s/def ::to (s/or :single ::us/email
:multi (s/coll-of ::us/email)))
(s/def ::from ::us/email)
(s/def ::reply-to ::us/email)
(s/def ::lang string?)
(s/def ::extra-data ::us/string)
(s/def ::context
(s/keys :req-un [::to]
:opt-un [::reply-to ::from ::lang ::priority ::extra-data]))
(defn template-factory
([id] (template-factory id {}))
([id extra-context]
(s/assert keyword? id)
(fn [context]
(us/verify ::context context)
(when-let [spec (s/get-spec id)]
(s/assert spec context))
(let [context (merge (if (fn? extra-context)
(extra-context)
extra-context)
context)
email (build-email-template id context)]
(when-not email
(ex/raise :type :internal
:code :email-template-does-not-exists
:hint "seems like the template is wrong or does not exists."
:context {:id id}))
(cond-> (assoc email :id (name id))
(:extra-data context)
(assoc :extra-data (:extra-data context))
(:from context)
(assoc :from (:from context))
(:reply-to context)
(assoc :reply-to (:reply-to context))
(:to context)
(assoc :to (:to context)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC HIGH-LEVEL API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn render
[email-factory context]
(email-factory context))
(defn send!
"Schedule the email for sending."
"Schedule an already defined email to be sent using asynchronously
using worker task."
[{:keys [::conn ::factory] :as context}]
(us/verify fn? factory)
(us/verify some? conn)
@@ -33,12 +263,137 @@
(wrk/submit! (assoc email
::wrk/task :sendmail
::wrk/delay 0
::wrk/max-retries 1
::wrk/max-retries 4
::wrk/priority 200
::wrk/conn conn))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SENDMAIL FN / TASK HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- BOUNCE/COMPLAINS HANDLING
(s/def ::username ::cf/smtp-username)
(s/def ::password ::cf/smtp-password)
(s/def ::tls ::cf/smtp-tls)
(s/def ::ssl ::cf/smtp-ssl)
(s/def ::host ::cf/smtp-host)
(s/def ::port ::cf/smtp-port)
(s/def ::default-reply-to ::cf/smtp-default-reply-to)
(s/def ::default-from ::cf/smtp-default-from)
(s/def ::smtp-config
(s/keys :opt-un [::username
::password
::tls
::ssl
::host
::port
::default-from
::default-reply-to]))
(declare send-to-logger!)
(s/def ::sendmail fn?)
(defmethod ig/pre-init-spec ::sendmail [_]
(s/spec ::smtp-config))
(defmethod ig/init-key ::sendmail
[_ cfg]
(fn [params]
(when (contains? cf/flags :smtp)
(let [session (create-smtp-session cfg)]
(with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))]
(.connect ^Transport transport
^String (:username cfg)
^String (:password cfg))
(let [^MimeMessage message (create-smtp-message cfg session params)]
(.sendMessage ^Transport transport
^MimeMessage message
(.getAllRecipients message))))))
(when (or (contains? cf/flags :log-emails)
(not (contains? cf/flags :smtp)))
(send-to-logger! cfg params))))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::sendmail ::mtx/metrics]))
(defmethod ig/init-key ::handler
[_ {:keys [sendmail]}]
(fn [{:keys [props] :as task}]
(sendmail props)))
(defn- send-to-logger!
[_ email]
(let [body (:body email)
out (with-out-str
(println "email console dump:")
(println "******** start email" (:id email) "**********")
(pp/pprint (dissoc email :body))
(if (string? body)
(println body)
(println (->> body
(filter #(= "text/plain" (:type %)))
(map :content)
first)))
(println "******** end email" (:id email) "**********"))]
(l/info ::l/raw out)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EMAIL FACTORIES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::subject ::us/string)
(s/def ::content ::us/string)
(s/def ::feedback
(s/keys :req-un [::subject ::content]))
(def feedback
"A profile feedback email."
(template-factory ::feedback))
(s/def ::name ::us/string)
(s/def ::register
(s/keys :req-un [::name]))
(def register
"A new profile registration welcome email."
(template-factory ::register))
(s/def ::token ::us/string)
(s/def ::password-recovery
(s/keys :req-un [::name ::token]))
(def password-recovery
"A password recovery notification email."
(template-factory ::password-recovery))
(s/def ::pending-email ::us/email)
(s/def ::change-email
(s/keys :req-un [::name ::pending-email ::token]))
(def change-email
"Password change confirmation email"
(template-factory ::change-email))
(s/def ::emails.invite-to-team/invited-by ::us/string)
(s/def ::emails.invite-to-team/team ::us/string)
(s/def ::emails.invite-to-team/token ::us/string)
(s/def ::invite-to-team
(s/keys :req-un [::emails.invite-to-team/invited-by
::emails.invite-to-team/token
::emails.invite-to-team/team]))
(def invite-to-team
"Teams member invitation email."
(template-factory ::invite-to-team))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; BOUNCE/COMPLAINS HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def sql:profile-complaint-report
"select (select count(*)
@@ -85,106 +440,3 @@
{:email email :type "bounce"}
{:limit 10}))]
(>= (count reports) threshold))))
;; --- EMAIL FACTORIES
(s/def ::subject ::us/string)
(s/def ::content ::us/string)
(s/def ::feedback
(s/keys :req-un [::subject ::content]))
(def feedback
"A profile feedback email."
(emails/template-factory ::feedback))
(s/def ::name ::us/string)
(s/def ::register
(s/keys :req-un [::name]))
(def register
"A new profile registration welcome email."
(emails/template-factory ::register))
(s/def ::token ::us/string)
(s/def ::password-recovery
(s/keys :req-un [::name ::token]))
(def password-recovery
"A password recovery notification email."
(emails/template-factory ::password-recovery))
(s/def ::pending-email ::us/email)
(s/def ::change-email
(s/keys :req-un [::name ::pending-email ::token]))
(def change-email
"Password change confirmation email"
(emails/template-factory ::change-email))
(s/def :internal.emails.invite-to-team/invited-by ::us/string)
(s/def :internal.emails.invite-to-team/team ::us/string)
(s/def :internal.emails.invite-to-team/token ::us/string)
(s/def ::invite-to-team
(s/keys :req-un [:internal.emails.invite-to-team/invited-by
:internal.emails.invite-to-team/token
:internal.emails.invite-to-team/team]))
(def invite-to-team
"Teams member invitation email."
(emails/template-factory ::invite-to-team))
;; --- SENDMAIL TASK
(declare send-console!)
(s/def ::username ::cf/smtp-username)
(s/def ::password ::cf/smtp-password)
(s/def ::tls ::cf/smtp-tls)
(s/def ::ssl ::cf/smtp-ssl)
(s/def ::host ::cf/smtp-host)
(s/def ::port ::cf/smtp-port)
(s/def ::default-reply-to ::cf/smtp-default-reply-to)
(s/def ::default-from ::cf/smtp-default-from)
(defmethod ig/pre-init-spec ::sendmail-handler [_]
(s/keys :opt-un [::username
::password
::tls
::ssl
::host
::port
::default-from
::default-reply-to]))
(defmethod ig/init-key ::sendmail-handler
[_ cfg]
(fn [{:keys [props] :as task}]
(let [enabled? (or (contains? cf/flags :smtp)
(cf/get :smtp-enabled)
(:enabled task))]
(when enabled?
(emails/send! cfg props))
(when (contains? cf/flags :log-emails)
(send-console! cfg props)))))
(defn- send-console!
[_ email]
(let [body (:body email)
out (with-out-str
(println "email console dump:")
(println "******** start email" (:id email) "**********")
(pp/pprint (dissoc email :body))
(if (string? body)
(println body)
(println (->> body
(filter #(= "text/plain" (:type %)))
(map :content)
first)))
(println "******** end email" (:id email) "**********"))]
(l/info ::l/raw out)))

View File

@@ -2,16 +2,15 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.transit :as t]
[app.http.doc :as doc]
[app.http.errors :as errors]
[app.http.middleware :as middleware]
[app.http.middleware :as mw]
[app.metrics :as mtx]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
@@ -67,8 +66,10 @@
:xnio/worker-threads (:worker-threads cfg)
:xnio/dispatch (:executor cfg)
:ring/async true}
handler (if (some? router)
(wrap-router router)
handler)
server (yt/server handler (d/without-nils options))]
(assoc cfg :server (yt/start! server))))
@@ -113,44 +114,52 @@
;; HTTP ROUTER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::rpc map?)
(s/def ::oauth map?)
(s/def ::storage map?)
(s/def ::assets map?)
(s/def ::feedback fn?)
(s/def ::ws fn?)
(s/def ::audit-handler fn?)
(s/def ::debug map?)
(s/def ::awsns-handler fn?)
(s/def ::debug-routes (s/nilable vector?))
(s/def ::doc-routes (s/nilable vector?))
(s/def ::feedback fn?)
(s/def ::oauth map?)
(s/def ::oidc-routes (s/nilable vector?))
(s/def ::rpc-routes (s/nilable vector?))
(s/def ::session map?)
(s/def ::storage map?)
(s/def ::ws fn?)
(defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::rpc ::mtx/metrics ::ws ::oauth ::storage ::assets
::session ::feedback ::awsns-handler ::debug ::audit-handler]))
(s/keys :req-un [::mtx/metrics
::ws
::storage
::assets
::session
::feedback
::awsns-handler
::debug-routes
::oidc-routes
::audit-handler
::rpc-routes
::doc-routes]))
(defmethod ig/init-key ::router
[_ {:keys [ws session rpc oauth metrics assets feedback debug] :as cfg}]
[_ {:keys [ws session metrics assets feedback] :as cfg}]
(rr/router
[["" {:middleware [[middleware/server-timing]
[middleware/format-response]
[middleware/params]
[middleware/parse-request]
[middleware/errors errors/handle]
[middleware/restrict-methods]]}
["/metrics" {:handler (:handler metrics)}]
[["" {:middleware [[mw/server-timing]
[mw/format-response]
[mw/params]
[mw/parse-request]
[mw/errors errors/handle]
[mw/restrict-methods]]}
["/metrics" {:handler (::mtx/handler metrics)
:allowed-methods #{:get}}]
["/assets" {:middleware [(:middleware session)]}
["/by-id/:id" {:handler (:objects-handler assets)}]
["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}]
["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]]
["/dbg" {:middleware [(:middleware session)]}
["" {:handler (:index debug)}]
["/changelog" {:handler (:changelog debug)}]
["/error-by-id/:id" {:handler (:retrieve-error debug)}]
["/error/:id" {:handler (:retrieve-error debug)}]
["/error" {:handler (:retrieve-error-list debug)}]
["/file/data" {:handler (:file-data debug)}]
["/file/changes" {:handler (:retrieve-file-changes debug)}]]
(:debug-routes cfg)
["/webhooks"
["/sns" {:handler (:awsns-handler cfg)
@@ -160,23 +169,12 @@
:handler ws
:allowed-methods #{:get}}]
["/api" {:middleware [[middleware/cors]
(:middleware session)]}
["/health" {:handler (:health-check debug)}]
["/_doc" {:handler (doc/handler rpc)
:allowed-methods #{:get}}]
["/feedback" {:handler feedback
:allowed-methods #{:post}}]
["/auth/oauth/:provider" {:handler (:handler oauth)
:allowed-methods #{:post}}]
["/auth/oauth/:provider/callback" {:handler (:callback-handler oauth)
:allowed-methods #{:get}}]
["/api" {:middleware [[mw/cors]
[(:middleware session)]]}
["/audit/events" {:handler (:audit-handler cfg)
:allowed-methods #{:post}}]
["/rpc"
["/query/:type" {:handler (:query-handler rpc)}]
["/mutation/:type" {:handler (:mutation-handler rpc)
:allowed-methods #{:post}}]]]]]))
["/feedback" {:handler feedback
:allowed-methods #{:post}}]
(:doc-routes cfg)
(:oidc-routes cfg)
(:rpc-routes cfg)]]]))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.assets
"Assets related handlers."
@@ -29,7 +29,7 @@
(defn coerce-id
[id]
(let [res (us/uuid-conformer id)]
(let [res (parse-uuid id)]
(when-not (uuid? res)
(ex/raise :type :not-found
:hint "object not found"))
@@ -52,18 +52,12 @@
(let [mdata (meta obj)
backend (sto/resolve-backend storage (:backend obj))]
(case (:type backend)
:db
(p/let [body (sto/get-object-bytes storage obj)]
(yrs/response :status 200
:body body
:headers {"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}))
:s3
(p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
(yrs/response :status 307
: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))}))
:fs

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.awsns
"AWS SNS webhook handler for bounces."
@@ -11,11 +11,14 @@
[app.common.logging :as l]
[app.db :as db]
[app.db.sql :as sql]
[app.http.client :as http]
[app.tokens :as tokens]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[jsonista.core :as j]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(declare parse-json)
@@ -23,17 +26,18 @@
(declare parse-notification)
(declare process-report)
(s/def ::http-client fn?)
(s/def ::http-client ::http/client)
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::http-client]))
(s/keys :req-un [::db/pool ::http-client ::sprops]))
(defmethod ig/init-key ::handler
[_ {:keys [executor] :as cfg}]
(fn [request respond _]
(let [data (slurp (:body request))]
(px/run! executor #(handle-request cfg data))
(respond (yrs/response 200)))))
(let [data (-> request yrq/body slurp)]
(px/run! executor #(handle-request cfg data)))
(respond (yrs/response 200))))
(defn handle-request
[{:keys [http-client] :as cfg} data]
@@ -45,7 +49,7 @@
(let [surl (get body "SubscribeURL")
stopic (get body "TopicArn")]
(l/info :action "subscription received" :topic stopic :url surl)
(http-client {:uri surl :method :post :timeout 10000} {:sync? true}))
(http/req! http-client {:uri surl :method :post :timeout 10000} {:sync? true}))
(= mtype "Notification")
(when-let [message (parse-json (get body "Message"))]
@@ -96,10 +100,10 @@
(get mail "headers")))
(defn- extract-identity
[{:keys [tokens] :as cfg} headers]
[{:keys [sprops]} headers]
(let [tdata (get headers "x-penpot-data")]
(when-not (str/empty? tdata)
(let [result (tokens :verify {:token tdata :iss :profile-identity})]
(let [result (tokens/verify sprops {:token tdata :iss :profile-identity})]
(:profile-id result)))))
(defn- parse-notification

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.client
"Http client abstraction layer."
@@ -12,6 +12,8 @@
[integrant.core :as ig]
[java-http-clj.core :as http]))
(s/def ::client fn?)
(defmethod ig/pre-init-spec :app.http/client [_]
(s/keys :req-un [::wrk/executor]))
@@ -28,3 +30,11 @@
(http/send req {:client client :as response-type})
(http/send-async req {:client client :as response-type}))))
{::client client})))
(defn req!
"A convencience toplevel function for gradual migration to a new API
convention."
([client request]
(client request))
([client request options]
(client request options)))

View File

@@ -2,39 +2,41 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.debug
(:refer-clojure :exclude [error-handler])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.rpc.mutations.files :as m.files]
[app.http.middleware :as mw]
[app.rpc.commands.binfile :as binf]
[app.rpc.mutations.files :refer [create-file]]
[app.rpc.queries.profile :as profile]
[app.util.blob :as blob]
[app.util.template :as tmpl]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs]
[datoteka.io :as io]
[emoji.core :as emj]
[fipp.edn :as fpp]
[integrant.core :as ig]
[markdown.core :as md]
[markdown.transformers :as mdt]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
;; (selmer.parser/cache-off!)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn authorized?
[pool {:keys [profile-id]}]
(or (= "devenv" (cf/get :host))
@@ -42,16 +44,34 @@
admins (or (cf/get :admins) #{})]
(contains? admins (:email profile)))))
(defn index
(defn prepare-response
[body]
(let [headers {"content-type" "application/transit+json"}]
(yrs/response :status 200 :body body :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)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INDEX
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn index-handler
[{:keys [pool]} request]
(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 "templates/debug.tmpl")
:body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def sql:retrieve-range-of-changes
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn")
@@ -59,28 +79,16 @@
(def sql:retrieve-single-change
"select revn, changes, data from file_change where file_id=? and revn = ?")
(defn prepare-response
[{:keys [params] :as request} body filename]
(when-not body
(ex/raise :type :not-found
:code :enpty-data
:hint "empty response"))
(cond-> (yrs/response :status 200
:body body
:headers {"content-type" "application/transit+json"})
(contains? params :download)
(update :headers assoc "content-disposition" (str "attachment; filename=" filename))))
(defn- retrieve-file-data
[{:keys [pool]} {:keys [params] :as request}]
[{:keys [pool]} {:keys [params profile-id] :as request}]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [file-id (some-> (get-in request [:params :file-id]) uuid/uuid)
revn (some-> (get-in request [:params :revn]) d/parse-integer)
(let [file-id (some-> params :file-id parse-uuid)
revn (some-> params :revn parse-long)
filename (str file-id)]
(when-not file-id
(ex/raise :type :validation
:code :missing-arguments))
@@ -88,35 +96,63 @@
(let [data (if (integer? revn)
(some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data)
(some-> (db/get-by-id pool :file file-id) :data))]
(if (contains? params :download)
(-> (prepare-response request data filename)
(update :headers assoc "content-type" "application/octet-stream"))
(prepare-response request (some-> data blob/decode) filename)))))
(when-not data
(ex/raise :type :not-found
:code :enpty-data
:hint "empty response"))
(cond
(contains? params :download)
(prepare-download-response data filename)
(contains? params :clone)
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
data (some-> data blob/decode)]
(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"))
:else
(prepare-response (some-> data blob/decode))))))
(defn- is-file-exists?
[pool id]
(let [sql "select exists (select 1 from file where id=?) as exists;"]
(-> (db/exec-one! pool [sql id]) :exists)))
(defn- upload-file-data
[{:keys [pool]} {:keys [profile-id params] :as request}]
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
data (some-> params :file :path fs/slurp-bytes blob/decode)]
data (some-> params :file :path io/read-as-bytes blob/decode)]
(if (and data project-id)
(let [fname (str "imported-file-" (dt/now))
file-id (try
(uuid/uuid (-> params :file :filename))
(catch Exception _ (uuid/next)))
file (db/exec-one! pool (sql/select :file {:id file-id}))]
(if file
(db/update! pool :file
{:data (blob/encode data)}
{:id file-id})
(m.files/create-file pool {:id file-id
:name fname
:project-id project-id
:profile-id profile-id
:data data}))
(yrs/response 200 "OK"))
(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))]
(if (and overwrite? file-id
(is-file-exists? pool file-id))
(do
(db/update! pool :file
{:data (blob/encode data)}
{:id file-id})
(yrs/response 200 "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"))))
(yrs/response 500 "ERROR"))))
(defn file-data
(defn file-data-handler
[cfg request]
(case (yrq/method request)
:get (retrieve-file-data cfg request)
@@ -124,47 +160,51 @@
(ex/raise :type :http
:code :method-not-found)))
(defn retrieve-file-changes
[{:keys [pool]} request]
(defn file-changes-handler
[{:keys [pool]} {:keys [params] :as request}]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [file-id (some-> (get-in request [:params :id]) uuid/uuid)
revn (or (get-in request [:params :revn]) "latest")
filename (str file-id)]
(letfn [(retrieve-changes [file-id revn]
(if (str/includes? revn ":")
(let [[start end] (->> (str/split revn #":")
(map str/trim)
(map parse-long))]
(some->> (db/exec! pool [sql:retrieve-range-of-changes file-id start end])
(map :changes)
(map blob/decode)
(mapcat identity)
(vec)))
(when (or (not file-id) (not revn))
(ex/raise :type :validation
:code :invalid-arguments
:hint "missing arguments"))
(if-let [revn (parse-long revn)]
(let [item (db/exec-one! pool [sql:retrieve-single-change file-id revn])]
(some-> item :changes blob/decode vec))
(ex/raise :type :validation :code :invalid-arguments))))]
(cond
(d/num-string? revn)
(let [item (db/exec-one! pool [sql:retrieve-single-change file-id (d/parse-integer revn)])]
(prepare-response request (some-> item :changes blob/decode vec) filename))
(let [file-id (some-> params :id parse-uuid)
revn (or (some-> params :revn parse-long) "latest")
filename (str file-id)]
(str/includes? revn ":")
(let [[start end] (->> (str/split revn #":")
(map str/trim)
(map d/parse-integer))
items (db/exec! pool [sql:retrieve-range-of-changes file-id start end])]
(prepare-response request
(some->> items
(map :changes)
(map blob/decode)
(mapcat identity)
(vec))
filename))
:else
(ex/raise :type :validation :code :invalid-arguments))))
(when (or (not file-id) (not revn))
(ex/raise :type :validation
:code :invalid-arguments
:hint "missing arguments"))
(let [data (retrieve-changes file-id revn)]
(if (contains? params :download)
(prepare-download-response data filename)
(prepare-response data))))))
(defn retrieve-error
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ERROR BROWSER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn error-handler
[{:keys [pool]} request]
(letfn [(parse-id [request]
(let [id (get-in request [:path-params :id])
id (us/uuid-conformer id)]
id (parse-uuid id)]
(when (uuid? id)
id)))
@@ -174,11 +214,10 @@
(render-template [report]
(let [context (dissoc report
:trace :cause :params :data :spec-problems
:trace :cause :params :data :spec-problems :message
:spec-explain :spec-value :error :explain :hint)
params {:context (with-out-str
(fpp/pprint context {:width 200}))
:hint (:hint report)
params {:context (pp/pprint-str context :width 200)
:hint (:hint report)
:spec-explain (:spec-explain report)
:spec-problems (:spec-problems report)
:spec-value (:spec-value report)
@@ -186,7 +225,7 @@
:trace (or (:trace report)
(some-> report :error :trace))
:params (:params report)}]
(-> (io/resource "templates/error-report.tmpl")
(-> (io/resource "app/templates/error-report.tmpl")
(tmpl/render params))))]
(when-not (authorized? pool request)
@@ -206,7 +245,7 @@
(def sql:error-reports
"select id, created_at from server_error_report order by created_at desc limit 100")
(defn retrieve-error-list
(defn error-list-handler
[{:keys [pool]} request]
(when-not (authorized? pool request)
(ex/raise :type :authentication
@@ -214,19 +253,104 @@
(let [items (db/exec! pool [sql:error-reports])
items (map #(update % :created-at dt/format-instant :rfc1123) items)]
(yrs/response :status 200
:body (-> (io/resource "templates/error-list.tmpl")
:body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items}))
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})))
(defn health-check
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXPORT/IMPORT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn export-handler
[{:keys [pool] :as cfg} {:keys [params profile-id] :as request}]
(let [file-ids (->> (:file-ids params)
(remove empty?)
(mapv parse-uuid))
libs? (contains? params :includelibs)
clone? (contains? params :clone)
embed? (contains? params :embedassets)]
(when-not (seq file-ids)
(ex/raise :type :validation
:code :missing-arguments))
(let [path (-> cfg
(assoc ::binf/file-ids file-ids)
(assoc ::binf/embed-assets? embed?)
(assoc ::binf/include-libraries? libs?)
(binf/export-to-tmpfile!))]
(if clone?
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)]
(binf/import!
(assoc cfg
::binf/input path
::binf/overwrite? false
::binf/ignore-index-errors? true
::binf/profile-id profile-id
::binf/project-id project-id))
(yrs/response
:status 200
:headers {"content-type" "text/plain"}
:body "OK CLONED"))
(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
[{:keys [pool] :as cfg} {:keys [params profile-id] :as request}]
(when-not (contains? params :file)
(ex/raise :type :validation
:code :missing-upload-file
:hint "missing upload file"))
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
overwrite? (contains? params :overwrite)
migrate? (contains? params :migrate)
ignore-index-errors? (contains? params :ignore-index-errors)]
(when-not project-id
(ex/raise :type :validation
:code :missing-project
:hint "project not found"))
(binf/import!
(assoc cfg
::binf/input (-> params :file :path)
::binf/overwrite? overwrite?
::binf/migrate? migrate?
::binf/ignore-index-errors? ignore-index-errors?
::binf/profile-id profile-id
::binf/project-id project-id))
(yrs/response
:status 200
:headers {"content-type" "text/plain"}
:body "OK")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn health-handler
"Mainly a task that performs a health check."
[{:keys [pool]} _]
(db/with-atomic [conn pool]
(db/exec-one! conn ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK")))
(try
(db/exec-one! conn ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK")
(catch Throwable cause
(l/warn :hint "unable to execute query on health handler"
:cause cause)
(yrs/response 503 "KO")))))
(defn changelog
(defn changelog-handler
[_ _]
(letfn [(transform-emoji [text state]
[(emj/emojify text) state])
@@ -238,22 +362,42 @@
:body (-> clog slurp md->html))
(yrs/response :status 404 :body "NOT FOUND"))))
(defn- wrap-async
[{:keys [executor] :as cfg} f]
(fn [request respond raise]
(-> (px/submit! executor #(f cfg request))
(p/then respond)
(p/catch raise))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INIT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/pre-init-spec ::handlers [_]
(s/keys :req-un [::db/pool ::wrk/executor]))
(def with-authorization
{:compile
(fn [& _]
(fn [handler pool]
(fn [request respond raise]
(if (authorized? pool request)
(handler request respond raise)
(raise (ex/error :type :authentication
:code :only-admins-allowed))))))})
(defmethod ig/init-key ::handlers
[_ cfg]
{:index (wrap-async cfg index)
:health-check (wrap-async cfg health-check)
:retrieve-file-changes (wrap-async cfg retrieve-file-changes)
:retrieve-error (wrap-async cfg retrieve-error)
:retrieve-error-list (wrap-async cfg retrieve-error-list)
:file-data (wrap-async cfg file-data)
:changelog (wrap-async cfg changelog)})
(s/def ::session map?)
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req-un [::db/pool ::wrk/executor ::session]))
(defmethod ig/init-key ::routes
[_ {:keys [session pool executor] :as cfg}]
[["/readyz" {:middleware [[mw/with-dispatch executor]
[mw/with-config cfg]]
:handler health-handler}]
["/dbg" {:middleware [[(:middleware session)]
[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}]]])

View File

@@ -1,54 +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) UXBOX Labs SL
(ns app.http.doc
"API autogenerated documentation."
(:require
[app.common.data :as d]
[app.config :as cf]
[app.util.services :as sv]
[app.util.template :as tmpl]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[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}})))
(defn prepare-context
[rpc]
(letfn [(gen-doc [type [name f]]
(let [mdata (meta f)]
;; (prn name mdata)
{:type (d/name type)
:name (d/name name)
:auth (:auth mdata true)
:docs (::sv/docs mdata)
:spec (get-spec-str (::sv/spec mdata))}))]
{:query-methods
(into []
(map (partial gen-doc :query))
(->> rpc :methods :query (sort-by first)))
:mutation-methods
(into []
(map (partial gen-doc :mutation))
(->> rpc :methods :mutation (sort-by first)))}))
(defn handler
[rpc]
(let [context (prepare-context rpc)]
(if (contains? cf/flags :backend-api-doc)
(fn [_ respond _]
(respond (yrs/response 200 (-> (io/resource "api-doc.tmpl")
(tmpl/render context)))))
(fn [_ respond _]
(respond (yrs/response 404))))))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.errors
"A errors handling for the http server."
@@ -10,6 +10,7 @@
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.http :as-alias http]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[yetti.request :as yrq]
@@ -50,6 +51,11 @@
[err _]
(yrs/response 400 (ex-data err)))
(defmethod handle-exception :rate-limit
[err _]
(let [headers (-> err ex-data ::http/headers)]
(yrs/response :status 429 :body "" :headers headers)))
(defmethod handle-exception :validation
[err _]
(let [{:keys [code] :as data} (ex-data err)]
@@ -71,7 +77,7 @@
[error request]
(let [edata (ex-data error)
explain (us/pretty-explain edata)]
(l/error ::l/raw (ex-message error)
(l/error ::l/raw (str (ex-message error) "\n" explain)
::l/context (get-context request)
:cause error)
(yrs/response :status 500
@@ -143,13 +149,11 @@
(defn handle
[cause request]
(cond
(or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))
(handle-exception (.getCause ^Throwable cause) request)
(ex/wrapped? cause)
(let [context (meta cause)
cause (deref cause)]

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.feedback
"A general purpose feedback module."

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.middleware
(:require
@@ -12,6 +12,8 @@
[app.config :as cf]
[app.util.json :as json]
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.adapter :as yt]
[yetti.middleware :as ymw]
[yetti.request :as yrq]
@@ -35,14 +37,14 @@
(let [header (yrq/get-header request "content-type")]
(cond
(str/starts-with? header "application/transit+json")
(with-open [is (-> request yrq/body yrq/body-stream)]
(with-open [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 (-> request yrq/body yrq/body-stream)]
(with-open [is (yrq/body request)]
(let [params (json/read is)]
(-> request
(assoc :body-params params)
@@ -113,7 +115,7 @@
(format-response [response request]
(let [body (yrs/body response)]
(if (coll? body)
(if (or (boolean? body) (coll? body))
(let [qs (yrq/query request)
opts (if (or (contains? cf/flags :transit-readable-response)
(str/includes? qs "transit_verbose"))
@@ -192,3 +194,23 @@
(def restrict-methods
{:name ::restrict-methods
:compile compile-restrict-methods})
(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)))))})

View File

@@ -1,459 +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) UXBOX Labs SL
(ns app.http.oauth
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uri :as u]
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.queries.profile :as profile]
[app.util.json :as json]
[app.util.time :as dt]
[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]))
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
(let [public (u/uri (:public-uri cfg))]
(str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback")))))
(defn- build-auth-uri
[{:keys [provider] :as cfg} state]
(let [params {:client_id (:client-id provider)
:redirect_uri (build-redirect-uri cfg)
:response_type "code"
:state state
:scope (str/join " " (:scopes provider []))}
query (u/map->query-string params)]
(-> (u/uri (:auth-uri provider))
(assoc :query query)
(str))))
(defn- qualify-props
[provider props]
(reduce-kv (fn [result k v]
(assoc result (keyword (:name provider) (name k)) v))
{}
props))
(defn retrieve-access-token
[{:keys [provider http-client] :as cfg} code]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code code
:grant_type "authorization_code"
:redirect_uri (build-redirect-uri cfg)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"
"accept" "application/json"}
:uri (:token-uri provider)
:body (u/map->query-string params)}]
(p/then
(http-client req)
(fn [{:keys [status body] :as res}]
(if (= status 200)
(let [data (json/read 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))))))
(defn- retrieve-user-info
[{:keys [provider http-client] :as cfg} tdata]
(letfn [(retrieve []
(http-client {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}))
(retrieve-emails []
(if (some? (:emails-uri provider))
(http-client {:uri (:emails-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get})
(p/resolved {:status 200})))
(validate-response [[retrieve-res emails-res]]
(when-not (s/int-in-range? 200 300 (:status retrieve-res))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status retrieve-res)
:http-body (:body retrieve-res)))
(when-not (s/int-in-range? 200 300 (:status emails-res))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status emails-res)
:http-body (:body emails-res)))
[retrieve-res emails-res])
(get-email [info]
(let [attr-kw (cf/get :oidc-email-attr :email)]
(get info attr-kw)))
(get-name [info]
(let [attr-kw (cf/get :oidc-name-attr :name)]
(get info attr-kw)))
(process-response [[retrieve-res emails-res]]
(let [info (json/read (:body retrieve-res))
email (if (some? (:extract-email-callback provider))
((:extract-email-callback provider) emails-res)
(get-email info))]
{:backend (:name provider)
:email email
:fullname (or (get-name info) email)
:props (->> (dissoc info :name :email)
(qualify-props provider))}))
(validate-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)]
(-> (p/all [(retrieve) (retrieve-emails)])
(p/then' validate-response)
(p/then' process-response)
(p/then' validate-info))))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
(s/def ::fullname ::us/not-empty-string)
(s/def ::props (s/map-of ::us/keyword any?))
(s/def ::info
(s/keys :req-un [::backend
::email
::fullname
::props]))
(defn retrieve-info
[{:keys [tokens provider] :as cfg} {:keys [params] :as request}]
(letfn [(validate-oidc [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 [provider-roles (into #{} (:roles provider))
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
roles (get info attr)]
(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? provider-roles profile-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enough permissions"))))
info)
(post-process [state info]
(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))))]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
(let [state (get params :state)
code (get params :code)
state (tokens :verify {: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))))))
;; --- HTTP HANDLERS
(defn- retrieve-profile
[{:keys [pool executor] :as cfg} info]
(px/with-dispatch executor
(with-open [conn (db/open pool)]
(some->> (:email info)
(profile/retrieve-profile-data-by-email conn)
(profile/populate-additional-data conn)
(profile/decode-profile-row)))))
(defn- redirect-response
[uri]
(yrs/response :status 302 :headers {"location" (str uri)}))
(defn- generate-error-redirect
[cfg error]
(let [uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
(redirect-response uri)))
(defn- generate-redirect
[{:keys [tokens session audit] :as cfg} request info profile]
(if profile
(let [sxf ((:create session) (:id profile))
token (or (:invitation-token info)
(tokens :generate {:iss :auth
:exp (dt/in-future "15m")
:profile-id (:id profile)}))
params {:token token}
uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string params)))]
(when (fn? audit)
(audit :cmd :submit
:type "mutation"
:name "login"
:profile-id (:id profile)
:ip-addr (audit/parse-client-ip request)
: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 info)
params (d/without-nils
{:token token
:fullname (:fullname info)})
uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))))
(defn- auth-handler
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise]
(try
(let [props (audit/extract-utm-params params)
state (tokens :generate
{:iss :oauth
:invitation-token (:invitation-token params)
:props props
:exp (dt/in-future "15m")})
uri (build-auth-uri cfg state)]
(respond (yrs/response 200 {:redirect-uri uri})))
(catch Throwable cause
(raise cause))))
(defn- callback-handler
[cfg request respond _]
(letfn [(process-request []
(p/let [info (retrieve-info cfg request)
profile (retrieve-profile cfg info)]
(generate-redirect cfg request info profile)))
(handle-error [cause]
(l/error :hint "error on oauth process" :cause cause)
(respond (generate-error-redirect cfg cause)))]
(-> (process-request)
(p/then respond)
(p/catch handle-error))))
;; --- INIT
(declare initialize)
(s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?)
(s/def ::tokens fn?)
(s/def ::rpc map?)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool]))
(defn wrap-handler
[cfg handler]
(fn [request respond raise]
(let [provider (get-in request [:path-params :provider])
provider (get-in @cfg [:providers provider])]
(if provider
(handler (assoc @cfg :provider provider)
request
respond
raise)
(raise
(ex/error
:type :not-found
:provider provider
:hint "provider not configured"))))))
(defmethod ig/init-key ::handler
[_ cfg]
(let [cfg (initialize cfg)]
{:handler (wrap-handler cfg auth-handler)
:callback-handler (wrap-handler cfg callback-handler)}))
(defn- discover-oidc-config
[{:keys [http-client]} {:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (ex/try (http-client {: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/read (:body response))]
{:token-uri (get data :token_endpoint)
:auth-uri (get data :authorization_endpoint)
:user-uri (get data :userinfo_endpoint)})
:else
(do
(l/warn :hint "unable to discover OIDC configuration"
:uri (str discovery-uri)
:response-status-code (:status response))
nil))))
(defn- obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
(defn- initialize-oidc-provider
[cfg]
(let [opts {:base-uri (cf/get :oidc-base-uri)
:client-id (cf/get :oidc-client-id)
:client-secret (cf/get :oidc-client-secret)
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
:name "oidc"}]
(if (and (string? (:base-uri opts))
(string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/debug :hint "initialize oidc provider" :name "generic-oidc"
:opts (update opts :client-secret obfuscate-string))
(if (and (string? (:token-uri opts))
(string? (:user-uri opts))
(string? (:auth-uri opts)))
(do
(l/debug :hint "initialized with user provided configuration")
(assoc-in cfg [:providers "oidc"] opts))
(do
(l/debug :hint "trying to discover oidc provider configuration using BASE_URI")
(if-let [opts' (discover-oidc-config cfg opts)]
(do
(l/debug :hint "discovered opts" :additional-opts opts')
(assoc-in cfg [:providers "oidc"] (merge opts opts')))
cfg))))
cfg)))
(defn- initialize-google-provider
[cfg]
(let [opts {:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret)
:scopes #{"openid" "email" "profile"}
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
:token-uri "https://oauth2.googleapis.com/token"
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
:name "google"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "google"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "google"] opts))
cfg)))
(defn extract-github-email
[response]
(let [emails (json/read (:body response))
primary-email (->> emails
(filter #(:primary %))
first)]
(:email primary-email)))
(defn- initialize-github-provider
[cfg]
(let [opts {:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)
:scopes #{"read:user" "user:email"}
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:emails-uri "https://api.github.com/user/emails"
:extract-email-callback extract-github-email
:user-uri "https://api.github.com/user"
:name "github"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "github"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "github"] opts))
cfg)))
(defn- initialize-gitlab-provider
[cfg]
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
opts {:base-uri base
:client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret)
:scopes #{"openid" "profile" "email"}
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/oauth/userinfo")
:name "gitlab"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "gitlab"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "gitlab"] opts))
cfg)))
(defn- initialize
[cfg]
(let [cfg (agent cfg :error-mode :continue)]
(send-off cfg initialize-google-provider)
(send-off cfg initialize-gitlab-provider)
(send-off cfg initialize-github-provider)
(send-off cfg initialize-oidc-provider)
cfg))

View File

@@ -2,42 +2,45 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.session
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.config :as cfg]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.metrics :as mtx]
[app.util.async :as aa]
[app.tokens :as tokens]
[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]
[yetti.request :as yrq]))
;; A default cookie name for storing the session. We don't allow to configure it.
(def token-cookie-name "auth-token")
;; A default cookie name for storing the session.
(def default-auth-token-cookie-name "auth-token")
;; A cookie that we can use to check from other sites of the same domain if a user
;; is registered. Is not intended for on premise installations, although nothing
;; prevents using it if some one wants to.
(def authenticated-cookie-name "authenticated")
;; A cookie that we can use to check from other sites of the same
;; domain if a user is authenticated.
(def default-authenticated-cookie-name "authenticated")
;; Default value for cookie max-age
(def default-cookie-max-age (dt/duration {:days 7}))
;; Default age for automatic session renewal
(def default-renewal-max-age (dt/duration {:hours 6}))
(defprotocol ISessionStore
(read-session [store key])
(write-session [store key data])
(update-session [store data])
(delete-session [store key]))
(defn- make-database-store
[{:keys [pool tokens executor]}]
[{:keys [pool sprops executor]}]
(reify ISessionStore
(read-session [_ token]
(px/with-dispatch executor
@@ -47,18 +50,24 @@
(px/with-dispatch executor
(let [profile-id (:profile-id data)
user-agent (:user-agent data)
token (tokens :generate {:iss "authentication"
:iat (dt/now)
:uid profile-id})
now (dt/now)
created-at (or (:created-at data) (dt/now))
token (tokens/generate sprops {:iss "authentication"
:iat created-at
:uid profile-id})
params {:user-agent user-agent
:profile-id profile-id
:created-at now
:updated-at now
:created-at created-at
:updated-at created-at
:id token}]
(db/insert! pool :http-session params)
token)))
(db/insert! pool :http-session params))))
(update-session [_ data]
(let [updated-at (dt/now)]
(px/with-dispatch executor
(db/update! pool :http-session
{:updated-at updated-at}
{:id (:id data)})
(assoc data :updated-at updated-at))))
(delete-session [_ token]
(px/with-dispatch executor
@@ -66,7 +75,7 @@
nil))))
(defn make-inmemory-store
[{:keys [tokens]}]
[{:keys [sprops]}]
(let [cache (atom {})]
(reify ISessionStore
(read-session [_ token]
@@ -76,24 +85,32 @@
(p/do
(let [profile-id (:profile-id data)
user-agent (:user-agent data)
token (tokens :generate {:iss "authentication"
:iat (dt/now)
:uid profile-id})
created-at (or (:created-at data) (dt/now))
token (tokens/generate sprops {:iss "authentication"
:iat created-at
:uid profile-id})
params {:user-agent user-agent
:created-at created-at
:updated-at created-at
:profile-id profile-id
:id token}]
(swap! cache assoc token params)
token)))
params)))
(update-session [_ data]
(let [updated-at (dt/now)]
(swap! cache update (:id data) assoc :updated-at updated-at)
(assoc data :updated-at updated-at)))
(delete-session [_ token]
(p/do
(swap! cache dissoc token)
nil)))))
(s/def ::tokens fn?)
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::store [_]
(s/keys :req-un [::db/pool ::wrk/executor ::tokens]))
(s/keys :req-un [::db/pool ::wrk/executor ::sprops]))
(defmethod ig/init-key ::store
[_ {:keys [pool] :as cfg}]
@@ -107,76 +124,122 @@
;; --- IMPL
(defn- create-session!
[store request profile-id]
(let [params {:user-agent (yrq/get-header request "user-agent")
[store profile-id user-agent]
(let [params {:user-agent user-agent
:profile-id profile-id}]
(write-session store nil params)))
(defn- update-session!
[store session]
(update-session store session))
(defn- delete-session!
[store {:keys [cookies] :as request}]
(when-let [token (get-in cookies [token-cookie-name :value])]
(delete-session store token)))
(let [name (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
(when-let [token (get-in cookies [name :value])]
(delete-session store token))))
(defn- retrieve-session
[store request]
(when-let [cookie (yrq/get-cookie request token-cookie-name)]
(-> (read-session store (:value cookie))
(p/then (fn [session]
(when session
{:session-id (:id session)
:profile-id (:profile-id session)}))))))
(let [cookie-name (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
(when-let [cookie (yrq/get-cookie request cookie-name)]
(read-session store (:value cookie)))))
(defn- add-cookies
[response token]
(let [cors? (contains? cfg/flags :cors)
secure? (contains? cfg/flags :secure-session-cookies)
authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
(update response :cookies
(fn [cookies]
(cond-> cookies
:always
(assoc token-cookie-name {:path "/"
:http-only true
:value token
:same-site (if cors? :none :lax)
:secure secure?})
(defn assign-auth-token-cookie
[response {token :id updated-at :updated-at}]
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
created-at (or updated-at (dt/now))
renewal (dt/plus created-at default-renewal-max-age)
expires (dt/plus created-at max-age)
secure? (contains? cf/flags :secure-session-cookies)
cors? (contains? cf/flags :cors)
name (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
cookie {:path "/"
:http-only true
:expires expires
:value token
:comment comment
:same-site (if cors? :none :lax)
:secure secure?}]
(update response :cookies assoc name cookie)))
(some? authenticated-cookie-domain)
(assoc authenticated-cookie-name {:domain authenticated-cookie-domain
:path "/"
:value true
:same-site :strict
:secure secure?}))))))
(defn assign-authenticated-cookie
[response {updated-at :updated-at}]
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
created-at (or updated-at (dt/now))
renewal (dt/plus created-at default-renewal-max-age)
expires (dt/plus created-at max-age)
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
secure? (contains? cf/flags :secure-session-cookies)
domain (cf/get :authenticated-cookie-domain)
name (cf/get :authenticated-cookie-name "authenticated")
cookie {:domain domain
:expires expires
:path "/"
:comment comment
:value true
:same-site :strict
:secure secure?}]
(cond-> response
(string? domain)
(update :cookies assoc name cookie))))
(defn- clear-cookies
(defn clear-auth-token-cookie
[response]
(let [authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
(assoc response :cookies
{token-cookie-name {:path "/"
:value ""
:max-age -1}
authenticated-cookie-name {:domain authenticated-cookie-domain
:path "/"
:value ""
:max-age -1}})))
(let [name (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
(update response :cookies assoc name {:path "/" :value "" :max-age -1})))
(defn- clear-authenticated-cookie
[response]
(let [name (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
domain (cf/get :authenticated-cookie-domain)]
(cond-> response
(string? domain)
(update :cookies assoc name {:domain domain :path "/" :value "" :max-age -1}))))
(defn- make-middleware
[{:keys [::events-ch store] :as cfg}]
{:name :session-middleware
:wrap (fn [handler]
(fn [request respond raise]
(try
(-> (retrieve-session store request)
(p/then' #(merge request %))
(p/finally (fn [request cause]
(if cause
(raise cause)
(do
(when-let [session-id (:session-id request)]
(a/offer! events-ch session-id))
(handler request respond raise))))))
(catch Throwable cause
(raise cause)))))})
[{:keys [store] :as cfg}]
(letfn [;; Check if time reached for automatic session renewal
(renew-session? [{:keys [updated-at] :as session}]
(and (dt/instant? updated-at)
(let [elapsed (dt/diff updated-at (dt/now))]
(neg? (compare default-renewal-max-age elapsed)))))
;; Wrap respond with session renewal code
(wrap-respond [respond session]
(fn [response]
(p/let [session (update-session! store session)]
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)
(respond)))))]
{:name :session
:compile (fn [& _]
(fn [handler]
(fn [request respond raise]
(try
(-> (retrieve-session store request)
(p/finally (fn [session cause]
(cond
(some? cause)
(raise cause)
(nil? session)
(handler request respond raise)
:else
(let [request (-> request
(assoc :profile-id (:profile-id session))
(assoc :session-id (:id session)))
respond (cond-> respond
(renew-session? session)
(wrap-respond session))]
(handler request respond raise))))))
(catch Throwable cause
(raise cause))))))}))
;; --- STATE INIT: SESSION
@@ -193,77 +256,23 @@
(defmethod ig/init-key :app.http/session
[_ {:keys [store] :as cfg}]
(let [events-ch (a/chan (a/dropping-buffer (:buffer-size cfg)))
cfg (assoc cfg ::events-ch events-ch)]
(-> cfg
(assoc :middleware (make-middleware cfg))
(assoc :create (fn [profile-id]
(fn [request response]
(p/let [token (create-session! store request profile-id)]
(add-cookies response token)))))
(assoc :delete (fn [request response]
(p/do
(delete-session! store request)
(-> cfg
(assoc :middleware (make-middleware cfg))
(assoc :create (fn [profile-id]
(fn [request response]
(p/let [uagent (yrq/get-header request "user-agent")
session (create-session! store profile-id uagent)]
(-> response
(assoc :status 204)
(assoc :body nil)
(clear-cookies))))))))
(defmethod ig/halt-key! :app.http/session
[_ data]
(a/close! (::events-ch data)))
;; --- STATE INIT: SESSION UPDATER
(declare update-sessions)
(s/def ::session map?)
(s/def ::max-batch-age ::cfg/http-session-updater-batch-max-age)
(s/def ::max-batch-size ::cfg/http-session-updater-batch-max-size)
(defmethod ig/pre-init-spec ::updater [_]
(s/keys :req-un [::db/pool ::wrk/executor ::mtx/metrics ::session]
:opt-un [::max-batch-age ::max-batch-size]))
(defmethod ig/prep-key ::updater
[_ cfg]
(merge {:max-batch-age (dt/duration {:minutes 5})
:max-batch-size 200}
(d/without-nils cfg)))
(defmethod ig/init-key ::updater
[_ {:keys [session metrics] :as cfg}]
(l/info :action "initialize session updater"
:max-batch-age (str (:max-batch-age cfg))
:max-batch-size (str (:max-batch-size cfg)))
(let [input (aa/batch (::events-ch session)
{:max-batch-size (:max-batch-size cfg)
:max-batch-age (inst-ms (:max-batch-age cfg))})]
(a/go-loop []
(when-let [[reason batch] (a/<! input)]
(let [result (a/<! (update-sessions cfg batch))]
(mtx/run! metrics {:id :session-update-total :inc 1})
(cond
(ex/exception? result)
(l/error :task "updater"
:hint "unexpected error on update sessions"
:cause result)
(= :size reason)
(l/debug :task "updater"
:hint "update sessions"
:reason (name reason)
:count result))
(recur))))))
(defn- update-sessions
[{:keys [pool executor]} ids]
(aa/with-thread executor
(db/exec-one! pool ["update http_session set updated_at=now() where id = ANY(?)"
(into-array String ids)])
(count ids)))
(assign-auth-token-cookie session)
(assign-authenticated-cookie session))))))
(assoc :delete (fn [request response]
(p/do
(delete-session! store request)
(-> response
(assoc :status 204)
(assoc :body nil)
(clear-auth-token-cookie)
(clear-authenticated-cookie)))))))
;; --- STATE INIT: SESSION GC
@@ -277,7 +286,7 @@
(defmethod ig/prep-key ::gc-task
[_ cfg]
(merge {:max-age (dt/duration {:days 15})}
(merge {:max-age default-cookie-max-age}
(d/without-nils cfg)))
(defmethod ig/init-key ::gc-task

View File

@@ -2,35 +2,126 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.http.websocket
"A penpot notification service for file cooperative edition."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.spec :as us]
[app.db :as db]
[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]
[yetti.websocket :as yws]))
(def recv-labels
(into-array String ["recv"]))
(def send-labels
(into-array String ["send"]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBSOCKET HOOKS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def state (atom {}))
(defn- on-connect
[{:keys [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 [metrics]} _ message]
(mtx/run! metrics
:id :websocket-messages-total
:labels recv-labels
:inc 1)
message)
(defn- on-snd-message
[{:keys [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))))
(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)
:http-session-id (::ws/http-session-id @wsp)
:subscribed-file (-> wsp deref ::file-subscription :file-id)
:subscribed-team (-> wsp deref ::team-subscription :team-id)}))
(defn repl-print-connection-info
[id]
(some-> id repl-get-connection-info pp/pprint))
(defn repl-print-connection-info-for-file
[file-id]
(some->> (repl-get-connections-for-file file-id)
(map repl-get-connection-info)
(pp/pprint)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBSOCKET HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmulti handle-message
(fn [_ message]
(fn [_ _ message]
(:type message)))
(defmethod handle-message :connect
[wsp _]
(l/trace :fn "handle-message" :event :connect)
[cfg wsp _]
(let [msgbus-fn (:msgbus @wsp)
(let [msgbus (:msgbus cfg)
conn-id (::ws/id @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
@@ -38,158 +129,191 @@
xform (remove #(= (:session-id %) session-id))
channel (a/chan (a/dropping-buffer 16) xform)]
(swap! wsp assoc ::profile-subs-channel channel)
(l/trace :fn "handle-message" :event "connect" :conn-id conn-id)
;; 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)
(msgbus-fn :cmd :sub :topic profile-id :chan channel)))
(mbus/sub! msgbus :topic profile-id :chan channel)))
(defmethod handle-message :disconnect
[wsp _]
(l/trace :fn "handle-message" :event :disconnect)
(a/go
(let [msgbus-fn (:msgbus @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
profile-ch (::profile-subs-channel @wsp)
subs (::subscriptions @wsp)]
;; Close the main profile subscription
(a/close! profile-ch)
(a/<! (msgbus-fn :cmd :purge :chans [profile-ch]))
;; Close all other active subscrption on this websocket context.
(doseq [{:keys [channel topic]} (map second subs)]
(a/close! channel)
(a/<! (msgbus-fn :cmd :pub :topic topic
:message {:type :disconnect
:profile-id profile-id
:session-id session-id}))
(a/<! (msgbus-fn :cmd :purge :chans [channel]))))))
(defmethod handle-message :subscribe-team
[wsp {:keys [team-id] :as params}]
(l/trace :fn "handle-message" :event :subscribe-team :team-id team-id)
(let [msgbus-fn (:msgbus @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
subs (get-in @wsp [::subscriptions team-id])
xform (comp
(remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id team-id)))]
(a/go
(when (not= (:team-id subs) team-id)
;; if it exists we just need to close that
(when-let [channel (:channel subs)]
(a/close! channel)
(a/<! (msgbus-fn :cmd :purge :chans [channel])))
(let [channel (a/chan (a/dropping-buffer 64) xform)]
;; Message forwarding
(a/pipe channel output-ch false)
(let [state {:team-id team-id :channel channel :topic team-id}]
(swap! wsp update ::subscriptions assoc team-id state))
(a/<! (msgbus-fn :cmd :sub :topic team-id :chan channel)))))))
(defmethod handle-message :subscribe-file
[wsp {:keys [subs-id file-id] :as params}]
(l/trace :fn "handle-message" :event :subscribe-file :subs-id subs-id :file-id file-id)
(let [msgbus-fn (:msgbus @wsp)
[cfg wsp _]
(let [msgbus (:msgbus cfg)
conn-id (::ws/id @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
profile-ch (::profile-subscription @wsp)
fsub (::file-subscription @wsp)
tsub (::team-subscription @wsp)
message {:type :disconnect
:subs-id profile-id
:profile-id profile-id
:session-id session-id}]
(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)))
(when-let [{:keys [topic channel]} fsub]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))
(a/<! (mbus/pub! msgbus :topic topic :message message))))))
(defmethod handle-message :subscribe-team
[cfg wsp {:keys [team-id] :as params}]
(let [msgbus (: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 subs-id)))
(map #(assoc % :subs-id team-id)))
channel (a/chan (a/dropping-buffer 64) xform)]
;; 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}]
(a/<! (msgbus-fn :cmd :pub
:topic file-id
:message message))))
(a/>! output-ch message)
(recur)))
(l/trace :fn "handle-message"
:event :subscribe-team
:team-id team-id
:conn-id conn-id)
(a/pipe channel output-ch false)
(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] :as params}]
(let [msgbus (: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)]
(l/trace :fn "handle-message"
:event :subscribe-file
:file-id file-id
:conn-id conn-id)
(let [state {:file-id file-id :channel channel :topic file-id}]
(swap! wsp update ::subscriptions assoc subs-id state))
(swap! wsp assoc ::file-subscription state))
(a/go
;; Close previous subscription if exists
(when-let [channel (:channel prev-subs)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))))
;; 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}]
(a/<! (mbus/pub! msgbus :topic file-id :message message))))
(a/>! output-ch message)
(recur))))
(a/go
;; Subscribe to file topic
(a/<! (msgbus-fn :cmd :sub :topic file-id :chan channel))
(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/<! (msgbus-fn :cmd :pub
:topic file-id
:message message))))))
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
(defmethod handle-message :unsubscribe-file
[wsp {:keys [subs-id] :as params}]
(l/trace :fn "handle-message" :event :unsubscribe-file :subs-id subs-id)
(let [msgbus-fn (:msgbus @wsp)
[cfg wsp {:keys [file-id] :as params}]
(let [msgbus (:msgbus cfg)
conn-id (::ws/id @wsp)
session-id (::session-id @wsp)
profile-id (::profile-id @wsp)]
profile-id (::profile-id @wsp)
subs (::file-subscription @wsp)
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-let [{:keys [file-id channel]} (get-in @wsp [::subscriptions subs-id])]
(let [message {:type :leave-file
:file-id file-id
:session-id session-id
:profile-id profile-id}]
(when (= (:file-id subs) file-id)
(let [channel (:channel subs)]
(a/close! channel)
(a/<! (msgbus-fn :cmd :pub :topic file-id :message message))
(a/<! (msgbus-fn :cmd :purge :chans [channel])))))))
(a/<! (mbus/purge! msgbus channel))
(a/<! (mbus/pub! msgbus :topic file-id :message message)))))))
(defmethod handle-message :keepalive
[_ _]
[_ _ _]
(l/trace :fn "handle-message" :event :keepalive)
(a/go :nothing))
(defmethod handle-message :pointer-update
[wsp {:keys [subs-id] :as message}]
(a/go
;; Only allow receive pointer updates when active subscription
(when-let [{:keys [topic]} (get-in @wsp [::subscriptions subs-id])]
(let [msgbus-fn (:msgbus @wsp)
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
message (-> message
(dissoc :subs-id)
(assoc :profile-id profile-id)
(assoc :session-id session-id))]
(a/<! (msgbus-fn :cmd :pub
:topic topic
:message message))))))
[cfg wsp {:keys [file-id] :as message}]
(let [msgbus (: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))))))
(defmethod handle-message :default
[_ message]
(a/go
(l/log :level :warn
:msg "received unexpected message"
:message message)))
[_ wsp message]
(let [conn-id (::ws/id @wsp)]
(l/warn :hint "received unexpected message"
:message message
:conn-id conn-id)
(a/go :none)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::msgbus fn?)
(s/def ::msgbus ::mbus/msgbus)
(s/def ::session-id ::us/uuid)
(s/def ::handler-params
@@ -201,12 +325,7 @@
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [profile-id params] :as req} respond raise]
(let [{:keys [session-id]} (us/conform ::handler-params params)
cfg (-> cfg
(assoc ::profile-id profile-id)
(assoc ::session-id session-id))]
(l/trace :hint "http request to websocket" :profile-id profile-id :session-id session-id)
(let [{:keys [session-id]} (us/conform ::handler-params params)]
(cond
(not profile-id)
(raise (ex/error :type :authentication
@@ -218,6 +337,15 @@
:hint "this endpoint only accepts websocket connections"))
:else
(->> (ws/handler handle-message cfg)
(yws/upgrade req)
(respond))))))
(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)
::ws/on-connect (partial on-connect cfg)
::ws/handler (partial handle-message cfg)
::profile-id profile-id
::session-id session-id)
(yws/upgrade req)
(respond)))))))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.loggers.audit
"Services related to the user activity (audit log)."
@@ -15,6 +15,7 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.tokens :as tokens]
[app.util.async :as aa]
[app.util.time :as dt]
[app.worker :as wrk]
@@ -32,7 +33,7 @@
[request]
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
(yrq/get-header request "x-real-ip")
(yrq/remote-addr request)))
(some-> (yrq/remote-addr request) str)))
(defn extract-utm-params
"Extracts additional data from params and namespace them under
@@ -51,7 +52,7 @@
(defn profile->props
[profile]
(-> profile
(select-keys [:is-active :is-muted :auth-backend :email :default-team-id :default-project-id :fullname :lang])
(select-keys [:id :is-active :is-muted :auth-backend :email :default-team-id :default-project-id :fullname :lang])
(merge (:props profile))
(d/without-nils)))
@@ -237,10 +238,10 @@
(s/def ::http-client fn?)
(s/def ::uri ::us/string)
(s/def ::tokens fn?)
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::archive-task [_]
(s/keys :req-un [::db/pool ::tokens ::http-client]
(s/keys :req-un [::db/pool ::sprops ::http-client]
:opt-un [::uri]))
(defmethod ig/init-key ::archive-task
@@ -257,12 +258,16 @@
(ex/raise :type :internal
:code :task-not-configured
:hint "archive task not configured, missing uri"))
(when enabled
(loop []
(let [res (archive-events cfg)]
(when (= res :continue)
(aa/thread-sleep 200)
(recur))))))))
(loop [total 0]
(let [n (archive-events cfg)]
(if n
(do
(aa/thread-sleep 100)
(recur (+ total n)))
(when (pos? total)
(l/trace :hint "events chunk archived" :num total)))))))))
(def sql:retrieve-batch-of-audit-log
"select * from audit_log
@@ -272,7 +277,7 @@
for update skip locked;")
(defn archive-events
[{:keys [pool uri tokens http-client] :as cfg}]
[{:keys [pool uri sprops http-client] :as cfg}]
(letfn [(decode-row [{:keys [props ip-addr context] :as row}]
(cond-> row
(db/pgobject? props)
@@ -296,9 +301,9 @@
:context]))
(send [events]
(let [token (tokens :generate {:iss "authentication"
:iat (dt/now)
:uid uuid/zero})
(let [token (tokens/generate sprops {:iss "authentication"
:iat (dt/now)
:uid uuid/zero})
body (t/encode {:events events})
headers {"content-type" "application/transit+json"
"origin" (cf/get :public-uri)
@@ -332,7 +337,7 @@
(l/debug :action "archive-events" :uri uri :events (count events))
(when (send events)
(mark-as-archived conn rows)
:continue))))))
(count events)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GC Task
@@ -340,23 +345,18 @@
(def sql:clean-archived
"delete from audit_log
where archived_at is not null
and archived_at < now() - ?::interval")
where archived_at is not null")
(defn- clean-archived
[{:keys [pool max-age]}]
(let [interval (db/interval max-age)
result (db/exec-one! pool [sql:clean-archived interval])
result (:next.jdbc/update-count result)]
(l/debug :action "clean archived audit log" :removed result)
[{:keys [pool]}]
(let [result (db/exec-one! pool [sql:clean-archived])
result (:next.jdbc/update-count result)]
(l/debug :hint "delete archived audit log entries" :deleted result)
result))
(s/def ::max-age ::cf/audit-log-gc-max-age)
(defmethod ig/pre-init-spec ::gc-task [_]
(s/keys :req-un [::db/pool ::max-age]))
(s/keys :req-un [::db/pool]))
(defmethod ig/init-key ::gc-task
[_ cfg]
(fn [_]
(clean-archived cfg)))
(partial clean-archived cfg))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.loggers.database
"A specific logger impl that persists errors on the database."
@@ -46,6 +46,7 @@
(defn parse-event
[event]
(-> (parse-event-data event)
(assoc :hint (or (:hint event) (:message event)))
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (cf/get :public-uri))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.loggers.loki
"A Loki integration."

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.loggers.mattermost
"A mattermost integration for error reporting."

View File

@@ -1,170 +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) UXBOX Labs SL
(ns app.loggers.sentry
"A mattermost integration for error reporting."
(:require
[app.common.logging :as l]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.util.async :as aa]
[app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig])
(:import
io.sentry.Scope
io.sentry.IHub
io.sentry.Hub
io.sentry.NoOpHub
io.sentry.protocol.User
io.sentry.SentryOptions
io.sentry.SentryLevel
io.sentry.ScopeCallback))
(defonce enabled (atom true))
(defn- parse-context
[event]
(reduce-kv
(fn [acc k v]
(cond
(= k :id) (assoc acc k (uuid/uuid v))
(= k :profile-id) (assoc acc k (uuid/uuid v))
(str/blank? v) acc
:else (assoc acc k v)))
{}
(:context event)))
(defn- parse-event
[event]
(assoc event :context (parse-context event)))
(defn- build-sentry-options
[cfg]
(let [version (:base cf/version)]
(doto (SentryOptions.)
(.setDebug (:debug cfg false))
(.setTracesSampleRate (:traces-sample-rate cfg 1.0))
(.setDsn (:dsn cfg))
(.setServerName (cf/get :host))
(.setEnvironment (cf/get :tenant))
(.setAttachServerName true)
(.setAttachStacktrace (:attach-stack-trace cfg false))
(.setRelease (str "backend@" (if (= version "0.0.0") "develop" version))))))
(defn handle-event
[^IHub shub event]
(letfn [(set-user! [^Scope scope {:keys [context] :as event}]
(let [user (User.)]
(.setIpAddress ^User user ^String (:ip-addr context))
(when-let [pid (:profile-id context)]
(.setId ^User user ^String (str pid)))
(.setUser scope ^User user)))
(set-level! [^Scope scope]
(.setLevel scope SentryLevel/ERROR))
(set-context! [^Scope scope {:keys [context] :as event}]
(let [uri (str (cf/get :public-uri) "/dbg/error-by-id/" (:id context))]
(.setContexts scope "detailed_error_uri" ^String uri))
(when-let [vers (:frontend-version event)]
(.setContexts scope "frontend_version" ^String vers))
(when-let [puri (:public-uri event)]
(.setContexts scope "public_uri" ^String (str puri)))
(when-let [uagent (:user-agent context)]
(.setContexts scope "user_agent" ^String uagent))
(when-let [tenant (:tenant event)]
(.setTag scope "tenant" ^String tenant))
(when-let [type (:error-type context)]
(.setTag scope "error_type" ^String (str type)))
(when-let [code (:error-code context)]
(.setTag scope "error_code" ^String (str code)))
)
(capture [^Scope scope {:keys [context error] :as event}]
(let [msg (str (:message error) "\n\n"
"======================================================\n"
"=================== Params ===========================\n"
"======================================================\n"
(:params context) "\n"
(when (:explain context)
(str "======================================================\n"
"=================== Explain ==========================\n"
"======================================================\n"
(:explain context) "\n"))
(when (:data context)
(str "======================================================\n"
"=================== Error Data =======================\n"
"======================================================\n"
(:data context) "\n"))
(str "======================================================\n"
"=================== Stack Trace ======================\n"
"======================================================\n"
(:trace error))
"\n")]
(set-user! scope event)
(set-level! scope)
(set-context! scope event)
(.captureMessage ^IHub shub msg)
))
]
(when @enabled
(.withScope ^IHub shub (reify ScopeCallback
(run [_ scope]
(->> event
(parse-event)
(capture scope))))))
))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Error Listener
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::receiver any?)
(s/def ::dsn ::cf/sentry-dsn)
(s/def ::trace-sample-rate ::cf/sentry-trace-sample-rate)
(s/def ::attach-stack-trace ::cf/sentry-attach-stack-trace)
(s/def ::debug ::cf/sentry-debug)
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
:opt-un [::dsn ::trace-sample-rate ::attach-stack-trace]))
(defmethod ig/init-key ::reporter
[_ {:keys [receiver dsn executor] :as cfg}]
(l/info :msg "initializing sentry reporter" :dsn dsn)
(let [opts (build-sentry-options cfg)
shub (if dsn
(Hub. ^SentryOptions opts)
(NoOpHub/getInstance))
output (a/chan (a/sliding-buffer 128)
(filter #(= (:level %) "error")))]
(receiver :sub output)
(a/go-loop []
(let [event (a/<! output)]
(if (nil? event)
(do
(l/info :msg "stoping error reporting loop")
(.close ^IHub shub))
(do
(a/<! (aa/with-thread executor (handle-event shub event)))
(recur)))))
output))
(defmethod ig/halt-key! ::reporter
[_ output]
(when output
(a/close! output)))

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.loggers.zmq
"A generic ZMQ listener."

View File

@@ -2,13 +2,15 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.main
(:require
[app.auth.oidc]
[app.common.logging :as l]
[app.config :as cf]
[app.util.time :as dt]
[cuerdas.core :as str]
[integrant.core :as ig])
(:gen-class))
@@ -22,36 +24,26 @@
:migrations (ig/ref :app.migrations/all)
:name :main
:min-size (cf/get :database-min-pool-size 0)
:max-size (cf/get :database-max-pool-size 30)}
:max-size (cf/get :database-max-pool-size 60)}
;; Default thread pool for IO operations
[::default :app.worker/executor]
{:parallelism (cf/get :default-executor-parallelism 60)
:prefix :default}
;; Constrained thread pool. Should only be used from high resources
;; demanding operations.
[::blocking :app.worker/executor]
{:parallelism (cf/get :blocking-executor-parallelism 10)
:prefix :blocking}
{:parallelism (cf/get :default-executor-parallelism 70)}
;; Dedicated thread pool for backround tasks execution.
[::worker :app.worker/executor]
{:parallelism (cf/get :worker-executor-parallelism 10)
:prefix :worker}
{:parallelism (cf/get :worker-executor-parallelism 20)}
:app.worker/scheduler
{:parallelism 1
:prefix :scheduler}
:app.worker/executors
{:default (ig/ref [::default :app.worker/executor])
:worker (ig/ref [::worker :app.worker/executor])
:blocking (ig/ref [::blocking :app.worker/executor])}
{:default (ig/ref [::default :app.worker/executor])
:worker (ig/ref [::worker :app.worker/executor])}
:app.worker/executors-monitor
:app.worker/executor-monitor
{:metrics (ig/ref :app.metrics/metrics)
:scheduler (ig/ref :app.worker/scheduler)
:executors (ig/ref :app.worker/executors)}
:app.migrations/migrations
@@ -63,19 +55,23 @@
:app.migrations/all
{:main (ig/ref :app.migrations/migrations)}
:app.redis/redis
{:uri (cf/get :redis-uri)
:metrics (ig/ref :app.metrics/metrics)}
:app.msgbus/msgbus
{:backend (cf/get :msgbus-backend :redis)
:executor (ig/ref [::default :app.worker/executor])
:redis-uri (cf/get :redis-uri)}
:redis (ig/ref :app.redis/redis)}
:app.tokens/tokens
{:keys (ig/ref :app.setup/keys)}
:app.storage.tmp/cleaner
{:executor (ig/ref [::worker :app.worker/executor])
:scheduler (ig/ref :app.worker/scheduler)}
:app.storage/gc-deleted-task
{:pool (ig/ref :app.db/pool)
:storage (ig/ref :app.storage/storage)
:executor (ig/ref [::worker :app.worker/executor])
:min-age (dt/duration {:hours 2})}
:executor (ig/ref [::worker :app.worker/executor])}
:app.storage/gc-touched-task
{:pool (ig/ref :app.db/pool)}
@@ -88,23 +84,15 @@
:app.http.session/store
{:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens)
:sprops (ig/ref :app.setup/props)
:executor (ig/ref [::default :app.worker/executor])}
:app.http.session/gc-task
{:pool (ig/ref :app.db/pool)
:max-age (cf/get :http-session-idle-max-age)}
:app.http.session/updater
{:pool (ig/ref :app.db/pool)
:metrics (ig/ref :app.metrics/metrics)
:executor (ig/ref [::worker :app.worker/executor])
:session (ig/ref :app.http/session)
:max-batch-age (cf/get :http-session-updater-batch-max-age)
:max-batch-size (cf/get :http-session-updater-batch-max-size)}
:max-age (cf/get :auth-token-cookie-max-age)}
:app.http.awsns/handler
{:tokens (ig/ref :app.tokens/tokens)
{:sprops (ig/ref :app.setup/props)
:pool (ig/ref :app.db/pool)
:http-client (ig/ref :app.http/client)
:executor (ig/ref [::worker :app.worker/executor])}
@@ -119,25 +107,88 @@
:max-body-size (cf/get :http-server-max-body-size)
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
:app.auth.ldap/provider
{:host (cf/get :ldap-host)
:port (cf/get :ldap-port)
:ssl (cf/get :ldap-ssl)
:tls (cf/get :ldap-starttls)
:query (cf/get :ldap-user-query)
:attrs-email (cf/get :ldap-attrs-email)
:attrs-fullname (cf/get :ldap-attrs-fullname)
:attrs-username (cf/get :ldap-attrs-username)
:base-dn (cf/get :ldap-base-dn)
:bind-dn (cf/get :ldap-bind-dn)
:bind-password (cf/get :ldap-bind-password)
:enabled? (contains? cf/flags :login-with-ldap)}
:app.auth.oidc/google-provider
{:enabled? (contains? cf/flags :login-with-google)
:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret)}
:app.auth.oidc/github-provider
{:enabled? (contains? cf/flags :login-with-github)
:http-client (ig/ref :app.http/client)
:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)}
:app.auth.oidc/gitlab-provider
{:enabled? (contains? cf/flags :login-with-gitlab)
:base-uri (cf/get :gitlab-base-uri "https://gitlab.com")
:client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret)}
:app.auth.oidc/generic-provider
{:enabled? (contains? cf/flags :login-with-oidc)
:http-client (ig/ref :app.http/client)
:client-id (cf/get :oidc-client-id)
:client-secret (cf/get :oidc-client-secret)
:base-uri (cf/get :oidc-base-uri)
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:scopes (cf/get :oidc-scopes)
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)}
:app.auth.oidc/routes
{:providers {:google (ig/ref :app.auth.oidc/google-provider)
:github (ig/ref :app.auth.oidc/github-provider)
:gitlab (ig/ref :app.auth.oidc/gitlab-provider)
:oidc (ig/ref :app.auth.oidc/generic-provider)}
:sprops (ig/ref :app.setup/props)
:http-client (ig/ref :app.http/client)
:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http/session)
:public-uri (cf/get :public-uri)
:executor (ig/ref [::default :app.worker/executor])}
;; TODO: revisit the dependencies of this service, looks they are too much unused of them
:app.http/router
{:assets (ig/ref :app.http.assets/handlers)
:feedback (ig/ref :app.http.feedback/handler)
:session (ig/ref :app.http/session)
:awsns-handler (ig/ref :app.http.awsns/handler)
:oauth (ig/ref :app.http.oauth/handler)
:debug (ig/ref :app.http.debug/handlers)
:debug-routes (ig/ref :app.http.debug/routes)
:oidc-routes (ig/ref :app.auth.oidc/routes)
:ws (ig/ref :app.http.websocket/handler)
:metrics (ig/ref :app.metrics/metrics)
:public-uri (cf/get :public-uri)
:storage (ig/ref :app.storage/storage)
:tokens (ig/ref :app.tokens/tokens)
:audit-handler (ig/ref :app.loggers.audit/http-handler)
:rpc (ig/ref :app.rpc/rpc)
:rpc-routes (ig/ref :app.rpc/routes)
:doc-routes (ig/ref :app.rpc.doc/routes)
:executor (ig/ref [::default :app.worker/executor])}
:app.http.debug/handlers
{:pool (ig/ref :app.db/pool)
:executor (ig/ref [::worker :app.worker/executor])}
:app.http.debug/routes
{:pool (ig/ref :app.db/pool)
:executor (ig/ref [::worker :app.worker/executor])
:storage (ig/ref :app.storage/storage)
:session (ig/ref :app.http/session)}
:app.http.websocket/handler
{:pool (ig/ref :app.db/pool)
@@ -156,125 +207,82 @@
{:pool (ig/ref :app.db/pool)
:executor (ig/ref [::default :app.worker/executor])}
:app.http.oauth/handler
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http/session)
:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens)
:audit (ig/ref :app.loggers.audit/collector)
:executor (ig/ref [::default :app.worker/executor])
:http-client (ig/ref :app.http/client)
:public-uri (cf/get :public-uri)}
:app.rpc/semaphores
{:metrics (ig/ref :app.metrics/metrics)
:executor (ig/ref [::default :app.worker/executor])}
:app.rpc/rpc
:app.rpc/rlimit
{:executor (ig/ref [::worker :app.worker/executor])
:scheduler (ig/ref :app.worker/scheduler)}
:app.rpc/methods
{:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http/session)
:tokens (ig/ref :app.tokens/tokens)
:sprops (ig/ref :app.setup/props)
:metrics (ig/ref :app.metrics/metrics)
:storage (ig/ref :app.storage/storage)
:msgbus (ig/ref :app.msgbus/msgbus)
:public-uri (cf/get :public-uri)
:redis (ig/ref :app.redis/redis)
:audit (ig/ref :app.loggers.audit/collector)
:ldap (ig/ref :app.auth.ldap/provider)
:http-client (ig/ref :app.http/client)
:executors (ig/ref :app.worker/executors)}
:rlimit (ig/ref :app.rpc/rlimit)
:executors (ig/ref :app.worker/executors)
:executor (ig/ref [::default :app.worker/executor])
:templates (ig/ref :app.setup/builtin-templates)
:semaphores (ig/ref :app.rpc/semaphores)
}
:app.worker/worker
{:executor (ig/ref [::worker :app.worker/executor])
:tasks (ig/ref :app.worker/registry)
:metrics (ig/ref :app.metrics/metrics)
:pool (ig/ref :app.db/pool)}
:app.rpc.doc/routes
{:methods (ig/ref :app.rpc/methods)}
:app.worker/cron
{:executor (ig/ref [::worker :app.worker/executor])
:scheduler (ig/ref :app.worker/scheduler)
:tasks (ig/ref :app.worker/registry)
:pool (ig/ref :app.db/pool)
:entries
[{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :file-gc}
{:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-xlog-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-deleted-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-touched-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :session-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc}
{:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry}
(when (cf/get :fdata-storage-backed)
{:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-offload})
(when (contains? cf/flags :audit-log-archive)
{:cron #app/cron "0 */5 * * * ?" ;; every 5m
:task :audit-log-archive})
(when (contains? cf/flags :audit-log-gc)
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :audit-log-gc})]}
:app.rpc/routes
{:methods (ig/ref :app.rpc/methods)}
:app.worker/registry
{:metrics (ig/ref :app.metrics/metrics)
:tasks
{:sendmail (ig/ref :app.emails/sendmail-handler)
{:sendmail (ig/ref :app.emails/handler)
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
:file-gc (ig/ref :app.tasks.file-gc/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:storage-deleted-gc (ig/ref :app.storage/gc-deleted-task)
:storage-touched-gc (ig/ref :app.storage/gc-touched-task)
:storage-gc-deleted (ig/ref :app.storage/gc-deleted-task)
:storage-gc-touched (ig/ref :app.storage/gc-touched-task)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:session-gc (ig/ref :app.http.session/gc-task)
:file-offload (ig/ref :app.tasks.file-offload/handler)
:audit-log-archive (ig/ref :app.loggers.audit/archive-task)
:audit-log-gc (ig/ref :app.loggers.audit/gc-task)}}
:app.emails/sendmail-handler
:app.emails/sendmail
{:host (cf/get :smtp-host)
:port (cf/get :smtp-port)
:ssl (cf/get :smtp-ssl)
:tls (cf/get :smtp-tls)
:username (cf/get :smtp-username)
:password (cf/get :smtp-password)
:metrics (ig/ref :app.metrics/metrics)
:default-reply-to (cf/get :smtp-default-reply-to)
:default-from (cf/get :smtp-default-from)}
:app.emails/handler
{:sendmail (ig/ref :app.emails/sendmail)
:metrics (ig/ref :app.metrics/metrics)}
:app.tasks.tasks-gc/handler
{:pool (ig/ref :app.db/pool)
:max-age cf/deletion-delay}
:app.tasks.objects-gc/handler
{:pool (ig/ref :app.db/pool)
:storage (ig/ref :app.storage/storage)
:max-age cf/deletion-delay}
:storage (ig/ref :app.storage/storage)}
:app.tasks.file-gc/handler
{:pool (ig/ref :app.db/pool)
:max-age cf/deletion-delay}
{:pool (ig/ref :app.db/pool)}
:app.tasks.file-xlog-gc/handler
{:pool (ig/ref :app.db/pool)
:max-age (dt/duration {:hours 72})}
:app.tasks.file-offload/handler
{:pool (ig/ref :app.db/pool)
:max-age (dt/duration {:seconds 5})
:storage (ig/ref :app.storage/storage)
:backend (cf/get :fdata-storage-backed :fdata-s3)}
{:pool (ig/ref :app.db/pool)}
:app.tasks.telemetry/handler
{:pool (ig/ref :app.db/pool)
@@ -287,13 +295,13 @@
{:port (cf/get :srepl-port)
:host (cf/get :srepl-host)}
:app.setup/builtin-templates
{:http-client (ig/ref :app.http/client)}
:app.setup/props
{:pool (ig/ref :app.db/pool)
:key (cf/get :secret-key)}
:app.setup/keys
{:props (ig/ref :app.setup/props)}
:app.loggers.zmq/receiver
{:endpoint (cf/get :loggers-zmq-uri)}
@@ -307,13 +315,12 @@
:app.loggers.audit/archive-task
{:uri (cf/get :audit-log-archive-uri)
:tokens (ig/ref :app.tokens/tokens)
:sprops (ig/ref :app.setup/props)
:pool (ig/ref :app.db/pool)
:http-client (ig/ref :app.http/client)}
:app.loggers.audit/gc-task
{:max-age (cf/get :audit-log-gc-max-age cf/deletion-delay)
:pool (ig/ref :app.db/pool)}
{:pool (ig/ref :app.db/pool)}
:app.loggers.loki/reporter
{:uri (cf/get :loggers-loki-uri)
@@ -336,23 +343,12 @@
:backends
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
:assets-db (ig/ref [::assets :app.storage.db/backend])
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
:tmp (ig/ref [::tmp :app.storage.fs/backend])
:fdata-s3 (ig/ref [::fdata :app.storage.s3/backend])
;; keep this for backward compatibility
:s3 (ig/ref [::assets :app.storage.s3/backend])
:fs (ig/ref [::assets :app.storage.fs/backend])}}
[::fdata :app.storage.s3/backend]
{:region (cf/get :storage-fdata-s3-region)
:bucket (cf/get :storage-fdata-s3-bucket)
:endpoint (cf/get :storage-fdata-s3-endpoint)
:prefix (cf/get :storage-fdata-s3-prefix)
:executor (ig/ref [::default :app.worker/executor])}
[::assets :app.storage.s3/backend]
{:region (cf/get :storage-assets-s3-region)
:endpoint (cf/get :storage-assets-s3-endpoint)
@@ -361,24 +357,69 @@
[::assets :app.storage.fs/backend]
{:directory (cf/get :storage-assets-fs-directory)}
})
[::tmp :app.storage.fs/backend]
{:directory "/tmp/penpot"}
[::assets :app.storage.db/backend]
{:pool (ig/ref :app.db/pool)}})
(def worker-config
{:app.worker/cron
{:executor (ig/ref [::worker :app.worker/executor])
:scheduler (ig/ref :app.worker/scheduler)
:tasks (ig/ref :app.worker/registry)
:pool (ig/ref :app.db/pool)
:entries
[{:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-xlog-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :session-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-gc-deleted}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-gc-touched}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc}
{:cron #app/cron "0 0 2 * * ?" ;; daily
:task :file-gc}
{:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry}
(when (contains? cf/flags :audit-log-archive)
{:cron #app/cron "0 */5 * * * ?" ;; every 5m
:task :audit-log-archive})
(when (contains? cf/flags :audit-log-gc)
{:cron #app/cron "30 */5 * * * ?" ;; every 5m
:task :audit-log-gc})]}
:app.worker/worker
{:executor (ig/ref [::worker :app.worker/executor])
:tasks (ig/ref :app.worker/registry)
:metrics (ig/ref :app.metrics/metrics)
:pool (ig/ref :app.db/pool)}})
(def system nil)
(defn start
[]
(ig/load-namespaces system-config)
(ig/load-namespaces (merge system-config worker-config))
(alter-var-root #'system (fn [sys]
(when sys (ig/halt! sys))
(-> system-config
(cond-> (contains? cf/flags :backend-worker)
(merge worker-config))
(ig/prep)
(ig/init))))
(l/info :msg "welcome to penpot"
:flags (str/join "," (map name cf/flags))
:worker? (contains? cf/flags :backend-worker)
:version (:full cf/version)))
(defn stop

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.media
"Media & Font postprocessing."
@@ -12,18 +12,16 @@
[app.common.media :as cm]
[app.common.spec :as us]
[app.config :as cf]
[app.storage.tmp :as tmp]
[app.util.svg :as svg]
[buddy.core.bytes :as bb]
[buddy.core.codecs :as bc]
[clojure.java.io :as io]
[clojure.java.shell :as sh]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs])
[datoteka.fs :as fs]
[datoteka.io :as io])
(:import
java.io.ByteArrayInputStream
java.io.OutputStream
org.apache.commons.io.IOUtils
org.im4java.core.ConvertCmd
org.im4java.core.IMOperation
org.im4java.core.Info))
@@ -93,18 +91,16 @@
(let [{:keys [path mtype]} input
format (or (cm/mtype->format mtype) format)
ext (cm/format->extension format)
tmp (fs/create-tempfile :suffix ext)]
tmp (tmp/tempfile :prefix "penpot.media." :suffix ext)]
(doto (ConvertCmd.)
(.run operation (into-array (map str [path tmp]))))
(let [thumbnail-data (fs/slurp-bytes tmp)]
(fs/delete tmp)
(assoc params
:format format
:mtype (cm/format->mtype format)
:size (alength ^bytes thumbnail-data)
:data (ByteArrayInputStream. thumbnail-data)))))
(assoc params
:format format
:mtype (cm/format->mtype format)
:size (fs/size tmp)
:data tmp)))
(defmethod process :generic-thumbnail
[{:keys [quality width height] :as params}]
@@ -201,59 +197,43 @@
(defmethod process :generate-fonts
[{:keys [input] :as params}]
(letfn [(ttf->otf [data]
(let [input-file (fs/create-tempfile :prefix "penpot")
output-file (fs/path (str input-file ".otf"))
_ (with-open [out (io/output-stream input-file)]
(IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "fontforge" "-lang=ff" "-c"
(str/fmt "Open('%s'); Generate('%s')"
(str input-file)
(str output-file)))]
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix "")
foutput (fs/path (str finput ".otf"))
_ (io/write-to-file! data finput)
res (sh/sh "fontforge" "-lang=ff" "-c"
(str/fmt "Open('%s'); Generate('%s')"
(str finput)
(str foutput)))]
(when (zero? (:exit res))
(fs/slurp-bytes output-file))))
foutput)))
(otf->ttf [data]
(let [input-file (fs/create-tempfile :prefix "penpot")
output-file (fs/path (str input-file ".ttf"))
_ (with-open [out (io/output-stream input-file)]
(IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "fontforge" "-lang=ff" "-c"
(str/fmt "Open('%s'); Generate('%s')"
(str input-file)
(str output-file)))]
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix "")
foutput (fs/path (str finput ".ttf"))
_ (io/write-to-file! data finput)
res (sh/sh "fontforge" "-lang=ff" "-c"
(str/fmt "Open('%s'); Generate('%s')"
(str finput)
(str foutput)))]
(when (zero? (:exit res))
(fs/slurp-bytes output-file))))
foutput)))
(ttf-or-otf->woff [data]
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
output-file (fs/path (str input-file ".woff"))
_ (with-open [out (io/output-stream input-file)]
(IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "sfnt2woff" (str input-file))]
;; NOTE: foutput is not used directly, it represents the
;; default output of the exection of the underlying
;; command.
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix "")
foutput (fs/path (str finput ".woff"))
_ (io/write-to-file! data finput)
res (sh/sh "sfnt2woff" (str finput))]
(when (zero? (:exit res))
(fs/slurp-bytes output-file))))
(ttf-or-otf->woff2 [data]
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
output-file (fs/path (str input-file ".woff2"))
_ (with-open [out (io/output-stream input-file)]
(IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "woff2_compress" (str input-file))]
(when (zero? (:exit res))
(fs/slurp-bytes output-file))))
foutput)))
(woff->sfnt [data]
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
_ (with-open [out (io/output-stream input-file)]
(IOUtils/writeChunked ^bytes data ^OutputStream out)
(.flush ^OutputStream out))
res (sh/sh "woff2sfnt" (str input-file)
:out-enc :bytes)]
(let [finput (tmp/tempfile :prefix "penpot" :suffix "")
_ (io/write-to-file! data finput)
res (sh/sh "woff2sfnt" (str finput)
:out-enc :bytes)]
(when (zero? (:exit res))
(:out res))))
@@ -280,15 +260,13 @@
(let [data (get input "font/ttf")]
(-> input
(update "font/otf" gen-if-nil #(ttf->otf data))
(update "font/woff" gen-if-nil #(ttf-or-otf->woff data))
(assoc "font/woff2" (ttf-or-otf->woff2 data))))
(update "font/woff" gen-if-nil #(ttf-or-otf->woff data))))
(contains? current "font/otf")
(let [data (get input "font/otf")]
(-> input
(update "font/woff" gen-if-nil #(ttf-or-otf->woff data))
(assoc "font/ttf" (otf->ttf data))
(assoc "font/woff2" (ttf-or-otf->woff2 data))))
(assoc "font/ttf" (otf->ttf data))))
(contains? current "font/woff")
(let [data (get input "font/woff")
@@ -300,8 +278,7 @@
(let [stype (get-sfnt-type sfnt)]
(cond-> input
true
(-> (assoc "font/woff" data)
(assoc "font/woff2" (ttf-or-otf->woff2 sfnt)))
(-> (assoc "font/woff" data))
(= stype :otf)
(-> (assoc "font/otf" sfnt)

View File

@@ -2,12 +2,14 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.metrics
(:refer-clojure :exclude [run!])
(:require
[app.common.logging :as l]
[app.common.spec :as us]
[app.metrics.definition :as-alias mdef]
[clojure.spec.alpha :as s]
[integrant.core :as ig])
(:import
@@ -16,11 +18,12 @@
io.prometheus.client.Counter$Child
io.prometheus.client.Gauge
io.prometheus.client.Gauge$Child
io.prometheus.client.Summary
io.prometheus.client.Summary$Child
io.prometheus.client.Summary$Builder
io.prometheus.client.Histogram
io.prometheus.client.Histogram$Child
io.prometheus.client.SimpleCollector
io.prometheus.client.Summary
io.prometheus.client.Summary$Builder
io.prometheus.client.Summary$Child
io.prometheus.client.exporter.common.TextFormat
io.prometheus.client.hotspot.DefaultExports
java.io.StringWriter))
@@ -28,7 +31,7 @@
(set! *warn-on-reflection* true)
(declare create-registry)
(declare create)
(declare create-collector)
(declare handler)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -37,114 +40,151 @@
(def default-metrics
{:update-file-changes
{:name "rpc_update_file_changes_total"
:help "A total number of changes submitted to update-file."
:type :counter}
{::mdef/name "penpot_rpc_update_file_changes_total"
::mdef/help "A total number of changes submitted to update-file."
::mdef/type :counter}
:update-file-bytes-processed
{:name "rpc_update_file_bytes_processed_total"
:help "A total number of bytes processed by update-file."
:type :counter}
{::mdef/name "penpot_rpc_update_file_bytes_processed_total"
::mdef/help "A total number of bytes processed by update-file."
::mdef/type :counter}
:rpc-mutation-timing
{:name "rpc_mutation_timing"
:help "RPC mutation method call timming."
:labels ["name"]
:type :histogram}
{::mdef/name "penpot_rpc_mutation_timing"
::mdef/help "RPC mutation method call timming."
::mdef/labels ["name"]
::mdef/type :histogram}
:rpc-command-timing
{::mdef/name "penpot_rpc_command_timing"
::mdef/help "RPC command method call timming."
::mdef/labels ["name"]
::mdef/type :histogram}
:rpc-query-timing
{:name "rpc_query_timing"
:help "RPC query method call timing."
:labels ["name"]
:type :histogram}
{::mdef/name "penpot_rpc_query_timing"
::mdef/help "RPC query method call timing."
::mdef/labels ["name"]
::mdef/type :histogram}
:websocket-active-connections
{:name "websocket_active_connections"
:help "Active websocket connections gauge"
:type :gauge}
{::mdef/name "penpot_websocket_active_connections"
::mdef/help "Active websocket connections gauge"
::mdef/type :gauge}
:websocket-messages-total
{:name "websocket_message_total"
:help "Counter of processed messages."
:labels ["op"]
:type :counter}
{::mdef/name "penpot_websocket_message_total"
::mdef/help "Counter of processed messages."
::mdef/labels ["op"]
::mdef/type :counter}
:websocket-session-timing
{:name "websocket_session_timing"
:help "Websocket session timing (seconds)."
:type :summary}
{::mdef/name "penpot_websocket_session_timing"
::mdef/help "Websocket session timing (seconds)."
::mdef/type :summary}
:session-update-total
{:name "http_session_update_total"
:help "A counter of session update batch events."
:type :counter}
{::mdef/name "penpot_http_session_update_total"
::mdef/help "A counter of session update batch events."
::mdef/type :counter}
:tasks-timing
{:name "penpot_tasks_timing"
:help "Background tasks timing (milliseconds)."
:labels ["name"]
:type :summary}
{::mdef/name "penpot_tasks_timing"
::mdef/help "Background tasks timing (milliseconds)."
::mdef/labels ["name"]
::mdef/type :summary}
:rlimit-queued-submissions
{:name "penpot_rlimit_queued_submissions"
:help "Current number of queued submissions on RLIMIT."
:labels ["name"]
:type :gauge}
:redis-eval-timing
{::mdef/name "penpot_redis_eval_timing"
::mdef/help "Redis EVAL commands execution timings (ms)"
::mdef/labels ["name"]
::mdef/type :summary}
:rlimit-used-permits
{:name "penpot_rlimit_used_permits"
:help "Current number of used permits on RLIMIT."
:labels ["name"]
:type :gauge}
:semaphore-queued-submissions
{::mdef/name "penpot_semaphore_queued_submissions"
::mdef/help "Current number of queued submissions on SEMAPHORE."
::mdef/labels ["name"]
::mdef/type :gauge}
:rlimit-acquires-total
{:name "penpot_rlimit_acquires_total"
:help "Total number of acquire operations on RLIMIT."
:labels ["name"]
:type :counter}
:semaphore-used-permits
{::mdef/name "penpot_semaphore_used_permits"
::mdef/help "Current number of used permits on SEMAPHORE."
::mdef/labels ["name"]
::mdef/type :gauge}
:semaphore-timing
{::mdef/name "penpot_semaphore_timing"
::mdef/help "Total timing of SEMAPHORE."
::mdef/labels ["name"]
::mdef/type :summary}
:executors-active-threads
{:name "penpot_executors_active_threads"
:help "Current number of threads available in the executor service."
:labels ["name"]
:type :gauge}
{::mdef/name "penpot_executors_active_threads"
::mdef/help "Current number of threads available in the executor service."
::mdef/labels ["name"]
::mdef/type :gauge}
:executors-completed-tasks
{:name "penpot_executors_completed_tasks_total"
:help "Aproximate number of completed tasks by the executor."
:labels ["name"]
:type :counter}
{::mdef/name "penpot_executors_completed_tasks_total"
::mdef/help "Aproximate number of completed tasks by the executor."
::mdef/labels ["name"]
::mdef/type :counter}
:executors-running-threads
{:name "penpot_executors_running_threads"
:help "Current number of threads with state RUNNING."
:labels ["name"]
:type :gauge}
{::mdef/name "penpot_executors_running_threads"
::mdef/help "Current number of threads with state RUNNING."
::mdef/labels ["name"]
::mdef/type :gauge}
:executors-queued-submissions
{:name "penpot_executors_queued_submissions"
:help "Current number of queued submissions."
:labels ["name"]
:type :gauge}})
{::mdef/name "penpot_executors_queued_submissions"
::mdef/help "Current number of queued submissions."
::mdef/labels ["name"]
::mdef/type :gauge}})
(s/def ::mdef/name string?)
(s/def ::mdef/help string?)
(s/def ::mdef/labels (s/every string? :kind vector?))
(s/def ::mdef/type #{:gauge :counter :summary :histogram})
(s/def ::mdef/instance
#(instance? SimpleCollector %))
(s/def ::mdef/definition
(s/keys :req [::mdef/name
::mdef/help
::mdef/type]
:opt [::mdef/labels
::mdef/instance]))
(s/def ::definitions
(s/map-of keyword? ::mdef/definition))
(s/def ::registry
#(instance? CollectorRegistry %))
(s/def ::handler fn?)
(s/def ::metrics
(s/keys :req [::registry
::handler
::definitions]))
(defmethod ig/init-key ::metrics
[_ _]
(l/info :action "initialize metrics")
(let [registry (create-registry)
definitions (reduce-kv (fn [res k v]
(->> (assoc v :registry registry)
(create)
(->> (assoc v ::registry registry)
(create-collector)
(assoc res k)))
{}
default-metrics)]
{:handler (partial handler registry)
:definitions definitions
:registry registry}))
(s/def ::handler fn?)
(s/def ::registry #(instance? CollectorRegistry %))
(s/def ::metrics
(s/keys :req-un [::registry ::handler]))
(us/verify! ::definitions definitions)
{::handler (partial handler registry)
::definitions definitions
::registry registry}))
(defn- handler
[registry _ respond _]
@@ -168,13 +208,16 @@
(def default-histogram-buckets
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
(defmulti run-collector! (fn [mdef _] (::mdef/type mdef)))
(defmulti create-collector ::mdef/type)
(defn run!
[{:keys [definitions]} {:keys [id] :as params}]
[{:keys [::definitions]} & {:keys [id] :as params}]
(when-let [mobj (get definitions id)]
((::fn mobj) params)
(run-collector! mobj params)
true))
(defn create-registry
(defn- create-registry
[]
(let [registry (CollectorRegistry.)]
(DefaultExports/register registry)
@@ -186,79 +229,89 @@
(and (.isArray ^Class oc)
(= (.getComponentType oc) String))))
(defn make-counter
[{:keys [name help registry reg labels] :as props}]
(defmethod run-collector! :counter
[{:keys [::mdef/instance]} {:keys [inc labels] :or {inc 1 labels default-empty-labels}}]
(let [instance (.labels instance (if (is-array? labels) labels (into-array String labels)))]
(.inc ^Counter$Child instance (double inc))))
(defmethod run-collector! :gauge
[{:keys [::mdef/instance]} {:keys [inc dec labels val] :or {labels default-empty-labels}}]
(let [instance (.labels ^Gauge instance (if (is-array? labels) labels (into-array String labels)))]
(cond (number? inc) (.inc ^Gauge$Child instance (double inc))
(number? dec) (.dec ^Gauge$Child instance (double dec))
(number? val) (.set ^Gauge$Child instance (double val)))))
(defmethod run-collector! :summary
[{:keys [::mdef/instance]} {:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Summary instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Summary$Child instance val)))
(defmethod run-collector! :histogram
[{:keys [::mdef/instance]} {:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Histogram instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Histogram$Child instance val)))
(defmethod create-collector :counter
[{::mdef/keys [name help reg labels]
::keys [registry]
:as props}]
(let [registry (or registry reg)
instance (.. (Counter/build)
(name name)
(help help))
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(help help))]
(when (seq labels)
(.labelNames instance (into-array String labels)))
{::instance instance
::fn (fn [{:keys [inc labels] :or {inc 1 labels default-empty-labels}}]
(let [instance (.labels instance (if (is-array? labels) labels (into-array String labels)))]
(.inc ^Counter$Child instance (double inc))))}))
(assoc props ::mdef/instance (.register instance registry))))
(defn make-gauge
[{:keys [name help registry reg labels] :as props}]
(defmethod create-collector :gauge
[{::mdef/keys [name help reg labels]
::keys [registry]
:as props}]
(let [registry (or registry reg)
instance (.. (Gauge/build)
(name name)
(help help))
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
{::instance instance
::fn (fn [{:keys [inc dec labels val] :or {labels default-empty-labels}}]
(let [instance (.labels ^Gauge instance (if (is-array? labels) labels (into-array String labels)))]
(cond (number? inc) (.inc ^Gauge$Child instance (double inc))
(number? dec) (.dec ^Gauge$Child instance (double dec))
(number? val) (.set ^Gauge$Child instance (double val)))))}))
(help help))]
(when (seq labels)
(.labelNames instance (into-array String labels)))
(defn make-summary
[{:keys [name help registry reg labels max-age quantiles buckets]
:or {max-age 3600 buckets 12 quantiles default-quantiles} :as props}]
(assoc props ::mdef/instance (.register instance registry))))
(defmethod create-collector :summary
[{::mdef/keys [name help reg labels max-age quantiles buckets]
::keys [registry]
:or {max-age 3600 buckets 12 quantiles default-quantiles}
:as props}]
(let [registry (or registry reg)
builder (doto (Summary/build)
(.name name)
(.help help))
_ (when (seq quantiles)
(.maxAgeSeconds ^Summary$Builder builder ^long max-age)
(.ageBuckets ^Summary$Builder builder buckets))
_ (doseq [[q e] quantiles]
(.quantile ^Summary$Builder builder q e))
_ (when (seq labels)
(.labelNames ^Summary$Builder builder (into-array String labels)))
instance (.register ^Summary$Builder builder registry)]
(.help help))]
{::instance instance
::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Summary instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Summary$Child instance val)))}))
(when (seq quantiles)
(.maxAgeSeconds ^Summary$Builder builder ^long max-age)
(.ageBuckets ^Summary$Builder builder buckets))
(defn make-histogram
[{:keys [name help registry reg labels buckets]
:or {buckets default-histogram-buckets}}]
(doseq [[q e] quantiles]
(.quantile ^Summary$Builder builder q e))
(when (seq labels)
(.labelNames ^Summary$Builder builder (into-array String labels)))
(assoc props ::mdef/instance (.register ^Summary$Builder builder registry))))
(defmethod create-collector :histogram
[{::mdef/keys [name help reg labels buckets]
::keys [registry]
:or {buckets default-histogram-buckets}
:as props}]
(let [registry (or registry reg)
instance (doto (Histogram/build)
(.name name)
(.help help)
(.buckets (into-array Double/TYPE buckets)))
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(.buckets (into-array Double/TYPE buckets)))]
{::instance instance
::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Histogram instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Histogram$Child instance val)))}))
(when (seq labels)
(.labelNames instance (into-array String labels)))
(defn create
[{:keys [type] :as props}]
(case type
:counter (make-counter props)
:gauge (make-gauge props)
:summary (make-summary props)
:histogram (make-histogram props)))
(assoc props ::mdef/instance (.register instance registry))))

View File

@@ -2,11 +2,11 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.migrations
(:require
[app.migrations.migration-0023 :as mg0023]
[app.migrations.clj.migration-0023 :as mg0023]
[app.util.migrations :as mg]
[integrant.core :as ig]))
@@ -226,6 +226,27 @@
{:name "0072-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0072-mod-file-object-thumbnail-table.sql")}
{:name "0073-mod-file-media-object-constraints"
:fn (mg/resource "app/migrations/sql/0073-mod-file-media-object-constraints.sql")}
{:name "0074-mod-file-library-rel-constraints"
:fn (mg/resource "app/migrations/sql/0074-mod-file-library-rel-constraints.sql")}
{:name "0075-mod-share-link-table"
:fn (mg/resource "app/migrations/sql/0075-mod-share-link-table.sql")}
{:name "0076-mod-storage-object-table"
:fn (mg/resource "app/migrations/sql/0076-mod-storage-object-table.sql")}
{:name "0077-mod-comment-thread-table"
:fn (mg/resource "app/migrations/sql/0077-mod-comment-thread-table.sql")}
{:name "0078-mod-file-media-object-table-drop-cascade"
:fn (mg/resource "app/migrations/sql/0078-mod-file-media-object-table-drop-cascade.sql")}
{:name "0079-mod-profile-table"
:fn (mg/resource "app/migrations/sql/0079-mod-profile-table.sql")}
])

View File

@@ -2,9 +2,9 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.migrations.migration-0023
(ns app.migrations.clj.migration-0023
(:require
[app.db :as db]
[app.util.blob :as blob]))

View File

@@ -0,0 +1,11 @@
ALTER TABLE file_media_object
ALTER CONSTRAINT file_media_object_media_id_fkey DEFERRABLE INITIALLY IMMEDIATE;
ALTER TABLE file_media_object
ALTER CONSTRAINT file_media_object_thumbnail_id_fkey DEFERRABLE INITIALLY IMMEDIATE;
ALTER TABLE file_media_object
RENAME CONSTRAINT media_object_file_id_fkey TO file_media_object_file_id_fkey;
ALTER TABLE file_media_object
ALTER CONSTRAINT file_media_object_file_id_fkey DEFERRABLE INITIALLY IMMEDIATE;

View File

@@ -0,0 +1,5 @@
ALTER TABLE file_library_rel
ALTER CONSTRAINT file_library_rel_file_id_fkey DEFERRABLE INITIALLY IMMEDIATE;
ALTER TABLE file_library_rel
ALTER CONSTRAINT file_library_rel_library_file_id_fkey DEFERRABLE INITIALLY IMMEDIATE;

View File

@@ -0,0 +1,5 @@
ALTER TABLE share_link
ADD COLUMN who_comment text NOT NULL DEFAULT('team'),
ADD COLUMN who_inspect text NOT NULL DEFAULT('team');
--- TODO: remove flags column in 1.15.x

View File

@@ -0,0 +1,10 @@
-- Renames the old, already deprecated backend name with new one on
-- all storage object rows.
UPDATE storage_object
SET backend = 'assets-fs'
WHERE backend = 'fs';
UPDATE storage_object
SET backend = 'assets-s3'
WHERE backend = 's3';

View File

@@ -0,0 +1,3 @@
--- Add frame_id field.
ALTER TABLE comment_thread
ADD COLUMN frame_id uuid NULL DEFAULT '00000000-0000-0000-0000-000000000000';

View File

@@ -0,0 +1,9 @@
ALTER TABLE file_media_object
DROP CONSTRAINT file_media_object_media_id_fkey,
ADD CONSTRAINT file_media_object_media_id_fkey
FOREIGN KEY (media_id) REFERENCES storage_object(id) ON DELETE NO ACTION DEFERRABLE;
ALTER TABLE file_media_object
DROP CONSTRAINT file_media_object_thumbnail_id_fkey,
ADD CONSTRAINT file_media_object_thumbnail_id_fkey
FOREIGN KEY (thumbnail_id) REFERENCES storage_object(id) ON DELETE NO ACTION DEFERRABLE;

View File

@@ -0,0 +1,2 @@
ALTER TABLE profile
ADD COLUMN is_blocked boolean DEFAULT false;

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.msgbus
"The msgbus abstraction implemented using redis as underlying backend."
@@ -13,28 +13,14 @@
[app.common.spec :as us]
[app.common.transit :as t]
[app.config :as cfg]
[app.redis :as redis]
[app.util.async :as aa]
[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])
(:import
io.lettuce.core.RedisClient
io.lettuce.core.RedisURI
io.lettuce.core.api.StatefulConnection
io.lettuce.core.api.StatefulRedisConnection
io.lettuce.core.api.async.RedisAsyncCommands
io.lettuce.core.codec.ByteArrayCodec
io.lettuce.core.codec.RedisCodec
io.lettuce.core.codec.StringCodec
io.lettuce.core.pubsub.RedisPubSubListener
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.resource.ClientResources
io.lettuce.core.resource.DefaultClientResources
java.time.Duration))
[promesa.core :as p]))
(set! *warn-on-reflection* true)
@@ -49,12 +35,12 @@
(declare ^:private redis-connect)
(declare ^:private redis-disconnect)
(declare ^:private start-io-loop)
(declare ^:private subscribe)
(declare ^:private purge)
(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]
@@ -62,77 +48,82 @@
:timeout (dt/duration {:seconds 30})}
(d/without-nils cfg)))
(s/def ::timeout ::dt/duration)
(s/def ::redis-uri ::us/string)
(s/def ::cmd-ch ::aa/channel)
(s/def ::rcv-ch ::aa/channel)
(s/def ::pub-ch ::aa/channel)
(s/def ::state ::us/agent)
(s/def ::pconn ::redis/connection)
(s/def ::sconn ::redis/connection)
(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-uri ::timeout ::wrk/executor]))
(s/keys :req-un [::buffer-size ::redis/timeout ::redis/redis ::wrk/executor]))
(defmethod ig/init-key ::msgbus
[_ {:keys [buffer-size redis-uri] :as cfg}]
(l/info :hint "initialize msgbus"
:buffer-size buffer-size
:redis-uri redis-uri)
[_ {:keys [buffer-size executor] :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)
state (agent {} :error-handler #(l/error :cause % :hint "unexpected error on agent" ::l/async false))
cfg (-> (redis-connect cfg)
state (agent {})
msgbus (-> (redis-connect cfg)
(assoc ::cmd-ch cmd-ch)
(assoc ::rcv-ch rcv-ch)
(assoc ::pub-ch pub-ch)
(assoc ::state state))]
(assoc ::state state)
(assoc ::wrk/executor executor))]
(start-io-loop cfg)
(us/verify! ::msgbus msgbus)
(with-meta
(fn [& {:keys [cmd] :as params}]
(a/go
(case cmd
:pub (a/>! pub-ch params)
:sub (a/<! (subscribe cfg params))
:purge (a/<! (purge cfg params))
(l/error :hint "unexpeced error on msgbus command processing" :params params))))
cfg)))
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/async false))
(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))
(defmethod ig/halt-key! ::msgbus
[_ f]
(let [mdata (meta f)]
(redis-disconnect mdata)
(a/close! (::cmd-ch mdata))
(a/close! (::rcv-ch mdata))))
[_ msgbus]
(redis-disconnect msgbus)
(a/close! (::cmd-ch msgbus))
(a/close! (::rcv-ch msgbus))
(a/close! (::pub-ch msgbus)))
;; --- IMPL
(defn- redis-connect
[{:keys [redis-uri timeout] :as cfg}]
(let [codec (RedisCodec/of StringCodec/UTF8 ByteArrayCodec/INSTANCE)
resources (.. (DefaultClientResources/builder)
(ioThreadPoolSize 4)
(computationThreadPoolSize 4)
(build))
uri (RedisURI/create redis-uri)
rclient (RedisClient/create ^ClientResources resources ^RedisURI uri)
pconn (.connect ^RedisClient rclient ^RedisCodec codec)
sconn (.connectPubSub ^RedisClient rclient ^RedisCodec codec)]
(.setTimeout ^StatefulRedisConnection pconn ^Duration timeout)
(.setTimeout ^StatefulRedisPubSubConnection sconn ^Duration timeout)
(-> cfg
(assoc ::resources resources)
(assoc ::pconn pconn)
(assoc ::sconn sconn))))
[{: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 ::resources] :as cfg}]
(.. ^StatefulConnection pconn close)
(.. ^StatefulConnection sconn close)
(.shutdown ^ClientResources resources))
[{:keys [::pconn ::sconn] :as cfg}]
(redis/close! pconn)
(redis/close! sconn))
(defn- conj-subscription
"A low level function that is responsible to create on-demand
@@ -160,7 +151,6 @@
"Function responsible to attach local subscription to the
state. Intended to be used in agent."
[state cfg topics chan done-ch]
(l/trace :hint "subscribe-to-topics" :topics topics ::l/async false)
(aa/with-closing done-ch
(let [state (update state :chans assoc chan topics)]
(reduce (fn [state topic]
@@ -184,48 +174,23 @@
useful when client disconnects or in-bulk unsubscribe
operations. Intended to be executed in agent."
[state cfg channels done-ch]
(l/trace :hint "unsubscribe-channels" :chans (count channels) ::l/async false)
(aa/with-closing done-ch
(reduce #(unsubscribe-single-channel %1 cfg %2) state channels)))
(defn- subscribe
[{:keys [::state executor] :as cfg} {:keys [topic topics chan]}]
(let [done-ch (a/chan)
topics (into [] (map prefix-topic) (if topic [topic] topics))]
(l/trace :hint "subscribe" :topics topics)
(send-via executor state subscribe-to-topics cfg topics chan done-ch)
done-ch))
(defn- purge
[{:keys [::state executor] :as cfg} {:keys [chans]}]
(l/trace :hint "purge" :chans (count chans))
(let [done-ch (a/chan)]
(send-via executor state unsubscribe-channels cfg chans done-ch)
done-ch))
(defn- create-listener
[rcv-ch]
(reify RedisPubSubListener
(message [_ _pattern _topic _message])
(message [_ 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)
(l/warn :msg "dropping message on subscription loop"))))
(psubscribed [_ _pattern _count])
(punsubscribed [_ _pattern _count])
(subscribed [_ _topic _count])
(unsubscribed [_ _topic _count])))
(defn start-io-loop
[{:keys [::sconn ::rcv-ch ::pub-ch ::state executor] :as cfg}]
;; Add a single listener to the pubsub connection
(.addListener ^StatefulRedisPubSubConnection sconn
^RedisPubSubListener (create-listener rcv-ch))
(redis/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)
(l/warn :msg "dropping message on subscription loop"))))))
(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 #{}]
@@ -271,11 +236,10 @@
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)
pcomm (.async ^StatefulRedisConnection pconn)]
(-> (.publish ^RedisAsyncCommands pcomm ^String topic ^bytes message)
res (a/chan 1)]
(-> (redis/publish! pconn topic message)
(p/finally (fn [_ cause]
(when (and cause (.isOpen ^StatefulConnection pconn))
(when (and cause (redis/open? pconn))
(a/offer! res cause))
(a/close! res))))
res))
@@ -284,14 +248,10 @@
"Create redis subscription. Blocking operation, intended to be used
inside an agent."
[{:keys [::sconn] :as cfg} topic]
(let [topic (into-array String [topic])
scomm (.sync ^StatefulRedisPubSubConnection sconn)]
(.subscribe ^RedisPubSubCommands scomm topic)))
(redis/subscribe! sconn topic))
(defn redis-unsub
"Removes redis subscription. Blocking operation, intended to be used
inside an agent."
[{:keys [::sconn] :as cfg} topic]
(let [topic (into-array String [topic])
scomm (.sync ^StatefulRedisPubSubConnection sconn)]
(.unsubscribe ^RedisPubSubCommands scomm topic)))
(redis/unsubscribe! sconn topic))

319
backend/src/app/redis.clj Normal file
View File

@@ -0,0 +1,319 @@
;; 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.redis
"The msgbus abstraction implemented using redis as underlying backend."
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.spec :as us]
[app.metrics :as mtx]
[app.redis.script :as-alias rscript]
[app.util.time :as dt]
[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])
(:import
clojure.lang.IDeref
io.lettuce.core.RedisClient
io.lettuce.core.RedisURI
io.lettuce.core.ScriptOutputType
io.lettuce.core.api.StatefulConnection
io.lettuce.core.api.StatefulRedisConnection
io.lettuce.core.api.async.RedisAsyncCommands
io.lettuce.core.api.async.RedisScriptingAsyncCommands
io.lettuce.core.codec.ByteArrayCodec
io.lettuce.core.codec.RedisCodec
io.lettuce.core.codec.StringCodec
io.lettuce.core.pubsub.RedisPubSubListener
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.resource.ClientResources
io.lettuce.core.resource.DefaultClientResources
io.netty.util.HashedWheelTimer
io.netty.util.Timer
java.lang.AutoCloseable
java.time.Duration))
(set! *warn-on-reflection* true)
(declare initialize-resources)
(declare shutdown-resources)
(declare connect)
(declare close!)
(s/def ::timer
#(instance? Timer %))
(s/def ::connection
#(or (instance? StatefulRedisConnection %)
(and (instance? IDeref %)
(instance? StatefulRedisConnection (deref %)))))
(s/def ::pubsub-connection
#(or (instance? StatefulRedisPubSubConnection %)
(and (instance? IDeref %)
(instance? StatefulRedisPubSubConnection (deref %)))))
(s/def ::redis-uri
#(instance? RedisURI %))
(s/def ::resources
#(instance? ClientResources %))
(s/def ::pubsub-listener
#(instance? RedisPubSubListener %))
(s/def ::uri ::us/not-empty-string)
(s/def ::timeout ::dt/duration)
(s/def ::connect? ::us/boolean)
(s/def ::io-threads ::us/integer)
(s/def ::worker-threads ::us/integer)
(s/def ::redis
(s/keys :req [::resources ::redis-uri ::timer ::mtx/metrics]
:opt [::connection]))
(defmethod ig/pre-init-spec ::redis [_]
(s/keys :req-un [::uri ::mtx/metrics]
:opt-un [::timeout
::connect?
::io-threads
::worker-threads]))
(defmethod ig/prep-key ::redis
[_ cfg]
(let [runtime (Runtime/getRuntime)
cpus (.availableProcessors ^Runtime runtime)]
(merge {:timeout (dt/duration 5000)
:io-threads (max 3 cpus)
:worker-threads (max 3 cpus)}
(d/without-nils cfg))))
(defmethod ig/init-key ::redis
[_ {:keys [connect?] :as cfg}]
(let [cfg (initialize-resources cfg)]
(cond-> cfg
connect? (assoc ::connection (connect cfg)))))
(defmethod ig/halt-key! ::redis
[_ state]
(shutdown-resources state))
(def default-codec
(RedisCodec/of StringCodec/UTF8 ByteArrayCodec/INSTANCE))
(def string-codec
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
(defn- initialize-resources
"Initialize redis connection resources"
[{:keys [uri io-threads worker-threads connect? metrics] :as cfg}]
(l/info :hint "initialize redis resources"
:uri uri
:io-threads io-threads
:worker-threads worker-threads
:connect? connect?)
(let [timer (HashedWheelTimer.)
resources (.. (DefaultClientResources/builder)
(ioThreadPoolSize ^long io-threads)
(computationThreadPoolSize ^long worker-threads)
(timer ^Timer timer)
(build))
redis-uri (RedisURI/create ^String uri)]
(-> cfg
(assoc ::mtx/metrics metrics)
(assoc ::cache (atom {}))
(assoc ::timer timer)
(assoc ::redis-uri redis-uri)
(assoc ::resources resources))))
(defn- shutdown-resources
[{:keys [::resources ::cache ::timer]}]
(run! close! (vals @cache))
(when resources
(.shutdown ^ClientResources resources))
(when timer
(.stop ^Timer timer)))
(defn connect
[{:keys [::resources ::redis-uri] :as cfg}
& {:keys [timeout codec type] :or {codec default-codec type :default}}]
(us/assert! ::resources resources)
(let [client (RedisClient/create ^ClientResources resources ^RedisURI redis-uri)
timeout (or timeout (:timeout cfg))
conn (case type
:default (.connect ^RedisClient client ^RedisCodec codec)
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
(.setTimeout ^StatefulConnection conn ^Duration timeout)
(reify
IDeref
(deref [_] conn)
AutoCloseable
(close [_]
(.close ^StatefulConnection conn)
(.shutdown ^RedisClient client)))))
(defn get-or-connect
[{:keys [::cache] :as state} key options]
(assoc state ::connection
(or (get @cache key)
(-> (swap! cache (fn [cache]
(when-let [prev (get cache key)]
(close! prev))
(assoc cache key (connect state options))))
(get key)))))
(defn add-listener!
[conn listener]
(us/assert! ::pubsub-connection @conn)
(us/assert! ::pubsub-listener listener)
(.addListener ^StatefulRedisPubSubConnection @conn
^RedisPubSubListener listener)
conn)
(defn publish!
[conn topic message]
(us/assert! ::us/string topic)
(us/assert! ::us/bytes message)
(us/assert! ::connection @conn)
(let [pcomm (.async ^StatefulRedisConnection @conn)]
(.publish ^RedisAsyncCommands pcomm ^String topic ^bytes message)))
(defn subscribe!
"Blocking operation, intended to be used on a worker/agent thread."
[conn & topics]
(us/assert! ::pubsub-connection @conn)
(let [topics (into-array String (map str topics))
cmd (.sync ^StatefulRedisPubSubConnection @conn)]
(.subscribe ^RedisPubSubCommands cmd topics)))
(defn unsubscribe!
"Blocking operation, intended to be used on a worker/agent thread."
[conn & topics]
(us/assert! ::pubsub-connection @conn)
(let [topics (into-array String (map str topics))
cmd (.sync ^StatefulRedisPubSubConnection @conn)]
(.unsubscribe ^RedisPubSubCommands cmd topics)))
(defn open?
[conn]
(.isOpen ^StatefulConnection @conn))
(defn pubsub-listener
[& {:keys [on-message on-subscribe on-unsubscribe]}]
(reify RedisPubSubListener
(message [_ pattern topic message]
(when on-message
(on-message pattern topic message)))
(message [_ topic message]
(when on-message
(on-message nil topic message)))
(psubscribed [_ pattern count]
(when on-subscribe
(on-subscribe pattern nil count)))
(punsubscribed [_ pattern count]
(when on-unsubscribe
(on-unsubscribe pattern nil count)))
(subscribed [_ topic count]
(when on-subscribe
(on-subscribe nil topic count)))
(unsubscribed [_ topic count]
(when on-unsubscribe
(on-unsubscribe nil topic count)))))
(defn close!
[o]
(.close ^AutoCloseable o))
(def ^:private scripts-cache (atom {}))
(def noop-fn (constantly nil))
(s/def ::rscript/name qualified-keyword?)
(s/def ::rscript/path ::us/not-empty-string)
(s/def ::rscript/keys (s/every any? :kind vector?))
(s/def ::rscript/vals (s/every any? :kind vector?))
(s/def ::rscript/script
(s/keys :req [::rscript/name
::rscript/path]
:opt [::rscript/keys
::rscript/vals]))
(defn eval!
[{:keys [::mtx/metrics] :as state} script]
(us/assert! ::rscript/script script)
(us/assert! ::redis state)
(let [rconn (-> state ::connection deref)
cmd (.async ^StatefulRedisConnection rconn)
keys (into-array String (map str (::rscript/keys script)))
vals (into-array String (map str (::rscript/vals script)))
sname (::rscript/name script)]
(letfn [(on-error [cause]
(if (instance? io.lettuce.core.RedisNoScriptException cause)
(do
(l/error :hint "no script found" :name sname :cause cause)
(-> (load-script)
(p/then eval-script)))
(if-let [on-error (::rscript/on-error script)]
(on-error cause)
(p/rejected cause))))
(eval-script [sha]
(let [tpoint (dt/tpoint)]
(-> (.evalsha ^RedisScriptingAsyncCommands cmd
^String sha
^ScriptOutputType ScriptOutputType/MULTI
^"[Ljava.lang.String;" keys
^"[Ljava.lang.String;" vals)
(p/then (fn [result]
(let [elapsed (tpoint)]
(mtx/run! metrics {:id :redis-eval-timing
:labels [(name sname)]
:val (inst-ms elapsed)})
(l/trace :hint "eval script"
:name (name sname)
:sha sha
:params (str/join "," (::rscript/vals script))
:elapsed (dt/format-duration elapsed))
result)))
(p/catch on-error))))
(read-script []
(-> script ::rscript/path io/resource slurp))
(load-script []
(l/trace :hint "load script" :name sname)
(-> (.scriptLoad ^RedisScriptingAsyncCommands cmd
^String (read-script))
(p/then (fn [sha]
(swap! scripts-cache assoc sname sha)
sha))))]
(if-let [sha (get @scripts-cache sname)]
(eval-script sha)
(-> (load-script)
(p/then eval-script))))))

View File

@@ -2,22 +2,23 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.rpc
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.db :as db]
[app.http :as-alias http]
[app.loggers.audit :as audit]
[app.metrics :as mtx]
[app.msgbus :as-alias mbus]
[app.rpc.retry :as retry]
[app.rpc.rlimit :as rlimit]
[app.util.async :as async]
[app.rpc.semaphore :as-alias rsem]
[app.util.services :as sv]
[app.worker :as wrk]
[app.util.time :as ts]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
@@ -30,9 +31,10 @@
(defn- handle-response-transformation
[response request mdata]
(if-let [transform-fn (:transform-response mdata)]
(p/do (transform-fn request response))
(p/resolved response)))
(let [response (if (sv/wrapped? response) @response response)]
(if-let [transform-fn (:transform-response mdata)]
(p/do (transform-fn request response))
(p/resolved response))))
(defn- handle-before-comple-hook
[response mdata]
@@ -40,88 +42,90 @@
(ex/ignoring (hook-fn)))
response)
(defn- handle-response
[request result]
(let [mdata (meta result)]
(p/-> (yrs/response 200 result (::http/headers mdata {}))
(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 [profile-id session-id params] :as request} respond raise]
(letfn [(handle-response [result]
(let [mdata (meta result)]
(-> (yrs/response 200 result)
(handle-response-transformation request mdata))))]
(let [type (keyword (:type params))
data (into {::http/request request} params)
data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods type default-handler)]
(let [type (keyword (:type params))
data (into {::request request} params)
data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods type default-handler)]
(-> (method data)
(p/then handle-response)
(p/then respond)
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context)))))))))
(-> (method data)
(p/then (partial handle-response request))
(p/then respond)
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context))))))))
(defn- rpc-mutation-handler
"Ring handler that dispatches mutation requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(letfn [(handle-response [result]
(let [mdata (meta result)]
(p/-> (yrs/response 200 result)
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata))))]
(let [type (keyword (:type params))
data (into {::request request} params)
data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
(let [type (keyword (:type params))
data (into {::request request} params)
data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods type default-handler)]
(-> (method data)
(p/then (partial handle-response request))
(p/then respond)
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context))))))))
method (get methods type default-handler)]
(-> (method data)
(p/then handle-response)
(p/then respond)
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context)))))))))
(defn- rpc-command-handler
"Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(let [cmd (keyword (:command params))
data (into {::request request} params)
data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods cmd default-handler)]
(-> (method data)
(p/then (partial handle-response request))
(p/then respond)
(p/catch (fn [cause]
(let [context {:profile-id profile-id}]
(raise (ex/wrap-with-context cause context))))))))
(defn- wrap-metrics
"Wrap service method with metrics measurement."
[{:keys [metrics ::metrics-id]} f mdata]
(let [labels (into-array String [(::sv/name mdata)])]
(fn [cfg params]
(let [start (System/nanoTime)]
(let [tp (ts/tpoint)]
(p/finally
(f cfg params)
(fn [_ _]
(mtx/run! metrics
{:id metrics-id
:val (/ (- (System/nanoTime) start) 1000000)
:labels labels})))))))
:id metrics-id
:val (inst-ms (tp))
:labels labels)))))))
(defn- wrap-dispatch
"Wraps service method into async flow, with the ability to dispatching
it to a preconfigured executor service."
[{:keys [executors] :as cfg} f mdata]
(let [dname (::async/dispatch mdata :default)]
(if (= :none dname)
(with-meta
(fn [cfg params]
(p/do (f cfg params)))
mdata)
(let [executor (get executors dname)]
(when-not executor
(ex/raise :type :internal
:code :executor-not-configured
:hint (format "executor %s not configured" dname)))
(with-meta
(fn [cfg params]
(-> (px/submit! executor #(f cfg params))
(p/bind p/wrap)))
mdata)))))
[{:keys [executor] :as cfg} f mdata]
(with-meta
(fn [cfg params]
(-> (px/submit! executor #(f cfg params))
(p/bind p/wrap)))
mdata))
(defn- wrap-audit
[{:keys [audit] :as cfg} f mdata]
@@ -132,10 +136,13 @@
(fn [result _]
(when result
(let [resultm (meta result)
profile-id (or (:profile-id params)
profile-id (or (::audit/profile-id resultm)
(:profile-id result)
(::audit/profile-id resultm))
props (d/merge params (::audit/props resultm))]
(:profile-id params))
props (or (::audit/replace-props resultm)
(-> params
(merge (::audit/props resultm))
(dissoc :type)))]
(audit :cmd :submit
:type (or (::audit/type resultm)
(::type cfg))
@@ -151,16 +158,16 @@
[cfg f mdata]
(let [f (as-> f $
(wrap-dispatch cfg $ mdata)
(rlimit/wrap-rlimit cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(wrap-audit cfg $ mdata)
(wrap-metrics cfg $ mdata)
)
(retry/wrap-retry cfg $ mdata)
(rsem/wrap cfg $ mdata)
(rlimit/wrap cfg $ mdata)
(wrap-audit cfg $ mdata))
spec (or (::sv/spec mdata) (s/spec any?))
auth? (:auth mdata true)]
(l/trace :action "register" :name (::sv/name mdata))
(l/debug :hint "register method" :name (::sv/name mdata))
(with-meta
(fn [{:keys [::request] :as params}]
;; Raise authentication error when rpc method requires auth but
@@ -179,7 +186,7 @@
[cfg vfn]
(let [mdata (meta vfn)]
[(keyword (::sv/name mdata))
(wrap cfg (deref vfn) mdata)]))
(wrap cfg vfn mdata)]))
(defn- resolve-query-methods
[cfg]
@@ -197,35 +204,84 @@
(defn- resolve-mutation-methods
[cfg]
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
(->> (sv/scan-ns 'app.rpc.mutations.demo
'app.rpc.mutations.media
(->> (sv/scan-ns 'app.rpc.mutations.media
'app.rpc.mutations.profile
'app.rpc.mutations.files
'app.rpc.mutations.comments
'app.rpc.mutations.projects
'app.rpc.mutations.teams
'app.rpc.mutations.management
'app.rpc.mutations.ldap
'app.rpc.mutations.fonts
'app.rpc.mutations.share-link
'app.rpc.mutations.verify-token)
(map (partial process-method cfg))
(into {}))))
(s/def ::storage some?)
(s/def ::session map?)
(s/def ::tokens fn?)
(defn- resolve-command-methods
[cfg]
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns 'app.rpc.commands.binfile
'app.rpc.commands.comments
'app.rpc.commands.management
'app.rpc.commands.verify-token
'app.rpc.commands.auth
'app.rpc.commands.ldap
'app.rpc.commands.demo
'app.rpc.commands.files)
(map (partial process-method cfg))
(into {}))))
(s/def ::audit (s/nilable fn?))
(s/def ::executors (s/map-of keyword? ::wrk/executor))
(s/def ::http-client fn?)
(s/def ::ldap (s/nilable map?))
(s/def ::msgbus ::mbus/msgbus)
(s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?)
(s/def ::storage some?)
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::rpc [_]
(s/keys :req-un [::storage ::session ::tokens ::audit
::executors ::mtx/metrics ::db/pool]))
(defmethod ig/pre-init-spec ::methods [_]
(s/keys :req-un [::storage
::session
::sprops
::audit
::public-uri
::msgbus
::http-client
::rsem/semaphores
::rlimit/rlimit
::mtx/metrics
::db/pool
::ldap]))
(defmethod ig/init-key ::rpc
(defmethod ig/init-key ::methods
[_ cfg]
(let [mq (resolve-query-methods cfg)
mm (resolve-mutation-methods cfg)]
{:methods {:query mq :mutation mm}
:query-handler (partial rpc-query-handler mq)
:mutation-handler (partial rpc-mutation-handler mm)}))
{: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?))
(s/def ::methods
(s/keys :req-un [::mutations
::queries
::commands]))
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req-un [::methods]))
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
[["/rpc"
["/command/:command" {: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}}]]])

View File

@@ -0,0 +1,500 @@
;; 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.auth
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.loggers.audit :as audit]
[app.rpc.doc :as-alias doc]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.rpc.semaphore :as rsem]
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.hashers :as hashers]
[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 ::profile-id ::us/uuid)
(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)
;; ---- HELPERS
(defn derive-password
[password]
(hashers/derive password
{:alg :argon2id
:memory 16384
:iterations 20
:parallelism 2}))
(defn verify-password
[attempt password]
(try
(hashers/verify attempt password)
(catch Exception _e
{: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."
[domains email]
(if (or (empty? domains)
(nil? domains))
true
(let [[_ candidate] (-> (str/lower email)
(str/split #"@" 2))]
(contains? domains candidate))))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = ?
and deleted_at is null) as val")
(defn check-profile-existence!
[conn {:keys [email] :as params}]
(let [email (str/lower email)
result (db/exec-one! conn [sql:profile-existence email])]
(when (:val result)
(ex/raise :type :validation
:code :email-already-exists))
params))
;; ---- COMMAND: login with password
(defn login-with-password
[{:keys [pool session sprops] :as cfg} {:keys [email password] :as params}]
(when-not (contains? cf/flags :login)
(ex/raise :type :restriction
:code :login-disabled
:hint "login is disabled in this instance"))
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password"))
(:valid (verify-password password (:password profile))))
(validate-profile [profile]
(when-not profile
(ex/raise :type :validation
:code :wrong-credentials))
(when-not (:is-active profile)
(ex/raise :type :validation
:code :wrong-credentials))
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(when-not (check-password profile password)
(ex/raise :type :validation
:code :wrong-credentials))
(when-let [deleted-at (:deleted-at profile)]
(when (dt/is-after? (dt/now) deleted-at)
(ex/raise :type :validation
:code :wrong-credentials)))
profile)]
(db/with-atomic [conn pool]
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
(validate-profile)
(profile/strip-private-attrs)
(profile/populate-additional-data conn)
(profile/decode-profile-row))
invitation (when-let [token (:invitation-token params)]
(tokens/verify sprops {:token token :iss :team-invitation}))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
;; invitation because invitations matches exactly; and user can't loging with other email and
;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)}
profile)]
(with-meta response
{:transform-response ((:create session) (:id profile))
::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]))
(sv/defmethod ::login-with-password
"Performs authentication using penpot password."
{:auth false
::rsem/queue :auth
::doc/added "1.15"}
[cfg params]
(login-with-password cfg params))
;; ---- COMMAND: Logout
(s/def ::logout
(s/keys :opt-un [::profile-id]))
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
{:auth false
::doc/added "1.15"}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))
;; ---- COMMAND: Recover Profile
(defn recover-profile
[{:keys [pool sprops] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token]
(let [tdata (tokens/verify sprops {:token token :iss :password-recovery})]
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (derive-password password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool]
(->> (validate-token token)
(update-password conn))
nil)))
(s/def ::token ::us/not-empty-string)
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile
{:auth false
::rsem/queue :auth
::doc/added "1.15"}
[cfg params]
(recover-profile cfg params))
;; ---- COMMAND: Prepare Register
(defn validate-register-attempt!
[{:keys [pool sprops]} params]
(when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)
(let [invitation (tokens/verify sprops {: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-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)))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spammer.
(when (eml/has-bounce-reports? pool (:email params))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email has one or many bounces reported"))
;; Perform a basic validation of email & password
(when (= (str/lower (:email params))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password")))
(def register-retry-threshold
(dt/duration "15m"))
(defn- elapsed-register-retry-threshold?
[profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
(pos? (compare elapsed register-retry-threshold))))
(defn prepare-register
[{:keys [pool sprops] :as cfg} params]
(validate-register-attempt! cfg params)
(let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))]
(cond
(:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked)
(and (not (:is-active profile))
(elapsed-register-retry-threshold? profile))
profile
:else
(ex/raise :type :validation
:code :email-already-exists
:hint "profile already exists")))
params {:email (:email params)
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:profile-id (:id profile)
:exp (dt/in-future {:days 7})}
params (d/without-nils params)
token (tokens/generate sprops params)]
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
(s/def ::prepare-register-profile
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::prepare-register-profile
{:auth false
::doc/added "1.15"}
[cfg params]
(prepare-register cfg params))
;; ---- COMMAND: Register Profile
(defn create-profile
"Create the profile entry on the database with limited set of input
attrs (all the other attrs are filled with default values)."
[conn params]
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
(merge {:viewed-tutorial? false
:viewed-walkthrough? false})
(db/tjson))
password (if-let [password (:password params)]
(derive-password password)
"!")
locale (:locale params)
locale (when (and (string? locale) (not (str/blank? locale)))
locale)
backend (:backend params "penpot")
is-demo (:is-demo params false)
is-muted (:is-muted params false)
is-active (:is-active params false)
email (str/lower (:email params))
params {:id id
:fullname (:fullname params)
:email email
:auth-backend backend
:lang locale
:password password
:deleted-at (:deleted-at params)
:props props
:is-active is-active
:is-muted is-muted
:is-demo is-demo}]
(try
(-> (db/insert! conn :profile params)
(profile/decode-profile-row))
(catch org.postgresql.util.PSQLException e
(let [state (.getSQLState e)]
(if (not= state "23505")
(throw e)
(ex/raise :type :validation
:code :email-already-exists
:cause e)))))))
(defn create-profile-relations
[conn profile]
(let [team (teams/create-team conn {:profile-id (:id profile)
:name "Default"
:is-default true})]
(-> profile
(profile/strip-private-attrs)
(assoc :default-team-id (:id team))
(assoc :default-project-id (:default-project-id team)))))
(defn send-email-verification!
[conn sprops profile]
(let [vtoken (tokens/generate sprops
{:iss :verify-email
:exp (dt/in-future "72h")
:profile-id (:id profile)
:email (:email profile)})
;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook
ptoken (tokens/generate sprops
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
(eml/send! {::eml/conn conn
::eml/factory eml/register
:public-uri (cf/get :public-uri)
:to (:email profile)
:name (:fullname profile)
:token vtoken
:extra-data ptoken})))
(defn register-profile
[{:keys [conn sprops session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify sprops {:token token :iss :prepared-register})
params (merge params claims)
is-active (or (:is-active params)
(not (contains? cf/flags :email-verification))
;; DEPRECATED: v1.15
(contains? cf/flags :insecure-register))
profile (if-let [profile-id (:profile-id claims)]
(profile/retrieve-profile conn profile-id)
(->> (assoc params :is-active is-active)
(create-profile conn)
(create-profile-relations conn)
(profile/decode-profile-row)))
audit-fn (:audit cfg)
invitation (when-let [token (:invitation-token params)]
(tokens/verify sprops {:token token :iss :team-invitation}))]
;; If profile is filled in claims, means it tries to register
;; again, so we proceed to update the modified-at attr
;; accordingly.
(when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
(audit-fn :cmd :submit
:type "fact"
:name "register-profile-retry"
:profile-id id))
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged). This happens
;; only if the invitation email matches with the register
;; email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens/generate sprops claims)
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
;; If auth backend is different from "penpot" means user is
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; In all other cases, send a verification email.
:else
(do
(send-email-verification! conn sprops profile)
(with-meta profile
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile
{:auth false
::rsem/queue :auth
::doc/added "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(register-profile params))))
;; ---- COMMAND: Request Profile Recovery
(defn request-profile-recovery
[{:keys [pool sprops] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate sprops
{:iss :password-recovery
:exp (dt/in-future "15m")
:profile-id id})]
(assoc profile :token token)))
(send-email-notification [conn profile]
(let [ptoken (tokens/generate sprops
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
(eml/send! {::eml/conn conn
::eml/factory eml/password-recovery
:public-uri (:public-uri cfg)
:to (:email profile)
:token (:token profile)
:name (:fullname profile)
:extra-data ptoken})
nil))]
(db/with-atomic [conn pool]
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
(when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(when-not (:is-active profile)
(ex/raise :type :validation
:code :profile-not-verified
:hint "the user need to validate profile before recover password"))
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(->> profile
(create-recovery-token)
(send-email-notification conn))))))
(s/def ::request-profile-recovery
(s/keys :req-un [::email]))
(sv/defmethod ::request-profile-recovery
{:auth false
::doc/added "1.15"}
[cfg params]
(request-profile-recovery cfg params))

View File

@@ -0,0 +1,900 @@
;; 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.binfile
(:refer-clojure :exclude [assert])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.media :as media]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.files :as files]
[app.rpc.queries.projects :as projects]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.tasks.file-gc]
[app.util.blob :as blob]
[app.util.fressian :as fres]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]
[cuerdas.core :as str]
[datoteka.io :as io]
[yetti.adapter :as yt]
[yetti.response :as yrs])
(:import
com.github.luben.zstd.ZstdInputStream
com.github.luben.zstd.ZstdOutputStream
java.io.DataInputStream
java.io.DataOutputStream
java.io.InputStream
java.io.OutputStream
java.lang.AutoCloseable))
(set! *warn-on-reflection* true)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DEFAULTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Threshold in MiB when we pass from using
;; in-memory byte-array's to use temporal files.
(def temp-file-threshold
(* 1024 1024 2))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; LOW LEVEL STREAM IO API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
(def ^:const penpot-magic-number 800099563638710213)
(def ^:const max-object-size (* 1024 1024 100)) ; Only allow 100MiB max file size.
(def ^:dynamic *position* nil)
(defn get-mark
[id]
(case id
:header 1
:stream 2
:uuid 3
:label 4
:obj 5
(ex/raise :type :validation
:code :invalid-mark-id
:hint (format "invalid mark id %s" id))))
(defmacro assert
[expr hint]
`(when-not ~expr
(ex/raise :type :validation
:code :unexpected-condition
:hint ~hint)))
(defmacro assert-mark
[v type]
`(let [expected# (get-mark ~type)
val# (long ~v)]
(when (not= val# expected#)
(ex/raise :type :validation
:code :unexpected-mark
:hint (format "received mark %s, expected %s" val# expected#)))))
(defmacro assert-label
[expr label]
`(let [v# ~expr]
(when (not= v# ~label)
(ex/raise :type :assertion
:code :unexpected-label
:hint (format "received label %s, expected %s" v# ~label)))))
;; --- PRIMITIVE IO
(defn write-byte!
[^DataOutputStream output data]
(l/trace :fn "write-byte!" :data data :position @*position* ::l/async false)
(.writeByte output (byte data))
(swap! *position* inc))
(defn read-byte!
[^DataInputStream input]
(let [v (.readByte input)]
(l/trace :fn "read-byte!" :val v :position @*position* ::l/async false)
(swap! *position* inc)
v))
(defn write-long!
[^DataOutputStream output data]
(l/trace :fn "write-long!" :data data :position @*position* ::l/async false)
(.writeLong output (long data))
(swap! *position* + 8))
(defn read-long!
[^DataInputStream input]
(let [v (.readLong input)]
(l/trace :fn "read-long!" :val v :position @*position* ::l/async false)
(swap! *position* + 8)
v))
(defn write-bytes!
[^DataOutputStream output ^bytes data]
(let [size (alength data)]
(l/trace :fn "write-bytes!" :size size :position @*position* ::l/async false)
(.write output data 0 size)
(swap! *position* + size)))
(defn read-bytes!
[^InputStream input ^bytes buff]
(let [size (alength buff)
readed (.readNBytes input buff 0 size)]
(l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/async false)
(swap! *position* + readed)
readed))
;; --- COMPOSITE IO
(defn write-uuid!
[^DataOutputStream output id]
(l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/async false)
(doto output
(write-byte! (get-mark :uuid))
(write-long! (uuid/get-word-high id))
(write-long! (uuid/get-word-low id))))
(defn read-uuid!
[^DataInputStream input]
(l/trace :fn "read-uuid!" :position @*position* ::l/async false)
(let [m (read-byte! input)]
(assert-mark m :uuid)
(let [a (read-long! input)
b (read-long! input)]
(uuid/custom a b))))
(defn write-obj!
[^DataOutputStream output data]
(l/trace :fn "write-obj!" :position @*position* ::l/async false)
(let [^bytes data (fres/encode data)]
(doto output
(write-byte! (get-mark :obj))
(write-long! (alength data))
(write-bytes! data))))
(defn read-obj!
[^DataInputStream input]
(l/trace :fn "read-obj!" :position @*position* ::l/async false)
(let [m (read-byte! input)]
(assert-mark m :obj)
(let [size (read-long! input)]
(assert (pos? size) "incorrect header size found on reading header")
(let [buff (byte-array size)]
(read-bytes! input buff)
(fres/decode buff)))))
(defn write-label!
[^DataOutputStream output label]
(l/trace :fn "write-label!" :label label :position @*position* ::l/async false)
(doto output
(write-byte! (get-mark :label))
(write-obj! label)))
(defn read-label!
[^DataInputStream input]
(l/trace :fn "read-label!" :position @*position* ::l/async false)
(let [m (read-byte! input)]
(assert-mark m :label)
(read-obj! input)))
(defn write-header!
[^OutputStream output version]
(l/trace :fn "write-header!"
:version version
:position @*position*
::l/async false)
(let [vers (-> version name (subs 1) parse-long)
output (io/data-output-stream output)]
(doto output
(write-byte! (get-mark :header))
(write-long! penpot-magic-number)
(write-long! vers))))
(defn read-header!
[^InputStream input]
(l/trace :fn "read-header!" :position @*position* ::l/async false)
(let [input (io/data-input-stream input)
mark (read-byte! input)
mnum (read-long! input)
vers (read-long! input)]
(when (or (not= mark (get-mark :header))
(not= mnum penpot-magic-number))
(ex/raise :type :validation
:code :invalid-penpot-file
:hint "invalid penpot file"))
(keyword (str "v" vers))))
(defn copy-stream!
[^OutputStream output ^InputStream input ^long size]
(let [written (io/copy! input output :size size)]
(l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/async false)
(swap! *position* + written)
written))
(defn write-stream!
[^DataOutputStream output stream size]
(l/trace :fn "write-stream!" :position @*position* ::l/async false :size size)
(doto output
(write-byte! (get-mark :stream))
(write-long! size))
(copy-stream! output stream size))
(defn read-stream!
[^DataInputStream input]
(l/trace :fn "read-stream!" :position @*position* ::l/async false)
(let [m (read-byte! input)
s (read-long! input)
p (tmp/tempfile :prefix "penpot.binfile.")]
(assert-mark m :stream)
(when (> s max-object-size)
(ex/raise :type :validation
:code :max-file-size-reached
:hint (str/ffmt "unable to import storage object with size % bytes" s)))
(if (> s temp-file-threshold)
(with-open [^OutputStream output (io/output-stream p)]
(let [readed (io/copy! input output :offset 0 :size s)]
(l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/async false)
(swap! *position* + readed)
[s p]))
[s (io/read-as-bytes input :size s)])))
(defmacro assert-read-label!
[input expected-label]
`(let [readed# (read-label! ~input)
expected# ~expected-label]
(when (not= readed# expected#)
(ex/raise :type :validation
:code :unexpected-label
:hint (format "unxpected label found: %s, expected: %s" readed# expected#)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- HELPERS
(defn zstd-input-stream
^InputStream
[input]
(ZstdInputStream. ^InputStream input))
(defn zstd-output-stream
^OutputStream
[output & {:keys [level] :or {level 0}}]
(ZstdOutputStream. ^OutputStream output (int level)))
(defn- retrieve-file
[pool file-id]
(->> (db/query pool :file {:id file-id})
(map files/decode-row)
(first)))
(def ^:private sql:file-media-objects
"SELECT * FROM file_media_object WHERE id = ANY(?)")
(defn- retrieve-file-media
[pool {:keys [data id] :as file}]
(with-open [^AutoCloseable conn (db/open pool)]
(let [ids (app.tasks.file-gc/collect-used-media data)
ids (db/create-array conn "uuid" ids)]
;; 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])
(mapv #(assoc % :file-id id))))))
(def ^:private storage-object-id-xf
(comp
(mapcat (juxt :media-id :thumbnail-id))
(filter uuid?)))
(def ^:private sql:file-libraries
"WITH RECURSIVE libs AS (
SELECT fl.id, fl.deleted_at
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
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();")
(defn- retrieve-libraries
[pool ids]
(with-open [^AutoCloseable 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- create-or-update-file
[conn params]
(let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?) "
"ON CONFLICT (id) DO UPDATE SET data=?")]
(db/exec-one! conn [sql
(:id params)
(:project-id params)
(:name params)
(:revn params)
(:is-shared params)
(:data params)
(:created-at params)
(:modified-at params)
(:data params)])))
;; --- GENERAL PURPOSE DYNAMIC VARS
(def ^:dynamic *state*)
(def ^:dynamic *options*)
;; --- EXPORT WRITTER
(defn- embed-file-assets
[data conn file-id]
(letfn [(walk-map-form [form state]
(cond
(uuid? (:fill-color-ref-file form))
(do
(vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)])
(assoc form :fill-color-ref-file file-id))
(uuid? (:stroke-color-ref-file form))
(do
(vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)])
(assoc form :stroke-color-ref-file file-id))
(uuid? (:typography-ref-file form))
(do
(vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)])
(assoc form :typography-ref-file file-id))
(uuid? (:component-file form))
(do
(vswap! state conj [(:component-file form) :components (:component-id form)])
(assoc form :component-file file-id))
:else
form))
(process-group-of-assets [data [lib-id items]]
;; NOTE: there are a posibility that shape refers to a not
;; existing file because the file was removed. In this
;; case we just ignore the asset.
(if-let [lib (retrieve-file conn lib-id)]
(reduce (partial process-asset lib) data items)
data))
(process-asset [lib data [bucket asset-id]]
(let [asset (get-in lib [:data bucket asset-id])
;; Add a special case for colors that need to have
;; correctly set the :file-id prop (pending of the
;; refactor that will remove it).
asset (cond-> asset
(= bucket :colors) (assoc :file-id file-id))]
(update data bucket assoc asset-id asset)))]
(let [assets (volatile! [])]
(walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data)
(->> (deref assets)
(filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id))))
(d/group-by first rest)
(reduce (partial process-group-of-assets) data)))))
(defmulti write-export ::version)
(defmulti write-section ::section)
(s/def ::output io/output-stream?)
(s/def ::file-ids (s/every ::us/uuid :kind vector? :min-count 1))
(s/def ::include-libraries? (s/nilable ::us/boolean))
(s/def ::embed-assets? (s/nilable ::us/boolean))
(s/def ::write-export-options
(s/keys :req-un [::db/pool ::sto/storage]
:req [::output ::file-ids]
:opt [::include-libraries? ::embed-assets?]))
(defn write-export!
"Do the exportation of a speficied file in custom penpot binary
format. There are some options available for customize the output:
`::include-libraries?`: additionaly to the specified file, all the
linked libraries also will be included (including transitive
dependencies).
`::embed-assets?`: instead of including the libraryes, embedd in the
same file library all assets used from external libraries."
[{:keys [::include-libraries? ::embed-assets?] :as options}]
(us/assert! ::write-export-options options)
(us/verify!
:expr (not (and include-libraries? embed-assets?))
:hint "the `include-libraries?` and `embed-assets?` are mutally excluding options")
(write-export options))
(defmethod write-export :default
[{:keys [::output] :as options}]
(write-header! output :v1)
(with-open [output (zstd-output-stream output :level 12)]
(with-open [output (io/data-output-stream output)]
(binding [*state* (volatile! {})]
(run! (fn [section]
(l/debug :hint "write section" :section section ::l/async false)
(write-label! output section)
(let [options (-> options
(assoc ::output output)
(assoc ::section section))]
(binding [*options* options]
(write-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
(defmethod write-section :v1/metadata
[{:keys [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)))
(defmethod write-section :v1/files
[{:keys [pool ::output ::embed-assets?]}]
;; Initialize SIDS with empty vector
(vswap! *state* assoc :sids [])
(doseq [file-id (-> *state* deref :files)]
(let [file (cond-> (retrieve-file pool file-id)
embed-assets?
(update :data embed-file-assets pool file-id))
media (retrieve-file-media pool file)]
(l/debug :hint "write penpot file"
:id file-id
:media (count media)
::l/async false)
(doto output
(write-obj! file)
(write-obj! media))
(vswap! *state* update :sids into storage-object-id-xf media))))
(defmethod write-section :v1/rels
[{:keys [pool ::output ::include-libraries?]}]
(let [rels (when include-libraries?
(retrieve-library-relations pool (-> *state* deref :files)))]
(l/debug :hint "found rels" :total (count rels) ::l/async false)
(write-obj! output rels)))
(defmethod write-section :v1/sobjects
[{:keys [storage ::output]}]
(let [sids (-> *state* deref :sids)
storage (media/configure-assets-storage storage)]
(l/debug :hint "found sobjects"
:items (count sids)
::l/async false)
;; Write all collected storage objects
(write-obj! output sids)
(doseq [id sids]
(let [{:keys [size] :as obj} @(sto/get-object storage id)]
(l/debug :hint "write sobject" :id id ::l/async false)
(doto output
(write-uuid! id)
(write-obj! (meta 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
:code :mismatch-readed-size
:hint (str/ffmt "found unexpected object size; size=% written=%" size written)))))))))
;; --- EXPORT READER
(declare lookup-index)
(declare update-index)
(declare relink-media)
(declare relink-shapes)
(defmulti read-import ::version)
(defmulti read-section ::section)
(s/def ::project-id ::us/uuid)
(s/def ::input io/input-stream?)
(s/def ::overwrite? (s/nilable ::us/boolean))
(s/def ::migrate? (s/nilable ::us/boolean))
(s/def ::ignore-index-errors? (s/nilable ::us/boolean))
(s/def ::read-import-options
(s/keys :req-un [::db/pool ::sto/storage]
:req [::project-id ::input]
:opt [::overwrite? ::migrate? ::ignore-index-errors?]))
(defn read-import!
"Do the importation of the specified resource in penpot custom binary
format. There are some options for customize the importation
behavior:
`::overwrite?`: if true, instead of creating new files and remaping id references,
it reuses all ids and updates existing objects; defaults to `false`.
`::migrate?`: if true, applies the migration before persisting the
file data; defaults to `false`.
`::ignore-index-errors?`: if true, do not fail on index lookup errors, can
happen with broken files; defaults to: `false`.
"
[{:keys [::input ::timestamp] :or {timestamp (dt/now)} :as options}]
(us/verify! ::read-import-options options)
(let [version (read-header! input)]
(read-import (assoc options ::version version ::timestamp timestamp))))
(defmethod read-import :v1
[{:keys [pool ::input] :as options}]
(with-open [input (zstd-input-stream input)]
(with-open [input (io/data-input-stream input)]
(db/with-atomic [conn pool]
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED;"])
(binding [*state* (volatile! {:media [] :index {}})]
(run! (fn [section]
(l/debug :hint "reading section" :section section ::l/async false)
(assert-read-label! input section)
(let [options (-> options
(assoc ::section section)
(assoc ::input input)
(assoc :conn conn))]
(binding [*options* options]
(read-section options))))
[:v1/metadata :v1/files :v1/rels :v1/sobjects])
;; Knowing that the ids of the created files are in
;; index, just lookup them and return it as a set
(let [files (-> *state* deref :files)]
(into #{} (keep #(get-in @*state* [:index %])) files)))))))
(defmethod read-section :v1/metadata
[{:keys [::input]}]
(let [{:keys [version files]} (read-obj! input)]
(l/debug :hint "metadata readed" :version (:full version) :files files ::l/async false)
(vswap! *state* update :index update-index files)
(vswap! *state* assoc :version version :files files)))
(defmethod read-section :v1/files
[{:keys [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)]
(when (not= file-id expected-file-id)
(ex/raise :type :validation
:code :inconsistent-penpot-file
:hint "the penpot file seems corrupt, found unexpected uuid (file-id)"))
;; Update index using with media
(l/debug :hint "update index with media" ::l/async false)
(vswap! *state* update :index update-index (map :id media'))
;; Store file media for later insertion
(l/debug :hint "update media references" ::l/async false)
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
(l/debug :hint "procesing file" :file-id file-id ::l/async false)
(let [file-id' (lookup-index file-id)
data (-> (:data file)
(assoc :id file-id')
(cond-> migrate? (pmg/migrate-data))
(update :pages-index relink-shapes)
(update :components relink-shapes)
(update :media relink-media))
params {:id file-id'
:project-id project-id
:name (:name file)
:revn (:revn file)
:is-shared (:is-shared file)
:data (blob/encode data)
:created-at timestamp
:modified-at timestamp}]
(l/debug :hint "create file" :id file-id' ::l/async false)
(if overwrite?
(create-or-update-file conn params)
(db/insert! conn :file params))
(when overwrite?
(db/delete! conn :file-thumbnail {:file-id file-id'}))))))
(defmethod read-section :v1/rels
[{:keys [conn ::input ::timestamp]}]
(let [rels (read-obj! input)]
;; Insert all file relations
(doseq [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/async false)
(db/insert! conn :file-library-rel rel)))))
(defmethod read-section :v1/sobjects
[{:keys [storage conn ::input ::overwrite?]}]
(let [storage (media/configure-assets-storage storage)
ids (read-obj! input)]
(doseq [expected-storage-id ids]
(let [id (read-uuid! input)
mdata (read-obj! input)]
(when (not= id expected-storage-id)
(ex/raise :type :validation
:code :inconsistent-penpot-file
:hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)"))
(l/debug :hint "readed storage object" :id id ::l/async false)
(let [[size resource] (read-stream! input)
hash (sto/calculate-hash resource)
content (-> (sto/content resource size)
(sto/wrap-with-hash hash))
params (-> mdata
(assoc ::sto/deduplicate? true)
(assoc ::sto/content content)
(assoc ::sto/touched-at (dt/now))
(assoc :bucket "file-media-object"))
sobject @(sto/put-object! storage params)]
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/async false)
(vswap! *state* update :index assoc id (:id sobject)))))
(doseq [item (:media @*state*)]
(l/debug :hint "inserting file media object"
:id (:id item)
:file-id (:file-id item)
::l/async false)
(let [file-id (lookup-index (:file-id item))]
(if (= file-id (:file-id item))
(l/warn :hint "ignoring file media object" :file-id (:file-id item) ::l/async false)
(db/insert! conn :file-media-object
(-> item
(assoc :file-id file-id)
(d/update-when :media-id lookup-index)
(d/update-when :thumbnail-id lookup-index))
{:on-conflict-do-nothing overwrite?}))))))
(defn- lookup-index
[id]
(let [val (get-in @*state* [:index id])]
(l/trace :fn "lookup-index" :id id :val val ::l/async false)
(when (and (not (::ignore-index-errors? *options*)) (not val))
(ex/raise :type :validation
:code :incomplete-index
:hint "looks like index has missing data"))
(or val id)))
(defn- update-index
[index coll]
(loop [items (seq coll)
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/async false)
(recur (rest items)
(assoc index id new-id)))
index)))
(defn- relink-shapes
"A function responsible to analyze all file data and
replace the old :component-file reference with the new
ones, using the provided file-index."
[data]
(letfn [(process-map-form [form]
(cond-> form
;; Relink image shapes
(and (map? (:metadata form))
(= :image (:type form)))
(update-in [:metadata :id] lookup-index)
;; Relink paths with fill image
(and (map? (:fill-image form))
(= :path (:type form)))
(update-in [:fill-image :id] lookup-index)
;; This covers old shapes and the new :fills.
(uuid? (:fill-color-ref-file form))
(update :fill-color-ref-file lookup-index)
;; This covers the old shapes and the new :strokes
(uuid? (:storage-color-ref-file form))
(update :stroke-color-ref-file lookup-index)
;; This covers all text shapes that have typography referenced
(uuid? (:typography-ref-file form))
(update :typography-ref-file lookup-index)
;; This covers the component instance links
(uuid? (:component-file form))
(update :component-file lookup-index)
;; This covers the shadows and grids (they have directly
;; the :file-id prop)
(uuid? (:file-id form))
(update :file-id lookup-index)))]
(walk/postwalk (fn [form]
(if (map? form)
(try
(process-map-form form)
(catch Throwable cause
(l/warn :hint "failed form" :form (pr-str form) ::l/async false)
(throw cause)))
form))
data)))
(defn- relink-media
"A function responsible of process the :media attr of file data and
remap the old ids with the new ones."
[media]
(reduce-kv (fn [res k v]
(let [id (lookup-index k)]
(if (uuid? id)
(-> res
(assoc id (assoc v :id id))
(dissoc k))
res)))
media
media))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn export!
[cfg output]
(let [id (uuid/next)
tp (dt/tpoint)
ab (volatile! false)
cs (volatile! nil)]
(try
(l/info :hint "start exportation" :export-id id)
(with-open [output (io/output-stream output)]
(binding [*position* (atom 0)]
(write-export! (assoc cfg ::output output))))
(catch java.io.IOException _cause
;; Do nothing, EOF means client closes connection abruptly
(vreset! ab true)
nil)
(catch Throwable cause
(vreset! cs cause)
(vreset! ab true)
(throw cause))
(finally
(l/info :hint "exportation finished" :export-id id
:elapsed (str (inst-ms (tp)) "ms")
:aborted @ab
:cause @cs)))))
(defn export-to-tmpfile!
[cfg]
(let [path (tmp/tempfile :prefix "penpot.export.")]
(with-open [output (io/output-stream path)]
(export! cfg output)
path)))
(defn import!
[{:keys [::input] :as cfg}]
(let [id (uuid/next)
ts (dt/now)
cs (volatile! nil)]
(try
(l/info :hint "start importation" :import-id id)
(binding [*position* (atom 0)]
(with-open [input (io/input-stream input)]
(read-import! (assoc cfg ::input input))))
(catch Throwable cause
(vreset! cs cause)
(throw cause))
(finally
(l/info :hint "importation finished" :import-id id
:elapsed (str (inst-ms (dt/diff ts (dt/now))) "ms")
:error? (some? @cs)
:cause @cs)))))
;; --- Command: export-binfile
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::include-libraries? ::us/boolean)
(s/def ::embed-assets? ::us/boolean)
(s/def ::export-binfile
(s/keys :req-un [::profile-id ::file-id ::include-libraries? ::embed-assets?]))
(sv/defmethod ::export-binfile
"Export a penpot file in a binary format."
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id include-libraries? embed-assets?] :as params}]
(files/check-read-permissions! pool profile-id file-id)
(let [resp (reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream]
(-> cfg
(assoc ::file-ids [file-id])
(assoc ::embed-assets? embed-assets?)
(assoc ::include-libraries? include-libraries?)
(export! output-stream))))]
(with-meta (sv/wrap nil)
{:transform-response (fn [_ response]
(-> response
(assoc :body resp)
(assoc :headers {"content-type" "application/octet-stream"})))})))
(s/def ::file ::media/upload)
(s/def ::import-binfile
(s/keys :req-un [::profile-id ::project-id ::file]))
(sv/defmethod ::import-binfile
"Import a penpot file in a binary format."
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id project-id file] :as params}]
(db/with-atomic [conn pool]
(projects/check-read-permissions! conn profile-id project-id)
(import! (assoc cfg
::input (:path file)
::project-id project-id
::ignore-index-errors? true))))

View File

@@ -0,0 +1,532 @@
;; 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.comments
(:require
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.db :as db]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.files :as files]
[app.rpc.queries.teams :as teams]
[app.rpc.retry :as retry]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn decode-row
[{:keys [participants position] :as row}]
(cond-> row
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
;; --- COMMAND: Get Comment Threads
(declare retrieve-comment-threads)
(s/def ::team-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-comment-threads
(s/and (s/keys :req-un [::profile-id]
:opt-un [::file-id ::share-id ::team-id])
#(or (:file-id %) (:team-id %))))
(sv/defmethod ::get-comment-threads
[{:keys [pool] :as cfg} params]
(with-open [conn (db/open pool)]
(retrieve-comment-threads conn params)))
(def sql:comment-threads
"select distinct on (ct.id)
ct.*,
f.name as file_name,
f.project_id as project_id,
first_value(c.content) over w as content,
(select count(1)
from comment as c
where c.thread_id = ct.id) as count_comments,
(select count(1)
from comment as c
where c.thread_id = ct.id
and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
from comment_thread as ct
inner join comment as c on (c.thread_id = ct.id)
inner join file as f on (f.id = ct.file_id)
left join comment_thread_status as cts
on (cts.thread_id = ct.id and
cts.profile_id = ?)
where ct.file_id = ?
window w as (partition by c.thread_id order by c.created_at asc)")
(defn retrieve-comment-threads
[conn {:keys [profile-id file-id share-id]}]
(files/check-comment-permissions! conn profile-id file-id share-id)
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
(into [] (map decode-row))))
;; --- COMMAND: Get Unread Comment Threads
(declare retrieve-unread-comment-threads)
(s/def ::team-id ::us/uuid)
(s/def ::get-unread-comment-threads
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::get-unread-comment-threads
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(retrieve-unread-comment-threads conn params)))
(def sql:comment-threads-by-team
"select distinct on (ct.id)
ct.*,
f.name as file_name,
f.project_id as project_id,
first_value(c.content) over w as content,
(select count(1)
from comment as c
where c.thread_id = ct.id) as count_comments,
(select count(1)
from comment as c
where c.thread_id = ct.id
and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments
from comment_thread as ct
inner join comment as c on (c.thread_id = ct.id)
inner join file as f on (f.id = ct.file_id)
inner join project as p on (p.id = f.project_id)
left join comment_thread_status as cts
on (cts.thread_id = ct.id and
cts.profile_id = ?)
where p.team_id = ?
window w as (partition by c.thread_id order by c.created_at asc)")
(def sql:unread-comment-threads-by-team
(str "with threads as (" sql:comment-threads-by-team ")"
"select * from threads where count_unread_comments > 0"))
(defn retrieve-unread-comment-threads
[conn {:keys [profile-id team-id]}]
(->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
(into [] (map decode-row))))
;; --- COMMAND: Get Single Comment Thread
(s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-comment-thread
(s/keys :req-un [::profile-id ::file-id ::id]
:opt-un [::share-id]))
(sv/defmethod ::get-comment-thread
[{:keys [pool] :as cfg} {:keys [profile-id file-id id share-id] :as params}]
(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 = ?")]
(-> (db/exec-one! conn [sql profile-id file-id id])
(decode-row)))))
(defn get-comment-thread
[conn {:keys [profile-id file-id id] :as params}]
(let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")]
(-> (db/exec-one! conn [sql profile-id file-id id])
(decode-row))))
;; --- COMMAND: Retrieve Comments
(declare get-comments)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::thread-id ::us/uuid)
(s/def ::get-comments
(s/keys :req-un [::profile-id ::thread-id]
:opt-un [::share-id]))
(sv/defmethod ::get-comments
[{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)]
(let [thread (db/get-by-id conn :comment-thread thread-id)]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id))
(get-comments conn thread-id)))
(def sql:comments
"select c.* from comment as c
where c.thread_id = ?
order by c.created_at asc")
(defn get-comments
[conn thread-id]
(->> (db/query conn :comment
{:thread-id thread-id}
{:order-by [[:created-at :asc]]})
(into [] (map decode-row))))
;; --- COMMAND: Get file comments users
(declare get-file-comments-users)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-profiles-for-file-comments
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::share-id]))
(sv/defmethod ::get-profiles-for-file-comments
"Retrieves a list of profiles with limited set of properties of all
participants on comment threads of the file."
{::doc/added "1.15"
::doc/changes ["1.15" "Imported from queries and renamed."]}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id]}]
(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)))
;; All the profiles that had comment the file, plus the current
;; profile.
(def sql:file-comment-users
"WITH available_profiles AS (
SELECT DISTINCT owner_id AS id
FROM comment
WHERE thread_id IN (SELECT id FROM comment_thread WHERE file_id=?)
)
SELECT p.id,
p.email,
p.fullname AS name,
p.fullname AS fullname,
p.photo_id,
p.is_active
FROM profile AS p
WHERE p.id IN (SELECT id FROM available_profiles) OR p.id=?")
(defn get-file-comments-users
[conn file-id profile-id]
(db/exec! conn [sql:file-comment-users file-id profile-id]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- COMMAND: Create Comment Thread
(declare upsert-comment-thread-status!)
(declare create-comment-thread)
(declare retrieve-page-name)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::profile-id ::us/uuid)
(s/def ::position ::gpt/point)
(s/def ::content ::us/string)
(s/def ::frame-id ::us/uuid)
(s/def ::create-comment-thread
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id ::frame-id]
:opt-un [::share-id]))
(sv/defmethod ::create-comment-thread
{::retry/max-retries 3
::retry/matches retry/conflict-db-insert?
::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(files/check-comment-permissions! conn profile-id file-id share-id)
(create-comment-thread conn params)))
(defn- retrieve-next-seqn
[conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
res (db/exec-one! conn [sql file-id])]
(:next-seqn res)))
(defn create-comment-thread
[conn {:keys [profile-id file-id page-id position content frame-id] :as params}]
(let [seqn (retrieve-next-seqn conn file-id)
now (dt/now)
pname (retrieve-page-name conn params)
thread (db/insert! conn :comment-thread
{:file-id file-id
:owner-id profile-id
:participants (db/tjson #{profile-id})
:page-name pname
:page-id page-id
:created-at now
:modified-at now
:seqn seqn
:position (db/pgpoint position)
:frame-id frame-id})]
;; Create a comment entry
(db/insert! conn :comment
{:thread-id (:id thread)
:owner-id profile-id
:created-at now
:modified-at now
:content content})
;; Make the current thread as read.
(upsert-comment-thread-status! conn profile-id (:id thread))
;; Optimistic update of current seq number on file.
(db/update! conn :file
{:comment-thread-seqn seqn}
{:id file-id})
(select-keys thread [:id :file-id :page-id])))
(defn- retrieve-page-name
[conn {:keys [file-id page-id]}]
(let [{:keys [data]} (db/get-by-id conn :file file-id)
data (blob/decode data)]
(get-in data [:pages-index page-id :name])))
;; --- COMMAND: Update Comment Thread Status
(s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::update-comment-thread-status
(s/keys :req-un [::profile-id ::id]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-status
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not cthr
(ex/raise :type :not-found))
(files/check-comment-permissions! conn profile-id (:file-id cthr) share-id)
(upsert-comment-thread-status! conn profile-id (:id cthr)))))
(def sql:upsert-comment-thread-status
"insert into comment_thread_status (thread_id, profile_id)
values (?, ?)
on conflict (thread_id, profile_id)
do update set modified_at = clock_timestamp()
returning modified_at;")
(defn upsert-comment-thread-status!
[conn profile-id thread-id]
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id]))
;; --- COMMAND: Update Comment Thread
(s/def ::is-resolved ::us/boolean)
(s/def ::update-comment-thread
(s/keys :req-un [::profile-id ::id ::is-resolved]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not thread
(ex/raise :type :not-found))
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(db/update! conn :comment-thread
{:is-resolved is-resolved}
{:id id})
nil)))
;; --- COMMAND: Add Comment
(declare create-comment)
(s/def ::create-comment
(s/keys :req-un [::profile-id ::thread-id ::content]
:opt-un [::share-id]))
(sv/defmethod ::create-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(create-comment conn params)))
(defn create-comment
[conn {:keys [profile-id thread-id content share-id] :as params}]
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
(decode-row))
pname (retrieve-page-name conn thread)]
;; Standard Checks
(when-not thread (ex/raise :type :not-found))
;; Permission Checks
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
;; Update the page-name cachedattribute on comment thread table.
(when (not= pname (:page-name thread))
(db/update! conn :comment-thread
{:page-name pname}
{:id thread-id}))
;; NOTE: is important that all timestamptz related fields are
;; created or updated on the database level for avoid clock
;; inconsistencies (some user sees something read that is not
;; read, etc...)
(let [ppants (:participants thread #{})
comment (db/insert! conn :comment
{:thread-id thread-id
:owner-id profile-id
:content content})]
;; NOTE: this is done in SQL instead of using db/update!
;; helper because currently the helper does not allow pass raw
;; function call parameters to the underlying prepared
;; statement; in a future when we fix/improve it, this can be
;; changed to use the helper.
;; Update thread modified-at attribute and assoc the current
;; profile to the participant set.
(let [ppants (conj ppants profile-id)
sql "update comment_thread
set modified_at = clock_timestamp(),
participants = ?
where id = ?"]
(db/exec-one! conn [sql (db/tjson ppants) thread-id]))
;; Update the current profile status in relation to the
;; current thread.
(upsert-comment-thread-status! conn profile-id thread-id)
;; Return the created comment object.
comment)))
;; --- COMMAND: Update Comment
(declare update-comment)
(s/def ::update-comment
(s/keys :req-un [::profile-id ::id ::content]
:opt-un [::share-id]))
(sv/defmethod ::update-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(update-comment conn params)))
(defn update-comment
[conn {:keys [profile-id id content share-id] :as params}]
(let [comment (db/get-by-id conn :comment id {:for-update true})
_ (when-not comment (ex/raise :type :not-found))
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
_ (when-not thread (ex/raise :type :not-found))
pname (retrieve-page-name conn thread)]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
;; Don't allow edit comments to not owners
(when-not (= (:owner-id thread) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/update! conn :comment
{:content content
:modified-at (dt/now)}
{:id (:id comment)})
(db/update! conn :comment-thread
{:modified-at (dt/now)
:page-name pname}
{:id (:id thread)})
nil))
;; --- COMMAND: Delete Comment Thread
(s/def ::delete-comment-thread
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not (= (:owner-id thread) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/delete! conn :comment-thread {:id id})
nil)))
;; --- COMMAND: Delete comment
(s/def ::delete-comment
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})]
(when-not (= (:owner-id comment) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/delete! conn :comment {:id id}))))
;; --- COMMAND: Update comment thread position
(s/def ::update-comment-thread-position
(s/keys :req-un [::profile-id ::id ::position ::frame-id]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-position
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id position frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(db/update! conn :comment-thread
{:modified-at (dt/now)
:position (db/pgpoint position)
:frame-id frame-id}
{:id (:id thread)})
nil)))
;; --- COMMAND: Update comment frame
(s/def ::update-comment-thread-frame
(s/keys :req-un [::profile-id ::id ::frame-id]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-frame
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(db/update! conn :comment-thread
{:modified-at (dt/now)
:frame-id frame-id}
{:id (:id thread)})
nil)))

View File

@@ -2,9 +2,9 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.rpc.mutations.demo
(ns app.rpc.commands.demo
"A demo specific mutations."
(:require
[app.common.exceptions :as ex]
@@ -12,7 +12,8 @@
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.mutations.profile :as profile]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.core.codecs :as bc]
@@ -21,7 +22,13 @@
(s/def ::create-demo-profile any?)
(sv/defmethod ::create-demo-profile {:auth false}
(sv/defmethod ::create-demo-profile
"A command that is responsible of creating a demo purpose
profile. It only works if the `demo-users` flag is inabled in the
configuration."
{:auth false
::doc/added "1.15"
::doc/changes ["1.15" "This methos is migrated from mutations to commands."]}
[{:keys [pool] :as cfg} _]
(let [id (uuid/next)
sem (System/currentTimeMillis)
@@ -45,8 +52,8 @@
:hint "Demo users are disabled by config."))
(db/with-atomic [conn pool]
(->> (#'profile/create-profile conn params)
(#'profile/create-profile-relations conn))
(->> (cmd.auth/create-profile conn params)
(cmd.auth/create-profile-relations conn))
(with-meta {:email email
:password password}

View File

@@ -0,0 +1,50 @@
;; 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
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.files :as files]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- Query: File Libraries used by a File
(declare retrieve-has-file-libraries)
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::has-file-libraries
(s/keys :req-un [::profile-id ::file-id]))
(sv/defmethod ::has-file-libraries
"Checks if the file has libraries. Returns a boolean"
{::doc/added "1.15.1"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(with-open [conn (db/open pool)]
(files/check-read-permissions! pool profile-id file-id)
(retrieve-has-file-libraries conn params)))
(def ^:private sql:has-file-libraries
"SELECT COUNT(*) > 0 AS has_libraries
FROM file_library_rel AS flr
JOIN file AS fl ON (flr.library_file_id = fl.id)
WHERE flr.file_id = ?::uuid
AND (fl.deleted_at IS NULL OR
fl.deleted_at > now())")
(defn- retrieve-has-file-libraries
[conn {:keys [file-id]}]
(let [row (db/exec-one! conn [sql:has-file-libraries file-id])]
(:has-libraries row)))

View File

@@ -0,0 +1,85 @@
;; 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.ldap
(:require
[app.auth.ldap :as ldap]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- COMMAND: login-with-ldap
(declare login-or-register)
(s/def ::email ::us/email)
(s/def ::password ::us/string)
(s/def ::invitation-token ::us/string)
(s/def ::login-with-ldap
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-ldap
"Performs the authentication using LDAP backend. Only works if LDAP
is properly configured and enabled with `login-with-ldap` flag."
{:auth false
::doc/added "1.15"}
[{:keys [session tokens ldap] :as cfg} params]
(when-not ldap
(ex/raise :type :restriction
:code :ldap-not-initialized
:hide "ldap auth provider is not initialized"))
(let [info (ldap/authenticate ldap params)]
(when-not info
(ex/raise :type :validation
:code :wrong-credentials))
(let [profile (login-or-register cfg info)]
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(if-let [token (:invitation-token params)]
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged).
(let [claims (tokens :verify {:token token :iss :team-invitation})
claims (assoc claims
:member-id (:id profile)
:member-email (:email profile))
token (tokens :generate claims)]
(with-meta {:invitation-token token}
{:transform-response ((:create session) (:id profile))
::audit/props (:props profile)
::audit/profile-id (:id profile)}))
(with-meta profile
{:transform-response ((:create session) (:id profile))
::audit/props (:props profile)
::audit/profile-id (:id profile)})))))
(defn- login-or-register
[{:keys [pool] :as cfg} info]
(db/with-atomic [conn pool]
(or (some->> (:email info)
(profile/retrieve-profile-data-by-email conn)
(profile/populate-additional-data conn)
(profile/decode-profile-row))
(->> (assoc info :is-active true :is-demo false)
(cmd.auth/create-profile conn)
(cmd.auth/create-profile-relations conn)
(profile/strip-private-attrs)))))

View File

@@ -0,0 +1,403 @@
;; 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.management
"A collection of RPC methods for manage the files, projects and team organization."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc.commands.binfile :as binfile]
[app.rpc.doc :as-alias doc]
[app.rpc.mutations.projects :refer [create-project-role create-project]]
[app.rpc.queries.projects :as proj]
[app.rpc.queries.teams :as teams]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.walk :as walk]))
;; --- COMMAND: Duplicate File
(declare duplicate-file)
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::duplicate-file
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::name]))
(sv/defmethod ::duplicate-file
"Duplicate a single file in the same team."
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(duplicate-file conn params)))
(defn- remap-id
[item index key]
(cond-> item
(contains? item key)
(assoc key (get index (get item key) (get item key)))))
(defn- process-file
[file index]
(letfn [(process-form [form]
(cond-> form
;; Relink library items
(and (map? form)
(uuid? (:component-file form)))
(update :component-file #(get index % %))
(and (map? form)
(uuid? (:fill-color-ref-file form)))
(update :fill-color-ref-file #(get index % %))
(and (map? form)
(uuid? (:stroke-color-ref-file form)))
(update :stroke-color-ref-file #(get index % %))
(and (map? form)
(uuid? (:typography-ref-file form)))
(update :typography-ref-file #(get index % %))
;; Relink Image Shapes
(and (map? form)
(map? (:metadata form))
(= :image (:type form)))
(update-in [:metadata :id] #(get index % %))))
;; A function responsible to analyze all file data and
;; replace the old :component-file reference with the new
;; ones, using the provided file-index
(relink-shapes [data]
(walk/postwalk process-form data))
;; A function responsible of process the :media attr of file
;; data and remap the old ids with the new ones.
(relink-media [media]
(reduce-kv (fn [res k v]
(let [id (get index k)]
(if (uuid? id)
(-> res
(assoc id (assoc v :id id))
(dissoc k))
res)))
media
media))]
(update file :data
(fn [data]
(-> data
(blob/decode)
(assoc :id (:id file))
(pmg/migrate-data)
(update :pages-index relink-shapes)
(update :components relink-shapes)
(update :media relink-media)
(d/without-nils)
(blob/encode))))))
(def sql:retrieve-used-libraries
"select flr.*
from file_library_rel as flr
inner join file as l on (flr.library_file_id = l.id)
where flr.file_id = ?
and l.deleted_at is null")
(def sql:retrieve-used-media-objects
"select fmo.*
from file_media_object as fmo
inner join storage_object as so on (fmo.media_id = so.id)
where fmo.file_id = ?
and so.deleted_at is null")
(defn duplicate-file*
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}]
(let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
;; memo uniform creation/modification date
now (dt/now)
ignore (dt/plus now (dt/duration {:seconds 5}))
;; add to the index all file media objects.
index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds)
flibs-xf (comp
(map #(remap-id % index :file-id))
(map #(remap-id % index :library-file-id))
(map #(assoc % :synced-at now))
(map #(assoc % :created-at now)))
;; remap all file-library-rel row
flibs (sequence flibs-xf flibs)
fmeds-xf (comp
(map #(assoc % :id (get index (:id %))))
(map #(assoc % :created-at now))
(map #(remap-id % index :file-id)))
;; remap all file-media-object rows
fmeds (sequence fmeds-xf fmeds)
file (cond-> file
(some? project-id)
(assoc :project-id project-id)
(some? name)
(assoc :name name)
(true? reset-shared-flag)
(assoc :is-shared false))
file (-> file
(assoc :created-at now)
(assoc :modified-at now)
(assoc :ignore-sync-until ignore)
(update :id #(get index %))
(process-file index))]
(db/insert! conn :file file)
(db/insert! conn :file-profile-rel
{:file-id (:id file)
:profile-id profile-id
:is-owner true
:is-admin true
:can-edit true})
(doseq [params flibs]
(db/insert! conn :file-library-rel params))
(doseq [params fmeds]
(db/insert! conn :file-media-object params))
file))
(defn duplicate-file
[conn {:keys [profile-id file-id] :as params}]
(let [file (db/get-by-id conn :file file-id)
index {file-id (uuid/next)}
params (assoc params :index index :file file)]
(proj/check-edition-permissions! conn profile-id (:project-id file))
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(-> (duplicate-file* conn params {:reset-shared-flag true})
(update :data blob/decode))))
;; --- COMMAND: Duplicate Project
(declare duplicate-project)
(s/def ::duplicate-project
(s/keys :req-un [::profile-id ::project-id]
:opt-un [::name]))
(sv/defmethod ::duplicate-project
"Duplicate an entire project with all the files"
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(duplicate-project conn params)))
(defn duplicate-project
[conn {:keys [profile-id project-id name] :as params}]
;; Defer all constraints
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(let [project (db/get-by-id conn :project project-id)
files (db/query conn :file
{:project-id (:id project)
:deleted-at nil}
{:columns [:id]})
project (cond-> project
(string? name)
(assoc :name name)
:always
(assoc :id (uuid/next)))]
;; Check if the source team-id allow creating new project for current user
(teams/check-edition-permissions! conn profile-id (:team-id project))
;; create the duplicated project and assign the current profile as
;; a project owner
(create-project conn project)
(create-project-role conn {:project-id (:id project)
:profile-id profile-id
:role :owner})
;; duplicate all files
(let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files)
params (-> params
(dissoc :name)
(assoc :project-id (:id project))
(assoc :index index))]
(doseq [{:keys [id]} files]
(let [file (db/get-by-id conn :file id)
params (assoc params :file file)
opts {:reset-shared-flag false}]
(duplicate-file* conn params opts))))
;; return the created project
project))
;; --- COMMAND: Move file
(def sql:retrieve-files
"select id, project_id from file where id = ANY(?)")
(def sql:move-files
"update file set project_id = ? where id = ANY(?)")
(def sql:delete-broken-relations
"with broken as (
(select * from file_library_rel as flr
inner join file as f on (flr.file_id = f.id)
inner join project as p on (f.project_id = p.id)
inner join file as lf on (flr.library_file_id = lf.id)
inner join project as lp on (lf.project_id = lp.id)
where p.id = ANY(?)
and lp.team_id != p.team_id)
)
delete from file_library_rel as rel
using broken as br
where rel.file_id = br.file_id
and rel.library_file_id = br.library_file_id")
(defn move-files
[conn {:keys [profile-id ids project-id] :as params}]
(let [fids (db/create-array conn "uuid" ids)
files (db/exec! conn [sql:retrieve-files fids])
source (into #{} (map :project-id) files)
pids (->> (conj source project-id)
(db/create-array conn "uuid"))]
;; Check if we have permissions on the destination project
(proj/check-edition-permissions! conn profile-id project-id)
;; Check if we have permissions on all source projects
(doseq [project-id source]
(proj/check-edition-permissions! conn profile-id project-id))
(when (contains? source project-id)
(ex/raise :type :validation
:code :cant-move-to-same-project
:hint "Unable to move a file to the same project"))
;; move all files to the project
(db/exec-one! conn [sql:move-files project-id fids])
;; delete possible broken relations on moved files
(db/exec-one! conn [sql:delete-broken-relations pids])
nil))
(s/def ::ids (s/every ::us/uuid :kind set?))
(s/def ::move-files
(s/keys :req-un [::profile-id ::ids ::project-id]))
(sv/defmethod ::move-files
"Move a set of files from one project to other."
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(move-files conn params)))
;; --- COMMAND: Move project
(defn move-project
[conn {:keys [profile-id team-id project-id] :as params}]
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
(map :id)
(db/create-array conn "uuid"))]
(teams/check-edition-permissions! conn profile-id (:team-id project))
(teams/check-edition-permissions! conn profile-id team-id)
(when (= team-id (:team-id project))
(ex/raise :type :validation
:code :cant-move-to-same-team
:hint "Unable to move a project to same team"))
;; move project to the destination team
(db/update! conn :project
{:team-id team-id}
{:id project-id})
;; delete possible broken relations on moved files
(db/exec-one! conn [sql:delete-broken-relations pids])
nil))
(s/def ::move-project
(s/keys :req-un [::profile-id ::team-id ::project-id]))
(sv/defmethod ::move-project
"Move projects between teams."
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(move-project conn params)))
;; --- COMMAND: Clone Template
(declare clone-template)
(s/def ::template-id ::us/not-empty-string)
(s/def ::clone-template
(s/keys :req-un [::profile-id ::project-id ::template-id]))
(sv/defmethod ::clone-template
"Clone into the specified project the template by its id."
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(clone-template params))))
(defn- clone-template
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
(let [template (d/seek #(= (:id %) template-id) templates)
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"))
(-> cfg
(assoc ::binfile/input (:path template))
(assoc ::binfile/project-id (:id project))
(assoc ::binfile/ignore-index-errors? true)
(assoc ::binfile/migrate? true)
(binfile/import!))))
;; --- COMMAND: Retrieve list of builtin templates
(s/def ::retrieve-list-of-builtin-templates any?)
(sv/defmethod ::retrieve-list-of-builtin-templates
[cfg _params]
(mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg)))

View File

@@ -0,0 +1,193 @@
;; 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.verify-token
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.doc :as-alias doc]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(s/def ::iss keyword?)
(s/def ::exp ::us/inst)
(defmulti process-token (fn [_ _ claims] (:iss claims)))
(s/def ::verify-token
(s/keys :req-un [::token]
:opt-un [::profile-id]))
(sv/defmethod ::verify-token
{:auth false
::doc/added "1.15"}
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify sprops {:token token})
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))
(defmethod process-token :change-email
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
(when (profile/retrieve-profile-data-by-email conn email)
(ex/raise :type :validation
:code :email-already-exists))
(db/update! conn :profile
{:email email}
{:id profile-id})
(with-meta claims
{::audit/name "update-profile-email"
::audit/props {:email email}
::audit/profile-id profile-id}))
(defmethod process-token :verify-email
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)
claims (assoc claims :profile profile)]
(when-not (:is-active profile)
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)}))
(with-meta claims
{:transform-response ((:create session) profile-id)
::audit/name "verify-profile-email"
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
(let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge
{:team-id team-id
:profile-id (:id member)}
(teams/role->params role))]
;; Do not allow blocked users accept invitations.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
;; If profile is not yet verified, mark it as verified because
;; accepting an invitation link serves as verification.
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id (:id member)}))
;; Delete the invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to member-email})
(assoc member :is-active true)))
(s/def ::spec.team-invitation/profile-id ::us/uuid)
(s/def ::spec.team-invitation/role ::us/keyword)
(s/def ::spec.team-invitation/team-id ::us/uuid)
(s/def ::spec.team-invitation/member-email ::us/email)
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
(s/def ::team-invitation-claims
(s/keys :req-un [::iss ::exp
::spec.team-invitation/profile-id
::spec.team-invitation/role
::spec.team-invitation/team-id
::spec.team-invitation/member-email]
:opt-un [::spec.team-invitation/member-id]))
(defmethod process-token :team-invitation
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
{:keys [member-id team-id member-email] :as claims}]
(us/assert ::team-invitation-claims claims)
(let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email})
profile (db/get* conn :profile
{:id profile-id}
{:columns [:id :email]})]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token
:hint "no invitation associated with the token"))
(if (some? profile)
(if (or (= member-id profile-id)
(= member-email (:email profile)))
;; if we have logged-in user and it matches the invitation we
;; proceed with accepting the invitation and joining the
;; current profile to the invited team.
(let [profile (accept-invitation cfg claims invitation profile)]
(with-meta
(assoc claims :state :created)
{::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id profile-id}))
(ex/raise :type :validation
:code :invalid-token
:hint "logged-in user does not matches the invitation"))
;; If we have not logged-in user, we try find the invited
;; profile by member-id or member-email props of the invitation
;; token; If profile is found, we accept the invitation and
;; leave the user logged-in.
(if-let [member (db/get* conn :profile
(if member-id
{:id member-id}
{:email member-email})
{:columns [:id :email]})]
(let [profile (accept-invitation cfg claims invitation member)]
(with-meta
(assoc claims :state :created)
{:transform-response ((:create session) (:id profile))
::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-register
:state :pending}))))
;; --- Default
(defmethod process-token :default
[_ _ _]
(ex/raise :type :validation
:code :invalid-token))

View File

@@ -0,0 +1,77 @@
;; 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.doc
"API autogenerated documentation."
(:require
[app.common.data :as d]
[app.config :as cf]
[app.rpc :as-alias rpc]
[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]
[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}})))
(defn- prepare-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)
: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))}))]
{: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))
(sort-by (juxt :module :name)))}))
(defn- handler
[methods]
(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)))))
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req-un [::rpc/methods]))
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
["/_doc" {:handler (handler methods)
:allowed-methods #{:get}}])

View File

@@ -2,7 +2,7 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.rpc.helpers
"General purpose RPC helpers."

View File

@@ -2,137 +2,66 @@
;; 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) UXBOX Labs SL
;; Copyright (c) KALEIDOS INC
(ns app.rpc.mutations.comments
(:require
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.db :as db]
[app.rpc.queries.comments :as comments]
[app.rpc.commands.comments :as cmd.comments]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.files :as files]
[app.rpc.retry :as retry]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
;; --- Mutation: Create Comment Thread
(declare upsert-comment-thread-status!)
(declare create-comment-thread)
(declare retrieve-page-name)
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::position ::gpt/point)
(s/def ::content ::us/string)
(s/def ::create-comment-thread
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
(s/def ::create-comment-thread ::cmd.comments/create-comment-thread)
(sv/defmethod ::create-comment-thread
{::retry/max-retries 3
::retry/matches retry/conflict-db-insert?}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
::retry/matches retry/conflict-db-insert?
::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(files/check-read-permissions! conn profile-id file-id)
(create-comment-thread conn params)))
(defn- retrieve-next-seqn
[conn file-id]
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
res (db/exec-one! conn [sql file-id])]
(:next-seqn res)))
(defn- create-comment-thread
[conn {:keys [profile-id file-id page-id position content] :as params}]
(let [seqn (retrieve-next-seqn conn file-id)
now (dt/now)
pname (retrieve-page-name conn params)
thread (db/insert! conn :comment-thread
{:file-id file-id
:owner-id profile-id
:participants (db/tjson #{profile-id})
:page-name pname
:page-id page-id
:created-at now
:modified-at now
:seqn seqn
:position (db/pgpoint position)})]
;; Create a comment entry
(db/insert! conn :comment
{:thread-id (:id thread)
:owner-id profile-id
:created-at now
:modified-at now
:content content})
;; Make the current thread as read.
(upsert-comment-thread-status! conn profile-id (:id thread))
;; Optimistic update of current seq number on file.
(db/update! conn :file
{:comment-thread-seqn seqn}
{:id file-id})
(select-keys thread [:id :file-id :page-id])))
(defn- retrieve-page-name
[conn {:keys [file-id page-id]}]
(let [{:keys [data]} (db/get-by-id conn :file file-id)
data (blob/decode data)]
(get-in data [:pages-index page-id :name])))
(files/check-comment-permissions! conn profile-id file-id share-id)
(cmd.comments/create-comment-thread conn params)))
;; --- Mutation: Update Comment Thread Status
(s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::update-comment-thread-status
(s/keys :req-un [::profile-id ::id]))
(s/def ::update-comment-thread-status ::cmd.comments/update-comment-thread-status)
(sv/defmethod ::update-comment-thread-status
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not cthr
(ex/raise :type :not-found))
(files/check-read-permissions! conn profile-id (:file-id cthr))
(upsert-comment-thread-status! conn profile-id (:id cthr)))))
(def sql:upsert-comment-thread-status
"insert into comment_thread_status (thread_id, profile_id)
values (?, ?)
on conflict (thread_id, profile_id)
do update set modified_at = clock_timestamp()
returning modified_at;")
(defn- upsert-comment-thread-status!
[conn profile-id thread-id]
(db/exec-one! conn [sql:upsert-comment-thread-status thread-id profile-id]))
(when-not cthr (ex/raise :type :not-found))
(files/check-comment-permissions! conn profile-id (:file-id cthr) share-id)
(cmd.comments/upsert-comment-thread-status! conn profile-id (:id cthr)))))
;; --- Mutation: Update Comment Thread
(s/def ::is-resolved ::us/boolean)
(s/def ::update-comment-thread
(s/keys :req-un [::profile-id ::id ::is-resolved]))
(s/def ::update-comment-thread ::cmd.comments/update-comment-thread)
(sv/defmethod ::update-comment-thread
[{:keys [pool] :as cfg} {:keys [profile-id id is-resolved] :as params}]
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not thread
(ex/raise :type :not-found))
(files/check-read-permissions! conn profile-id (:file-id thread))
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(db/update! conn :comment-thread
{:is-resolved is-resolved}
{:id id})
@@ -141,121 +70,54 @@
;; --- Mutation: Add Comment
(s/def ::add-comment
(s/keys :req-un [::profile-id ::thread-id ::content]))
(s/def ::add-comment ::cmd.comments/create-comment)
(sv/defmethod ::add-comment
[{:keys [pool] :as cfg} {:keys [profile-id thread-id content] :as params}]
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
(comments/decode-row))
pname (retrieve-page-name conn thread)]
;; Standard Checks
(when-not thread (ex/raise :type :not-found))
;; Permission Checks
(files/check-read-permissions! conn profile-id (:file-id thread))
;; Update the page-name cachedattribute on comment thread table.
(when (not= pname (:page-name thread))
(db/update! conn :comment-thread
{:page-name pname}
{:id thread-id}))
;; NOTE: is important that all timestamptz related fields are
;; created or updated on the database level for avoid clock
;; inconsistencies (some user sees something read that is not
;; read, etc...)
(let [ppants (:participants thread #{})
comment (db/insert! conn :comment
{:thread-id thread-id
:owner-id profile-id
:content content})]
;; NOTE: this is done in SQL instead of using db/update!
;; helper because currently the helper does not allow pass raw
;; function call parameters to the underlying prepared
;; statement; in a future when we fix/improve it, this can be
;; changed to use the helper.
;; Update thread modified-at attribute and assoc the current
;; profile to the participant set.
(let [ppants (conj ppants profile-id)
sql "update comment_thread
set modified_at = clock_timestamp(),
participants = ?
where id = ?"]
(db/exec-one! conn [sql (db/tjson ppants) thread-id]))
;; Update the current profile status in relation to the
;; current thread.
(upsert-comment-thread-status! conn profile-id thread-id)
;; Return the created comment object.
comment))))
(cmd.comments/create-comment conn params)))
;; --- Mutation: Update Comment
(s/def ::update-comment
(s/keys :req-un [::profile-id ::id ::content]))
(s/def ::update-comment ::cmd.comments/update-comment)
(sv/defmethod ::update-comment
[{:keys [pool] :as cfg} {:keys [profile-id id content] :as params}]
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})
_ (when-not comment (ex/raise :type :not-found))
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
_ (when-not thread (ex/raise :type :not-found))
pname (retrieve-page-name conn thread)]
(files/check-read-permissions! conn profile-id (:file-id thread))
;; Don't allow edit comments to not owners
(when-not (= (:owner-id thread) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(db/update! conn :comment
{:content content
:modified-at (dt/now)}
{:id (:id comment)})
(db/update! conn :comment-thread
{:modified-at (dt/now)
:page-name pname}
{:id (:id thread)})
nil)))
(cmd.comments/update-comment conn params)))
;; --- Mutation: Delete Comment Thread
(s/def ::delete-comment-thread
(s/keys :req-un [::profile-id ::id]))
(s/def ::delete-comment-thread ::cmd.comments/delete-comment-thread)
(sv/defmethod ::delete-comment-thread
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not (= (:owner-id thread) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(ex/raise :type :validation :code :not-allowed))
(db/delete! conn :comment-thread {:id id})
nil)))
;; --- Mutation: Delete comment
(s/def ::delete-comment
(s/keys :req-un [::profile-id ::id]))
(s/def ::delete-comment ::cmd.comments/delete-comment)
(sv/defmethod ::delete-comment
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})]
(when-not (= (:owner-id comment) profile-id)
(ex/raise :type :validation
:code :not-allowed))
(ex/raise :type :validation :code :not-allowed))
(db/delete! conn :comment {:id id}))))

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