Compare commits

...

861 Commits

Author SHA1 Message Date
Andrés Moya
adc6af129c wip 2023-06-22 11:32:31 +02:00
Pablo Alba
0ae4988908 🐛 Fix Internal server error occurred after clicking on '3 dots' menu of copy component on Design tab 2023-06-22 10:08:26 +02:00
Alejandro Alonso
a53176489a 🐛 Fix extra line framing dashboard cards 2023-06-22 09:27:46 +02:00
Andrés Moya
d8121364ad 🐛 Fix touched on adding shapes to a component copy and undo 2023-06-22 09:27:27 +02:00
Alejandro Alonso
d4fe810813 🐛 Fix shared link broken 2023-06-22 08:01:15 +02:00
Alejandro Alonso
64ddfa0c31 📎 Update CHANGES.md file 2023-06-21 17:06:29 +02:00
Andrés Moya
e8dde477a5 🐛 Fix restore remote component 2023-06-21 17:04:46 +02:00
Alejandro Alonso
1b0848389c 📎 Update CHANGES.md file 2023-06-21 17:03:42 +02:00
Pablo Alba
4f02cc3e86 Merge pull request #3331 from penpot/hiru-restore-comp-missing-lib
🐛 Disallow restore component when the library has been detached
2023-06-21 16:46:16 +02:00
Andrés Moya
749d60be48 🐛 Disallow restore component when the library has been detached 2023-06-21 16:39:17 +02:00
Alejandro Alonso
bb8a523208 📎 Update CHANGES.md file 2023-06-21 12:52:13 +02:00
Alejandro Alonso
4d3e7f9a75 Merge remote-tracking branch 'origin/staging' into develop 2023-06-21 12:50:49 +02:00
Alejandro Alonso
2edbc10851 📎 Update CHANGES.md file 2023-06-21 12:50:04 +02:00
Alejandro Alonso
5fc303a05d Merge remote-tracking branch 'origin/staging' into develop 2023-06-21 12:45:54 +02:00
Alejandro Alonso
9a45ce80a6 🐛 Fix comments navigation 2023-06-21 12:41:49 +02:00
Pablo Alba
3645d1af20 Merge pull request #3327 from penpot/superalex-fix-right-click-options-over-layer-or-shape
🐛 Fix right click options over layer or shape
2023-06-21 12:22:24 +02:00
Alejandro Alonso
d2bfd98a05 🐛 Fix right click options over layer or shape 2023-06-20 13:29:07 +02:00
Andrey Antukh
ecedf46c2a 📎 Add missing changelog entries for the 1.18.5 2023-06-20 11:21:09 +02:00
Andrey Antukh
73d42c03d5 Allow override the default nginx resolver
using the PENPOT_INTERNAL_RESOLVER environment variable
2023-06-20 11:21:09 +02:00
Pablo Alba
e96bedc1c8 🎉 Create multiple componentes 2023-06-20 11:07:33 +02:00
Aitor Moreno
c5f37fadba Merge pull request #3323 from penpot/alotor-fix-reload
 Not hotreload cursors
2023-06-19 16:15:59 +02:00
Aitor
8052c5f973 📎 Add [data-test] to page-items 2023-06-19 16:13:48 +02:00
Andrés Moya
c499c8a323 🐛 Small fix 2023-06-19 16:09:16 +02:00
alonso.torres
6b9962b2b3 Not hotreload cursors 2023-06-19 14:57:51 +02:00
Eva Marco
0a81ae1ea0 Merge pull request #3313 from penpot/azazeln28-fix-cursors
🐛 Fix creation cursors not being displayed
2023-06-19 13:55:19 +02:00
Alejandro
c6d71ea902 Merge pull request #3321 from penpot/niwinz-bugfixes-export
Niwinz bugfixes export
2023-06-19 13:16:44 +02:00
Andrey Antukh
4d850ebe6e 🐛 Add proper features initialization on render entrypoint 2023-06-19 13:08:11 +02:00
Andrey Antukh
dac18e876f 🐛 Fix validation error on password recovery submit operation 2023-06-19 13:07:46 +02:00
Andrey Antukh
d016876710 🐛 Add missing file-id validation on get-page rpc method 2023-06-19 13:07:26 +02:00
Andrey Antukh
ddeb540df6 🐛 Fix pointer map related issues on get-page rpc method
mainly used on render.html endpoint which is used by exporter
2023-06-19 13:06:44 +02:00
Pablo Alba
7733bc4419 🐛 Fix ungroup component 2023-06-19 12:29:54 +02:00
Alejandro Alonso
128fe29619 Show interactions on click as default setting at the view mode 2023-06-19 12:00:08 +02:00
Alejandro Alonso
23e200dece 🐛 Fix user select layer mode 2023-06-19 11:05:51 +02:00
Pablo Alba
d9375c1dd1 Fix duplicate shape in a component copy maintains its ref 2023-06-19 10:33:17 +02:00
Alejandro Alonso
aeebed6ef7 Merge remote-tracking branch 'origin/staging' into develop 2023-06-16 14:13:51 +02:00
Pablo Alba
498ba257b6 Merge pull request #3290 from penpot/hiru-fix-update-notifications
🐛 Solve error in notification of library changes
2023-06-16 14:07:35 +02:00
Andrés Moya
6edba71c12 🐛 Fix calculation of component modified and remove unneeded check 2023-06-16 13:24:41 +02:00
Andrés Moya
a559e7310a 🐛 Solve error in notification of library changes
(See main.data.workspace.notifications/schema:handle-file-change)
2023-06-16 12:23:11 +02:00
Andrés Moya
ebd172ab05 🐛 Fix detection of libraries needing to update 2023-06-16 12:22:14 +02:00
Pablo Alba
cdc3367d1b Merge pull request #3286 from penpot/superalex-fix-add-flow-option-for-frames
🐛 Fix add flow option in contextual menu for frames
2023-06-16 12:17:48 +02:00
Pablo Alba
8d37d63a27 Merge pull request #3292 from penpot/hiru-fix-export-components
🐛 Fix export components for v2
2023-06-16 12:12:01 +02:00
Aitor
95f0f63276 🐛 Fix creation cursors not being displayed 2023-06-16 12:04:16 +02:00
Pablo Alba
5cab599a06 Merge pull request #3285 from penpot/hiru-fill-problems
🐛 Revert #9de962bb and solve the fill issues in a different way
2023-06-16 11:56:39 +02:00
Alejandro Alonso
24715a85e5 Deleted fonts auto match 2023-06-16 11:07:16 +02:00
Alejandro
559c03550d Merge pull request #3298 from penpot/superalex-improve-invitations-validation
 Improve invitations validation
2023-06-16 10:51:57 +02:00
Alejandro
b8137d80cc Merge pull request #3314 from penpot/superalex-fix-survey-issues-2
🐛 Fix survey select 'other' options
2023-06-16 10:49:58 +02:00
Alejandro Alonso
0d7cac28c4 🐛 Fix survey select 'other' options 2023-06-16 10:35:37 +02:00
Alejandro Alonso
ae4fe73ac9 🐛 Fix survey select default options 2023-06-16 08:40:29 +02:00
Alejandro
1c1397a5d8 Merge pull request #3307 from penpot/eva-fix-color-context
🐛 Fix number of color bullets shown on context menu
2023-06-15 12:17:30 +02:00
Eva
cbebf9a94c 🐛 Fix number of color bullets shown on context menu 2023-06-15 11:51:25 +02:00
Alejandro
119b3e7884 Merge pull request #3306 from penpot/eva-fix-shortcuts
🐛 Fix shortcuts translation error
2023-06-15 11:03:27 +02:00
Eva
13607adf86 🐛 Fix shortcuts translation error 2023-06-15 10:59:40 +02:00
Eva Marco
247c950cce Merge pull request #3304 from penpot/alotor-fix-shape-to-path
🐛 Fix problem when transforming shape to path
2023-06-15 10:36:41 +02:00
Eva Marco
1555d4abaf Merge pull request #3303 from penpot/azazeln28-cursors
 Add CSS cursor classes
2023-06-15 08:10:44 +02:00
Alejandro
77a16a6074 Merge pull request #3301 from penpot/juan-shorcuts-ui-redesign
Shorcuts UI redesign
2023-06-15 08:03:12 +02:00
Alejandro
28b1c9c6d6 Merge pull request #3302 from penpot/superalex-fix-survey-issues
🐛 Fix some onboarding survey issues
2023-06-15 07:38:47 +02:00
Alejandro Alonso
1bb1734448 🐛 Fix some onboarding survey issues 2023-06-15 07:33:11 +02:00
alonso.torres
dd472bee64 🐛 Fix problem when transforming shape to path 2023-06-14 18:07:33 +02:00
Aitor
216454f66f Add CSS cursor classes 2023-06-14 16:27:14 +02:00
elhombretecla
ca85854baf 🎉 Adds basic shortcuts structure 2023-06-14 13:12:50 +02:00
Eva Marco
0682ed101d Merge pull request #3297 from penpot/alotor-global-styles
 Fix new styles leaking for scroll
2023-06-13 11:54:58 +02:00
Alejandro Alonso
8a9a3cbf37 Improve invitations validation 2023-06-13 11:51:03 +02:00
alonso.torres
c74ccfaa8d Fix new styles leaking for scroll 2023-06-13 11:50:21 +02:00
Andrés Moya
f2fcd0f82f 🐛 Fix export components for v2 2023-06-12 17:13:10 +02:00
Andrés Moya
a43d439b31 🐛 Revert #9de962bb and solve the fill issues in a different way 2023-06-09 21:13:43 +02:00
Alejandro
b73ab97556 Merge pull request #3284 from penpot/hiru-fix-blend-mode-validation
🐛 Flix blend mode validation when importing svg
2023-06-09 11:26:02 +02:00
Alejandro Alonso
baca9a8ce5 🐛 Fix survey spanish typo 2023-06-09 10:49:03 +02:00
Alejandro Alonso
bc64fdb1bc 🐛 Fix add flow option in contextual menu for frames 2023-06-09 09:28:27 +02:00
Andrés Moya
1d5d5e2499 🐛 Flix blend mode validation when importing svg 2023-06-08 09:52:34 +02:00
Pablo Alba
8b29a50577 Fix paste shapes from another components should detach them 2023-06-07 16:46:52 +02:00
Pablo Alba
55a821f193 🐛 Fix copy paste can produce nested components in copies 2023-06-07 13:08:17 +02:00
Pablo Alba
291180816a 🐛 Fix go to main component on another page 2023-06-07 11:53:56 +02:00
Pablo Alba
27695f5ae1 Merge pull request #3270 from penpot/hiru-bugtixes-3
Hiru bugfixes 3
2023-06-06 16:18:51 +02:00
Pablo Alba
69d3bda01f 🐛 Remove graphics from assets filter for components v2 2023-06-06 16:11:09 +02:00
Alejandro
1632530b21 Merge pull request #3280 from penpot/superalex-fix-develop-2
🐛 Fix align.cljc lint
2023-06-06 14:11:16 +02:00
Alejandro Alonso
c89f2fc627 🐛 Fix align.cljc lint 2023-06-06 14:03:08 +02:00
Alejandro Alonso
d0c68dbc23 🎉 Updage CHANGES.md 2023-06-06 13:23:34 +02:00
Alejandro
e41c36f534 Merge pull request #3267 from dfelinto/fix-distribute
🐛 Distribute vertical spacing failing for overlapped text
2023-06-06 13:21:51 +02:00
Andrés Moya
9de962bbc9 🐛 Do not render fills block when there is no fill. 2023-06-06 13:16:28 +02:00
Aitor Moreno
4947169a7c Merge pull request #3268 from penpot/superalex-file-libraries-colors-order
 Improve file libraries colors order
2023-06-06 10:43:52 +02:00
Andrés Moya
f425a5866b 🐛 Allow empty fills in text content 2023-06-05 17:37:41 +02:00
Andrés Moya
3e30d4776a 🐛 Avoid unneeded component update, that was generating loops 2023-06-05 15:43:57 +02:00
Andrés Moya
bca90c54e9 🐛 Preserve root shape position on parent when create component 2023-06-05 15:43:57 +02:00
Andrés Moya
8c3f90fe36 🐛 Fix erroneous touched state when delete a copy and then undo 2023-06-05 15:43:57 +02:00
Andrés Moya
0b316d6828 🐛 Fix touched erroneously set after a text component sync 2023-06-05 15:43:57 +02:00
alonso.torres
8772e51bd2 🐛 Fix problem with padding input 2023-06-05 11:23:08 +02:00
Alejandro Alonso
7e8afb4228 Merge remote-tracking branch 'origin/staging' into develop 2023-06-05 10:19:43 +02:00
Alejandro
6659ab110c Merge pull request #3273 from penpot/alotor-fix-gap
🐛 Fix problem with undefined gaps
2023-06-05 10:04:26 +02:00
alonso.torres
3b8c3647fa 🐛 Fix problem with undefined gaps 2023-06-05 09:56:03 +02:00
Alejandro Alonso
4fc8ac61f1 ❤️ Add thanks for Dalai Felinto 2023-06-05 06:41:07 +02:00
Alejandro Alonso
5b475f9206 ❤️ Add thanks for Dalai Felinto 2023-06-05 06:40:30 +02:00
Alejandro
c228f2fd68 Merge pull request #3266 from dfelinto/fix-distribute-enable
🐛 Distribute fix enabled when two elements were selected
2023-06-05 06:38:33 +02:00
Dalai Felinto
395fbef19e 🐛 Distribute vertical spacing failing for overlapped text
The code was doing what it was designed to, however there is no
reason to prevent elements with a bit of overlap to also be
equally distributed.

closes #3141

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

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

Developer's Certificate of Origin 1.1

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

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

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

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

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

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

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

Signed-off-by: Ryan Breen
2023-05-05 10:59:15 +02:00
Alejandro
5dd1fa0f98 Merge pull request #3171 from penpot/niwinz-enhancements-3
 Improve file-gc task
2023-05-05 10:55:14 +02:00
Alejandro Alonso
82b2f920c1 Merge branch 'akshay-gupta7-akshayg7-click-to-select-full-values-design-sidebar' into develop 2023-05-05 10:49:49 +02:00
Akshay Gupta
1c0e1237c2 🎉 Add feature to select full values on click at the design sidebar 2023-05-05 10:49:25 +02:00
Andrey Antukh
ceeed73dea Merge remote-tracking branch 'origin/staging' into develop 2023-05-04 22:15:18 +02:00
Alejandro Alonso
890583a13a Add mvp access-token support 2023-05-04 22:14:55 +02:00
Alejandro Alonso
21c6730dc7 🐛 Fix emails from header 2023-05-04 15:46:52 +02:00
Aitor
19727a648d better rules performance 2023-05-04 12:46:37 +02:00
Alejandro Alonso
b90aef4e1d Merge branch 'akshay-gupta7-akshayg7-set-line-height-to-auto' into develop 2023-05-04 12:34:28 +02:00
Akshay Gupta
412ffe4b46 🎉 Add feature to set line-height to auto as 1.2 2023-05-04 12:34:10 +02:00
Alejandro Alonso
c5cfe7e2e9 Change libraries and templates order 2023-05-04 08:51:14 +02:00
Akshay Gupta
45356ae1fc 🎉 Add feature to focus input on search when searching a file at projects dashboard 2023-05-03 14:15:59 +02:00
Eva
86b0e95458 :sparkles:Add new layers panel UI design 2023-04-27 12:26:26 +02:00
Pablo Alba
90fb619dfc Fix restore main component when it was inside a group 2023-04-26 13:30:23 +02:00
Andrey Antukh
5e89aa2726 Improve file-gc task
make it more aware of fragments referenced on changes snapshots
2023-04-26 13:28:32 +02:00
Alejandro Alonso
82dad3217b 🐛 Fix translations typo 2023-04-26 12:38:02 +02:00
Alejandro Alonso
47cb228e30 Merge branch 'akshay-gupta7-akshayg7-empty-state-for-color-typographies' into develop 2023-04-26 12:36:39 +02:00
Akshay Gupta
35c0b94e0d 🎉 Add message for empty state for color and typography palettes 2023-04-26 12:36:31 +02:00
Pablo Alba
a7015f2517 Fix restore and instanciate (in copy and paste) components with parent 2023-04-26 11:34:26 +02:00
Pablo Alba
4f471f39da Merge pull request #3166 from penpot/hiru-frame-titles
 Hide frame titles for component copies
2023-04-25 21:33:56 +02:00
Pablo Alba
f14641396f Merge pull request #3165 from penpot/hiru-board-selection
 Give frames that are components more priority on selection
2023-04-25 21:31:01 +02:00
Alejandro
d97bbdf140 Merge pull request #3169 from penpot/niwinz-enhancements-2
 Add the abiltiy to forward command params as query-string
2023-04-25 16:33:03 +02:00
Andrey Antukh
f1c42a698d 📎 Increase http socket backlog 2023-04-25 16:25:49 +02:00
Andrey Antukh
8fb62628d2 Add the abiltiy to forward command params as query-string 2023-04-25 16:25:30 +02:00
Andrey Antukh
5026bfa6c1 📎 Fix linter issues introduced in previous merge 2023-04-25 13:35:26 +02:00
Andrey Antukh
b37a92aaf7 Merge remote-tracking branch 'origin/staging' into develop 2023-04-25 13:34:28 +02:00
Alejandro
c44e2a9526 Merge pull request #3167 from penpot/niwinz-bugfixes-12
 Improve OICD attrs lookup mechanism
2023-04-25 12:26:42 +02:00
Andrey Antukh
c0ccc4a5c5 Improve OICD attrs lookup mechanism 2023-04-25 12:18:06 +02:00
Alejandro
364dadc93f Merge pull request #3164 from penpot/niwinz-bugfixes-10
🎉 Enable several languages
2023-04-25 12:16:59 +02:00
Andrés Moya
b45bdb52b2 Hide frame titles for component copies 2023-04-25 11:55:03 +02:00
Andrés Moya
7c612d8bcf Give frames that are components more priority on selection 2023-04-25 11:21:24 +02:00
Andrey Antukh
3311bf6ac0 📎 Remove duplicated file 2023-04-25 11:11:58 +02:00
Andrey Antukh
b5c160732e 🎉 Add ukranian lang 2023-04-25 11:11:58 +02:00
Andrey Antukh
f8d00c0e7f 🎉 Add latvian lang 2023-04-25 11:11:58 +02:00
Andrey Antukh
42acae3ae3 🎉 Add korean lang 2023-04-25 11:11:58 +02:00
Andrey Antukh
571439871b 🎉 Add faroese lang 2023-04-25 11:11:58 +02:00
Andrey Antukh
e051e26dad 🎉 Add czech lang 2023-04-25 11:11:51 +02:00
Pablo Alba
4ddd3811b2 🐛 Fix copy and paste components between files 2023-04-25 10:27:06 +02:00
Alejandro
da54557aab Merge pull request #3163 from penpot/niwinz-bugfixes-9
🐛 🔥 Fix merge bugs and remove deprecated code
2023-04-25 10:26:47 +02:00
Aitor
52763ceaf7 Merge pull request #3138 from penpot/fix-bad-undo-group-association-in-alt-copy
🐛 Fix bad undo group associations
2023-04-25 09:42:22 +02:00
Andrey Antukh
c0ccbaebaf 🔥 Remove deprecated queries and mutations 2023-04-24 20:18:14 +02:00
Andrey Antukh
36953eef1a 🐛 Use proper commands (instead of queries) on render frontend namespace 2023-04-24 19:47:28 +02:00
Andrey Antukh
84c8a6eced 🐛 Use correct parameters on password update on login 2023-04-24 19:46:42 +02:00
Andrey Antukh
1f023eebeb 🔥 Remove unused code 2023-04-24 18:21:48 +02:00
Andrey Antukh
6af783ea91 Merge remote-tracking branch 'origin/staging' into develop 2023-04-24 16:55:18 +02:00
Alejandro
cea0c7277c Merge pull request #3161 from penpot/niwinz-bugfixes-8
🎉 Bugfixes and translation updates
2023-04-24 16:43:42 +02:00
Andrey Antukh
8e860ec5a6 🎉 Add Indonesian lang to the supported languages 2023-04-24 16:22:31 +02:00
Andrey Antukh
7b93c0bb09 📎 Sort all translation strings 2023-04-24 16:22:04 +02:00
Andrey Antukh
3ae8da7d22 Merge remote-tracking branch 'weblate/develop' into niwinz-bugfixes-8 2023-04-24 16:10:34 +02:00
Andrey Antukh
c31eb2df42 🐛 Fix OICD auth provider roles checking mechanism 2023-04-24 15:13:24 +02:00
Alejandro Alonso
e89378453a Merge remote-tracking branch 'origin/staging' into develop 2023-04-24 09:15:22 +02:00
Alejandro
3a57b436a4 Merge pull request #3140 from penpot/azazeln28-fix-project-file-count
🐛 Fix project file count
2023-04-24 09:14:59 +02:00
Aitor
657ce4fa0a 🐛 Fix project file count 2023-04-24 09:14:44 +02:00
Alejandro
dde7063da0 Merge pull request #3145 from penpot/azazeln28-fix-page-context-menu
🐛 Fix page item context menu
2023-04-24 09:11:55 +02:00
Aitor
b3c4ff4dc0 🐛 Fix page item context menu 2023-04-24 09:04:09 +02:00
Alejandro Alonso
b7d1488aa3 Merge branch 'akshay-gupta7-akshayg7-duplicate-with-drag-and-alt' into develop 2023-04-24 08:22:28 +02:00
Akshay Gupta
d586f82da1 🎉 Implement functionality to duplicate objects via drag + alt 2023-04-24 08:22:14 +02:00
Alejandro Alonso
a658493ac5 Merge branch 'akshay-gupta7-akshayg7-typography-palette-order' into develop 2023-04-24 07:44:06 +02:00
Akshay Gupta
eaaeef2335 🎉 Update Typography palette order 2023-04-24 07:43:47 +02:00
Alejandro Alonso
bef9bbaa6a Merge branch 'abstractalgo-patch-1' into develop 2023-04-24 06:39:22 +02:00
Dragan Okanovic
32810f2ecd 🐛 Fix broken link in README 2023-04-24 06:38:17 +02:00
Ally Tiago
8856a635ed 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2023-04-24 05:54:38 +02:00
Radek Sawicki
d6bd4ac7fd 🌐 Add translations for: Polish.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2023-04-21 23:52:22 +02:00
Radek Sawicki
efa5fb609c 🌐 Add translations for: Polish.
Currently translated at 93.5% (1108 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2023-04-20 17:52:11 +02:00
王世阳
2f920ba651 🌐 Add translations for: Chinese (Simplified).
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2023-04-15 15:47:45 +02:00
Alejandro Alonso
ed164ce69b Merge remote-tracking branch 'origin/staging' into develop 2023-04-14 13:28:26 +02:00
Alejandro Alonso
e1652d17d9 Update material design template to version 3 2023-04-14 13:27:06 +02:00
Pablo Alba
33656f8eb4 Merge pull request #3115 from penpot/hiru-components-boards
🎉 Now all component roots are frames
2023-04-14 12:40:32 +02:00
Andrés Moya
bbd561a772 🔧 Fix test cases 2023-04-14 12:31:04 +02:00
Andrés Moya
2790111405 🎉 Now all component roots are frames 2023-04-14 12:31:03 +02:00
Alejandro Alonso
47b791e938 Board as ruler origin 2023-04-14 09:22:43 +02:00
Pablo Alba
47b432e307 🐛 Fix bad undo group associations 2023-04-13 18:44:40 +02:00
Alejandro Alonso
ce341a05e1 Merge remote-tracking branch 'origin/staging' into develop 2023-04-13 16:34:22 +02:00
alonso.torres
9a750bad93 🐛 Fix problem with rulers not placing correctly 2023-04-13 16:32:19 +02:00
alonso.torres
d127177029 🐛 Fix problem when "show in view mode" flag 2023-04-13 16:32:02 +02:00
Suhwan Kim
9b7506ee8d 🌐 Add translations for: Korean.
Currently translated at 15.5% (184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2023-04-13 14:51:00 +02:00
王世阳
176d48707f 🌐 Add translations for: Chinese (Simplified).
Currently translated at 93.5% (1108 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2023-04-13 14:50:50 +02:00
Alejandro Alonso
45fc55dee9 Merge remote-tracking branch 'origin/staging' into develop 2023-04-13 12:24:52 +02:00
Alejandro
9a695a76ed Merge pull request #3135 from penpot/palba-fix-first-level-boards-show-in-view-mode2
🐛 Fix first level board "Show in view mode" is automatically unchecked
2023-04-13 12:23:58 +02:00
Pablo Alba
36bd82ceb4 🐛 Fix first level board "Show in view mode" is automatically unchecked 2023-04-13 12:15:08 +02:00
Alejandro
067b76ebd8 Merge pull request #3134 from penpot/niwinz-bugfixes-7
🐛 Fix upload-file-media-object rpc method
2023-04-13 11:07:15 +02:00
Andrey Antukh
cb02b07395 🐛 Fix upload-file-media-object rpc method 2023-04-13 10:55:15 +02:00
Alejandro Alonso
81d718570d 🐛 Fix backend import 2023-04-13 09:21:57 +02:00
Alejandro Alonso
ee1b9e861e Merge remote-tracking branch 'origin/staging' into develop 2023-04-13 09:17:06 +02:00
Alejandro
0f9b50de50 Merge pull request #3127 from penpot/niwinz-bugfixes-6
🐛 Fix inconsistencies on drop datauri images (mainly for firefox …
2023-04-13 09:16:17 +02:00
Andrés Moya
271b83de2e 🐛 Fix features activation by devtools console 2023-04-12 16:14:36 +02:00
Andrey Antukh
7ef07385c6 🐛 Fix inconsistencies on drop datauri images (mainly for firefox browser) 2023-04-12 13:18:24 +02:00
Alejandro Alonso
aaca901fd9 🎉 Create typography style from a selected text layer 2023-04-12 09:30:41 +02:00
Pablo Alba
ccaac2a5c7 Merge pull request #3120 from penpot/superalex-default-naming-of-text-layers
🎉 Default naming of text layers
2023-04-11 17:50:53 +02:00
Pablo Alba
147beb3963 Merge pull request #3100 from penpot/hiru-detach-top-level-only
🎉 Detach component now only affects top instance, not subinstances
2023-04-11 15:19:20 +02:00
Pablo Alba
e481f1cc99 Merge pull request #3081 from penpot/hiru-cancel-remove-graphics
 Allow to cancel and resume later remove graphics
2023-04-11 15:16:05 +02:00
Alejandro
5cec969ee3 Merge pull request #3124 from penpot/niwinz-bugfixes-6
 Improve audit props for upload-file-media-objects rpc method
2023-04-11 13:03:15 +02:00
Andrey Antukh
12ac01a9cb Improve audit props for upload-file-media-objects rpc method 2023-04-11 12:40:59 +02:00
Pablo Alba
c1ed5a5b33 Merge pull request #3097 from penpot/hiru-fix-features-detect
♻️ Enhance features loading to avoid race conditions
2023-04-11 09:55:11 +02:00
Alejandro Alonso
4d8f471eca Merge remote-tracking branch 'origin/staging' into develop 2023-04-11 06:52:35 +02:00
Alejandro
3233c78130 Merge pull request #3123 from penpot/alotor-hotfix
Hotfix page hangs
2023-04-11 06:50:30 +02:00
alonso.torres
dfb1a79179 🐛 Fix problem when closing paths for big imported SVGs 2023-04-11 06:43:53 +02:00
andy
9758b2722e 🌐 Added translation for: Burmese. 2023-04-10 16:45:59 +02:00
Alejandro Alonso
5993b9855e 🎉 Default naming of text layers 2023-04-10 13:16:26 +02:00
Andrey Antukh
6972227b8d 📎 Add improved docstring for penpot_secret_key 2023-04-07 08:55:53 +02:00
Ņikita K
1c13ab6de5 🌐 Add translations for: Latvian.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2023-04-06 12:35:17 +02:00
Suhwan Kim
7e403c65c2 🌐 Add translations for: Korean.
Currently translated at 5.4% (64 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2023-04-06 12:34:52 +02:00
Alejandro Alonso
08c6ebe10c 🐛 Fix metrics and doc endpoints 2023-04-05 20:08:20 +02:00
Alejandro
73b913065f Merge pull request #3110 from penpot/alotor-fixes
🐛 Fix problem with frame title rotation
2023-04-05 09:28:03 +02:00
alonso.torres
8db3f2b277 🐛 Fix problem with frame title rotation 2023-04-05 09:23:51 +02:00
Alejandro Alonso
408de63ea3 Merge remote-tracking branch 'origin/staging' into develop 2023-04-05 07:35:36 +02:00
Alejandro
6d3baaae47 Merge pull request #3109 from penpot/niwinz-bugfixes-5
🐛 Add missing storage dependency to dbg routes module
2023-04-05 06:59:27 +02:00
Andrey Antukh
30e1c7d54e 🐛 Add missing storage dependency to dbg routes module 2023-04-04 19:20:10 +02:00
Andrey Antukh
28d3402793 Merge branch 'leonekmi-patch-1' into staging 2023-04-04 19:10:38 +02:00
Leon
e9daaa2eb8 🐛 Fix default port for penpot-exporter on nginx entrypoint
By default, penpot-frontend will try to reach penpot-export on port 80,
this is wrong, the exporter listens by default to port 6061.
2023-04-04 19:09:44 +02:00
Andrés Moya
e66f9597a9 Update component copy icon 2023-04-04 15:04:40 +02:00
Alejandro
dc7e62245d Merge pull request #3102 from penpot/alotor-bugfixing-16
SVG Uploading fixes
2023-04-04 10:14:10 +02:00
alonso.torres
da953f0bc0 🐛 Fix problems with imported SVG embedded images and transforms 2023-04-04 09:52:15 +02:00
alonso.torres
4b086ab2bc 🐛 Fix problems with imported SVG shadows 2023-04-04 09:52:15 +02:00
Alejandro
cd9bc1d8d7 Merge pull request #3104 from penpot/niwinz-bugfixes-4
🐛 Enable by default mattermost webhook error reporter
2023-04-04 08:45:10 +02:00
Andrey Antukh
cdaf63afa0 🐛 Enable by default mattermost webhook error reporter 2023-04-04 08:38:05 +02:00
Alejandro
7ebc185b3a Merge pull request #3103 from penpot/niwinz-bugfixes-3
🐛 Fix incorrect metrics reference on rpc methods middleware
2023-04-04 06:46:22 +02:00
Andrey Antukh
3222583a69 🐛 Fix incorrect metrics reference on rpc methods middleware 2023-04-03 23:09:39 +02:00
Ņikita K
85fe0130c3 🌐 Add translations for: Latvian.
Currently translated at 13.0% (154 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2023-04-03 14:21:30 +02:00
Alejandro Alonso
f7e37924e5 🐛 Fix backend update-profile-password! call 2023-04-03 12:53:04 +02:00
elhombretecla
1afecf23aa 🐛 Fix onboarding slides typo 2023-04-03 12:52:53 +02:00
Alejandro Alonso
68b26d5f41 Merge remote-tracking branch 'origin/staging' into develop 2023-04-03 12:21:12 +02:00
Andrés Moya
a27fa8b317 🎉 Detach component now only affects top instance, not subinstances 2023-04-03 11:52:26 +02:00
elhombretecla
6b724d9572 Add new optimized gifs 2023-04-03 11:22:59 +02:00
elhombretecla
2789ecc22a Add new spacing optimized gif 2023-04-03 11:07:37 +02:00
Alejandro
2eba317797 Merge pull request #3099 from penpot/alotor-bugfixing-15
🐛 Fix problem with opacity in imported SVG's
2023-04-03 09:49:52 +02:00
alonso.torres
5856e3cc03 🐛 Fix problem with opacity in imported SVG's 2023-04-03 09:24:54 +02:00
Andrés Moya
8cd59c39ed 🌐 Added translation for: Latvian. 2023-04-03 09:22:22 +02:00
Andrés Moya
18efa4ff2c ♻️ Enhance features loading to avoid race conditions 2023-03-31 16:13:11 +02:00
Aimee
722e95abf2 🌐 Add translations for: French.
Currently translated at 88.5% (1048 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2023-03-31 15:37:52 +02:00
Pablo Alba
b33e469501 🎉 Copy paste components, even to another page 2023-03-31 14:04:06 +02:00
Alejandro
cc469b116d Merge pull request #3093 from penpot/alotor-bugfixing-14
Alotor bugfixing 14
2023-03-31 13:56:25 +02:00
Andrey Antukh
9fe49b5546 🐛 Fix unexpected responste truncation related to shared links
that contains old data that is not used but can be still present
on the table; we should consider right now to proceed to delete
the row completly
2023-03-31 12:48:24 +02:00
Andrey Antukh
0c89b7cdb1 🐛 Fix some issues on read-only database connection 2023-03-31 12:48:24 +02:00
Andrey Antukh
90d48c1d30 Add the ability to check read-only state of connection
on the db ns helper; previously it only worked with datasource
instances
2023-03-31 12:48:24 +02:00
alonso.torres
2792c22ec9 🐛 Fix problem with overlays positioning 2023-03-31 12:22:16 +02:00
alonso.torres
a838dac01b 🐛 Fix problem when calculating group bounds 2023-03-31 11:20:42 +02:00
Alejandro Alonso
d5bbc7b1aa 🐛 Fix hide grid keyboard shortcut 2023-03-31 09:57:24 +02:00
Alejandro
e1e6816544 Merge pull request #3089 from penpot/alotor-bugfixing-13
Alotor bugfixing 13
2023-03-31 08:49:46 +02:00
alonso.torres
64c0273554 🐛 Fix problem when reorder layers removes show in viewer 2023-03-30 16:30:21 +02:00
alonso.torres
532caea169 🐛 Fix relative position overlay positioning 2023-03-30 16:20:37 +02:00
alonso.torres
0c8d8d92ba 🐛 Fix precision for wrap in flex 2023-03-30 16:20:36 +02:00
Alejandro Alonso
af428ab0ae 🐛 Fix view mode header buttons overlapping in small resolutions 2023-03-30 16:16:24 +02:00
Alejandro Alonso
85b3605c33 🐛 Fix dashboard scrolling using 'Page Up' and 'Page Down' 2023-03-30 16:16:24 +02:00
Alejandro
f1431b7b77 Merge pull request #3086 from penpot/release-info-1.18
Add new onboarding slides 1.18 info
2023-03-30 14:37:38 +02:00
elhombretecla
1ea1d53971 Add new 1.18 info 2023-03-30 14:35:10 +02:00
Alejandro Alonso
8bf01858bb 🐛 Fix alt+P combination while left bar buttons focused 2023-03-29 16:26:34 +02:00
Alejandro Alonso
f05f527336 🐛 Fix allow change team image for editor role users 2023-03-29 16:26:34 +02:00
Alejandro Alonso
fa4c7a1eb7 🐛 Fix last update project timer update after creating new file 2023-03-29 16:26:34 +02:00
Alejandro Alonso
3e6b3bcdc4 🐛 Fix unpublish and delete shared library warning messages 2023-03-29 16:26:34 +02:00
Alejandro
aca242046e Merge pull request #3082 from penpot/alotor-bugfixing-12
🐛 Fix problem with invalid SVG shape
2023-03-29 16:05:57 +02:00
alonso.torres
be27ce4914 🐛 Fix problem with invalid SVG shape 2023-03-29 16:00:07 +02:00
Andrés Moya
e8d49fae13 Allow to cancel and resume later remove graphics 2023-03-29 12:57:21 +02:00
Alejandro
190b77ff95 Merge pull request #3080 from penpot/alotor-bugfixing-11
🐛 Fix problem with SVG and flex layout
2023-03-29 11:47:09 +02:00
alonso.torres
6e78745ed5 🐛 Fix problem with SVG and flex layout 2023-03-29 11:22:26 +02:00
Alejandro
f03def32fd Merge pull request #3078 from penpot/alotor-bugfixing-10
Alotor bugfixing 10
2023-03-29 09:32:18 +02:00
alonso.torres
a98ae69a03 🐛 Disable empty names on rename files 2023-03-29 09:18:22 +02:00
alonso.torres
43fe2390c8 🐛 Fix problem with copy/paste shapes 2023-03-29 09:12:03 +02:00
alonso.torres
d54e152a3d 🐛 Fix problem creating files in project page 2023-03-29 09:12:03 +02:00
alonso.torres
ac23c7bb4a 🐛 Remove "show in view mode" flag when moving frame to frame 2023-03-29 09:12:03 +02:00
alonso.torres
66444e27b1 🐛 Fix problem with selection colors and texts 2023-03-29 09:12:03 +02:00
Alejandro Alonso
92baf75ccd 🐛 Fix import typo 2023-03-29 08:53:10 +02:00
Alejandro Alonso
0714dc34c5 🐛 Fix spelling mistake in confirmation after importing only 1 file 2023-03-28 17:25:44 +02:00
Alejandro Alonso
aa068c70c2 🐛 Fix expanded typography on assets sidebar is moving 2023-03-28 17:25:44 +02:00
Alejandro Alonso
70974efc74 🐛 Fix dashboard left sidebar, the [x] overlaps the field 2023-03-28 17:25:44 +02:00
Alejandro Alonso
acccba6ed4 🐛 Fix invalid files amount after moving on dashboard 2023-03-28 17:25:44 +02:00
Alejandro Alonso
2e549b164f 🐛 Fix internal error on imported svgs 2023-03-28 17:25:44 +02:00
Alejandro Alonso
3df2b80427 🐛 Fix rename option is absent in RMB menu for file 2023-03-28 17:25:44 +02:00
Alejandro Alonso
0ec89e8bbe 🐛 Fix enter emails on onboarding new user creating team 2023-03-28 17:25:44 +02:00
Alejandro Alonso
694497803b 🐛 Fix don't show invite user hero to users with editor role 2023-03-28 17:25:44 +02:00
Pablo Alba
b73ce14560 Merge pull request #2967 from penpot/hiru-refactor-instances
🔧 Read component shapes from pages
2023-03-28 12:00:10 +02:00
Alejandro
88db456127 Merge pull request #3075 from penpot/alotor-bugfixes-9
Alotor bugfixes 9
2023-03-28 09:08:45 +02:00
alonso.torres
6832b4a304 🐛 Fix problem with text carring over next line when changing to fixed 2023-03-27 17:06:45 +02:00
alonso.torres
5079582e1f 🐛 Fix problem with round corners scaling 2023-03-27 13:26:24 +02:00
alonso.torres
4313c45870 🐛 Fix sending invitation to existing members 2023-03-27 13:18:52 +02:00
alonso.torres
1f9e7f2ae8 🐛 Fix markdown message 2023-03-27 13:18:52 +02:00
alonso.torres
f7bba745ab 🐛 Changes to the header menu 2023-03-27 13:18:52 +02:00
alonso.torres
391ba77da9 🐛 Fix scaling of texts 2023-03-27 13:18:52 +02:00
alonso.torres
1d7b43ffbc 🐛 Fix problem with outer stroke in texts 2023-03-27 13:18:52 +02:00
alonso.torres
7256759488 🐛 Fix problem with color picker not able to change hue 2023-03-27 13:18:52 +02:00
alonso.torres
f11c782c0f 🐛 Fix problem when copy/pasting shapes 2023-03-27 13:18:52 +02:00
Pablo Alba
26aec7d129 🐛 Fix usiong padding/marging value on updating with shift 2023-03-27 13:18:08 +02:00
Pablo Alba
d61c799846 🐛 Fix padding/gap/margin remain glowing when the shape is deselected and selected again 2023-03-27 13:18:08 +02:00
Pablo Alba
c3c41c5b7d 🐛 Fix rotate board breaks paddings 2023-03-27 13:18:08 +02:00
Pablo Alba
eeb76b1e50 🐛 Fix during scale paddings glow 2023-03-27 13:18:08 +02:00
Pablo Alba
caf462e9b8 🐛 Fix padding prediction does not work with one shape 2023-03-27 13:18:08 +02:00
Pablo Alba
4d70d3b909 🐛 Bad padding gui on nil sizing 2023-03-27 13:18:08 +02:00
Pablo Alba
6a1115ddda 🐛 Fix usiong padding/marging value on updating with shift 2023-03-27 11:55:23 +02:00
Pablo Alba
d3ae53e3ef 🐛 Fix padding/gap/margin remain glowing when the shape is deselected and selected again 2023-03-27 11:55:23 +02:00
Pablo Alba
4774cc4859 🐛 Fix rotate board breaks paddings 2023-03-27 11:55:23 +02:00
Pablo Alba
bc07dad4ae 🐛 Fix during scale paddings glow 2023-03-27 11:55:23 +02:00
Pablo Alba
0f9ad0907e 🐛 Fix padding prediction does not work with one shape 2023-03-27 11:55:23 +02:00
Pablo Alba
300ad15f5a 🐛 Bad padding gui on nil sizing 2023-03-27 11:55:23 +02:00
Andrés Moya
ad786ab95f 🎉 Group component sync changes in a single undo 2023-03-27 10:39:35 +02:00
Andrés Moya
fe898315c3 🐛 Fix absorb libraries 2023-03-27 10:39:35 +02:00
Andrés Moya
96540af2b1 🎉 Instantiate component with duplicate 2023-03-27 10:39:35 +02:00
Pablo Alba
6889440014 🐛 Fix wrong shape-ref on duplicate component 2023-03-27 10:39:35 +02:00
Pablo Alba
e59d106315 🐛 Fix duplicate component in assets generates wrong main copy 2023-03-27 10:39:35 +02:00
Andrés Moya
7391a4086a 🔧 Refactor delete/restore components 2023-03-27 10:39:35 +02:00
Andrés Moya
b91f1959b4 🎉 Update tests 2023-03-27 10:39:35 +02:00
Andrés Moya
0711fa700b 🔧 Read component shapes from pages 2023-03-27 10:39:33 +02:00
Alejandro Alonso
a4dd5fccff 🐛 Fix develop branch after merge 2023-03-24 13:06:16 +01:00
Alejandro Alonso
4fad2ab619 Merge remote-tracking branch 'origin/staging' into develop 2023-03-24 12:33:14 +01:00
Alejandro Alonso
91e81823a5 🐛 Fix deleted files appear in search results 2023-03-24 12:20:00 +01:00
Alejandro Alonso
d0ab0bccb9 🐛 Fix drag and drop files from browser or file explorer under circumstances 2023-03-24 12:20:00 +01:00
Alejandro Alonso
b2b91bfa57 🐛 Fix change email and password for users signed in via social login 2023-03-24 12:20:00 +01:00
Alejandro
fc857aad08 Merge pull request #3068 from penpot/alotor-bugfixes-8
Alotor bugfixes 8
2023-03-24 08:32:38 +01:00
alonso.torres
5874922367 🐛 Fix problem with guides not showing when moving over nested frames 2023-03-23 17:13:04 +01:00
alonso.torres
1657f06a48 🐛 Select children after ungroup action 2023-03-23 16:41:00 +01:00
alonso.torres
2ad9c3cc72 🐛 Forbid empty names for assets 2023-03-23 16:11:23 +01:00
alonso.torres
fae76f6d4e 🐛 Fix problem with geometry of groups 2023-03-23 12:03:25 +01:00
alonso.torres
d0878aa805 🐛 Fix visual problem in select options 2023-03-23 12:03:25 +01:00
alonso.torres
020454e701 🐛 Fix header not showing when exiting fullscreen mode in viewer 2023-03-23 12:03:25 +01:00
alonso.torres
eedb83e863 🐛 Fix problem with text out of borders when changing from auto-width to fixed 2023-03-23 12:03:25 +01:00
alonso.torres
8a6809848e 🐛 Show warning when trying to invite a user that is already in members 2023-03-23 12:03:25 +01:00
Alejandro Alonso
3b2083134e 🐛 Fix multiplayer username sometimes is not displayed correctly 2023-03-23 09:14:58 +01:00
Alejandro Alonso
b5fc074e35 🐛 Fix horizontal margins drag don't always start from place 2023-03-23 09:14:58 +01:00
Alejandro
bc794816db Merge pull request #3066 from penpot/alotor-bugfixes-7
Alotor bugfixes 7
2023-03-22 16:28:19 +01:00
alonso.torres
f1b5ac27a9 🐛 Fix path options not showing when editing rects or ellipses 2023-03-22 14:45:19 +01:00
alonso.torres
ea438d3626 🐛 Add tooltip for text alignment options 2023-03-22 14:25:32 +01:00
alonso.torres
6d93501dc7 🐛 Fix shortcuts for alignment 2023-03-22 14:25:24 +01:00
alonso.torres
09d0a9e3f8 🐛 Fix problem when assigning color from palette or assets 2023-03-22 12:36:40 +01:00
alonso.torres
2fef90e7eb 🐛 Fix problem with selected colors and texts 2023-03-22 11:58:33 +01:00
Alejandro Alonso
c851f60de4 🐛 Fix deleted files appear in search results 2023-03-22 09:47:03 +01:00
Alejandro Alonso
6b4bca50ee bug: Fix manipulate duplicated project 2023-03-22 09:47:03 +01:00
Alejandro Alonso
f05e37590a 🐛 Fix font kerning on export 2023-03-22 09:30:07 +01:00
Alejandro Alonso
fbf06a4de0 Use tabulators to navigate layers 2023-03-22 09:23:06 +01:00
Alejandro
25014a81c3 Merge pull request #3062 from penpot/alotor-bugfixes-6
Bugfixes
2023-03-22 07:29:43 +01:00
Andrés Moya
ce3e30ea02 🐛 Fix linter issues 2023-03-21 17:12:54 +01:00
Andrés Moya
1d026ab085 🎉 Added 'go to main component' to components context menu 2023-03-21 17:12:54 +01:00
alonso.torres
5d77f7e5b1 🐛 Fix linter issues 2023-03-21 17:11:23 +01:00
alonso.torres
131e4f2446 🐛 Fix nested frame interaction created flow in wrong frame 2023-03-21 16:50:59 +01:00
alonso.torres
8ab264af80 🐛 Fix problem in Firefox with scroll jumping when changin pages 2023-03-21 16:43:04 +01:00
Andrey Antukh
60d629a0c6 Merge branch 'connecting-line-height-values-to-variables' into develop 2023-03-19 18:37:56 +01:00
Ondřej Konečný
d337dbfa5d ♻️ Connect line-heigh values to variables and set scale
Signed-off-by: Ondřej Konečný <ondrej.konecny@gmail.com>
2023-03-19 18:37:40 +01:00
Ally Tiago
e735b9f90e 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2023-03-18 22:41:11 +01:00
Renan Mayrinck
95fb3dfcd2 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2023-03-18 22:41:10 +01:00
Andrey Antukh
582ec187f8 Merge remote-tracking branch 'origin/staging' into develop 2023-03-17 10:19:04 +01:00
Alejandro
40ca804d93 Merge pull request #3051 from penpot/niwinz-experiments-2
🐛 Fix many issues related to the concurrency refactor PR
2023-03-17 08:25:31 +01:00
Alejandro
b32e0f458c Merge pull request #3050 from penpot/alotor-fixes-layout
Alotor fixes layout
2023-03-17 08:24:30 +01:00
Alejandro
484a50949a Merge pull request #3049 from penpot/alotor-bugfixes-5
Bugfixes
2023-03-17 08:23:39 +01:00
alonso.torres
a118f34b49 🐛 Add version to presence and fixes off-page updates 2023-03-17 08:23:03 +01:00
Andrey Antukh
2818666a1a 📎 Fix minnor cosmetic issue on instant and duration pretty printing 2023-03-16 22:33:35 +01:00
Andrey Antukh
9143639357 🐛 Fix incorrect webhook url validation 2023-03-16 22:33:35 +01:00
Andrey Antukh
f18d2ea629 🐛 Add missing fragment persistence on creating file
Related with storage/pointer-map feature.
2023-03-16 22:33:35 +01:00
Andrey Antukh
938890c04c 🐛 Fix vthread pining on get-file-data-for-thumbnail rpc method 2023-03-16 22:33:35 +01:00
Andrey Antukh
9173c73eca 🐛 Forward var bindings on climit submit operation 2023-03-16 22:33:35 +01:00
Andrey Antukh
69c8a89dd2 🎉 Add the ability to specify the output format from query string 2023-03-16 22:33:35 +01:00
Andrey Antukh
b462ac019a 🐛 Fix typo on error type 2023-03-16 22:33:35 +01:00
Andrey Antukh
3011d24905 📎 Enable storage features on start-dev and repl scripts 2023-03-16 22:33:35 +01:00
alonso.torres
120d3005ea 🐛 Fix change layer index when moving absolute positioned shape 2023-03-16 17:39:11 +01:00
alonso.torres
2272977d67 🐛 Fix problem when editing gap/margins in layout 2023-03-16 17:11:46 +01:00
alonso.torres
cbe8587db3 🐛 Fix problem with z positioning of elements 2023-03-16 15:08:49 +01:00
alonso.torres
6a4d505033 🐛 Fix problem with alt getting stuck when alt+tab 2023-03-16 15:08:03 +01:00
alonso.torres
bd44f49175 🐛 Fix problem with board titles misplaced 2023-03-15 15:51:30 +01:00
alonso.torres
acdcf82c6c 🐛 Fix filter in layers z-index 2023-03-15 15:48:45 +01:00
Alejandro
afb09919ed Merge pull request #3001 from penpot/niwinz-experiments-2
♻️ Refactor concurrency model (start using JDK19 virtual threads on RPC and WebSockets)
2023-03-15 11:34:25 +01:00
Alejandro Alonso
d685888720 Merge remote-tracking branch 'origin/staging' into develop 2023-03-15 09:44:44 +01:00
Alejandro
bda2468a86 Merge pull request #3046 from penpot/alotor-bugfixes-4
Alotor bugfixes 4
2023-03-15 09:43:48 +01:00
alonso.torres
2dea2d9d27 🐛 Ignore remote changes in size 2023-03-15 09:28:46 +01:00
alonso.torres
107d607d37 🐛 Fix error with empty curves 2023-03-15 09:28:46 +01:00
alonso.torres
2c6513ac85 🐛 Fix problems with touch devices and Wacom tablets 2023-03-15 09:28:46 +01:00
Pablo Alba
8ae1148ef9 🎉 Go to main component from context menu or with double click on the asset 2023-03-14 17:15:53 +01:00
alonso.torres
5bd4be1950 🐛 No select frames without fill should happen only on ctrl click 2023-03-14 15:42:46 +01:00
alonso.torres
dad88cb42e 🐛 Fix close colorpicker on Firefox when mouse-up is outside the picker 2023-03-14 15:42:45 +01:00
Alejandro
b6e01077ed Merge pull request #3044 from penpot/azazeln28-improve-rotate-matrix
Improve rotate matrix
2023-03-14 13:42:31 +01:00
Aitor
538a05b359 improve rotate matrix 2023-03-14 13:05:52 +01:00
Alejandro
1b3281457e Merge pull request #3042 from penpot/azazeln28-fix-scaling-frame-proportionally
Fix scaling frame proportionally
2023-03-14 12:33:04 +01:00
Andrey Antukh
c9ec5234d3 ♻️ Refactor local in-memory cache api 2023-03-14 12:30:27 +01:00
Andrey Antukh
76b931108e Increase strenght of password hashing algorithm
And enable password update mechanism on login
2023-03-14 12:30:27 +01:00
Andrey Antukh
84dc3c8fd9 🔥 Remove debugging prn 2023-03-14 12:30:27 +01:00
Andrey Antukh
2cddc49463 Remove several reflection calls 2023-03-14 12:30:27 +01:00
Andrey Antukh
91b5a0afdd Add missing type hints on matrix type functions 2023-03-14 12:30:27 +01:00
Andrey Antukh
dfdc9c9fa5 ♻️ Refactor storage internal concurrency model 2023-03-14 12:30:27 +01:00
Andrey Antukh
aafbf6bc15 ♻️ Refactor cocurrency model on backend
Mainly the followin changes:

- Pass majority of code to the old and plain synchronous style
  and start using virtual threads for the RPC (and partially some
  HTTP server middlewares).
- Make some improvements on how CLIMIT is handled, simplifying code
- Improve considerably performance reducing the reflection and
  unnecesary funcion calls on the whole stack-trace of an RPC call.
- Improve efficiency reducing considerably the total threads number.
2023-03-14 12:30:27 +01:00
Andrey Antukh
2e717882f1 ♻️ Refactor websockets impl to use virtual threads
Removing the use of core.async code and implement code using
plain old and familiar synchronous code
2023-03-14 12:30:27 +01:00
Andrey Antukh
14b53a4d5e Don't log duplicate traceback 2023-03-14 12:30:27 +01:00
Andrey Antukh
04b321caae Add several improvements to internal worker impl
Mainly for make the cron jobs do not block the scheduled executor
and offload all work to a separate threads
2023-03-14 12:30:27 +01:00
Andrey Antukh
cad1851e95 🔥 Replace own scheduled executor with the one defined in promesa lib 2023-03-14 12:30:27 +01:00
Andrey Antukh
012ead65b5 🎉 Add missing ::us/atom global spec 2023-03-14 12:30:27 +01:00
Andrey Antukh
d549fcb2ae 🐛 Pass a valid executor instance to yetti http server 2023-03-14 12:30:27 +01:00
Andrey Antukh
4c85e55176 📎 Improve tests performance making all tables as unlogged 2023-03-14 12:30:27 +01:00
Andrey Antukh
1eb593703f 📎 Update clj-kondo config 2023-03-14 12:30:27 +01:00
Andrey Antukh
771fc1788c 📎 Update backend repl script 2023-03-14 12:30:27 +01:00
Andrey Antukh
ae9886080e 📎 Add better database configuration for devenv 2023-03-14 12:30:27 +01:00
Andrey Antukh
d76baa3266 ⬆️ Update promesa dependency
And adapt all code for breaking changes
2023-03-14 12:30:27 +01:00
Alejandro
37b20571d2 Merge pull request #3041 from penpot/azazeln28-improve-scale-matrix
Improve scale matrix
2023-03-14 12:27:32 +01:00
Alejandro
4661fb26dc Merge pull request #3039 from penpot/alotor-fix-text-sync
Fix text sync problems
2023-03-14 12:22:14 +01:00
Eva
adffdb31f3 Add css variables and theme switch 2023-03-14 11:48:31 +01:00
Aitor
b9559d99da Improve scale matrix computation 2023-03-14 11:24:15 +01:00
alonso.torres
aa4a3ef940 🐛 Fix apply structure modifiers to children 2023-03-14 11:02:11 +01:00
alonso.torres
3a2e1b5c94 Adapt scale to flex elements 2023-03-14 11:02:10 +01:00
Aitor
44c35e6aee 🐛 Fix scaling frame proportionally 2023-03-14 11:01:57 +01:00
alonso.torres
a56dc25fae 🐛 Fix problems with text synchronization 2023-03-13 13:15:36 +01:00
Pablo Alba
4eeef41ed4 🐛 Fix flex layout gaps showing for nested items 2023-03-13 11:52:00 +01:00
Alejandro Alonso
b77f85b697 📎 Prepare new development cycle 2023-03-13 10:39:58 +01:00
Alejandro Alonso
9cd207595f 📎 Prepare new development cycle 2023-03-13 10:37:34 +01:00
Alejandro
c21e0739f2 Merge pull request #3037 from penpot/alotor-bugfixes-3
Bug fixes
2023-03-13 10:34:35 +01:00
alonso.torres
83367dd519 🐛 Fix viewer layers styles 2023-03-13 10:30:12 +01:00
alonso.torres
0d9695de1d 🐛 Fix shortcuts for zoom now take into account the mouse position 2023-03-13 10:30:12 +01:00
alonso.torres
468e61e1e0 🐛 Fix snap pixel when moving path points on high zoom 2023-03-13 10:30:12 +01:00
alonso.torres
481e9b0d32 🐛 Fix unlink library color when blur color picker input 2023-03-13 10:30:12 +01:00
alonso.torres
ce85a1b1d5 🐛 Fix problem with text editor in Safari 2023-03-13 10:22:57 +01:00
Alejandro Alonso
da74d0d732 🐛 Fix viewer wrong translations 2023-03-13 10:21:53 +01:00
Pablo Alba
e6306e5109 Add visualization and mouse control to paddings, margins and gaps in frames with layout 2023-03-10 13:59:50 +01:00
Alejandro
5fae9526d6 Merge pull request #3028 from penpot/alotor-bugfixes-fixes
Fixes after QA revision
2023-03-09 17:39:23 +01:00
alonso.torres
37f52cafc9 🐛 Fix problem with rules when changing pages 2023-03-09 17:28:21 +01:00
alonso.torres
2a632512b3 🐛 Fix select in area of hidden children elements 2023-03-09 16:15:11 +01:00
alonso.torres
079cff0bc0 🐛 Fix problem with undo transactions 2023-03-09 15:53:10 +01:00
Alejandro
7954ad0edf Merge pull request #3025 from penpot/alotor-bugfixes2
Bug fixes
2023-03-09 13:21:32 +01:00
alonso.torres
2500d192e8 🐛 Changed the text dominant-baseline to use ideographic 2023-03-09 10:57:49 +01:00
Pablo Alba
480a72b6e2 🐛 Fix paddings and gaps prediction on create layout 2023-03-09 09:25:10 +01:00
alonso.torres
b2c3dc1504 🐛 Fix problem when loading fonts 2023-03-08 15:22:31 +01:00
alonso.torres
e170011e3c 🐛 Fix problem on selection numeric inputs on Firefox 2023-03-08 15:22:31 +01:00
alonso.torres
f3f611848c 🐛 Improve deeps selection of nested arboards 2023-03-08 15:22:31 +01:00
Alejandro Alonso
c3ce0eb794 Merge remote-tracking branch 'origin/staging' into develop 2023-03-08 07:25:19 +01:00
alonso.torres
1643287775 🐛 Fix problem with area selection 2023-03-07 15:55:39 +01:00
Alejandro Alonso
9e35229ebd 🐛 Fix components texts not displayed in assets panel 2023-03-07 15:22:24 +01:00
alonso.torres
046bd59726 🐛 Fix style for absolute positioning 2023-03-07 14:16:42 +01:00
Alejandro
e8027d3316 Merge pull request #3010 from penpot/niwinz-docker-frontend-2
🐳 Add backend and exporter uri env vars to frontend docker image
2023-03-07 13:08:59 +01:00
Andrey Antukh
ad34ebff89 🐳 Add backend and exporter uri env vars to frontend docker image 2023-03-07 13:08:38 +01:00
Alvaro Araoz
0ead390ef4 🌐 Add translations for: Spanish.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2023-03-07 12:37:50 +01:00
Pablo Hinojosa
aefa73a06f 🌐 Add translations for: Spanish.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2023-03-07 12:37:50 +01:00
Alejandro Alonso
f733497f0f 🐛 Fix some typos on english translation 2023-03-07 10:57:37 +01:00
Alejandro Alonso
ed917fa194 🐛 Fix font translations not detected as markdown 2023-03-07 10:57:37 +01:00
Alejandro Alonso
313df74202 🐛 Fix handle correctly slashes in emails 2023-03-07 10:51:31 +01:00
Alejandro
db7c234053 Merge pull request #3019 from penpot/alotor-bugfixes
Alotor bugfixes
2023-03-07 10:49:57 +01:00
Alejandro Alonso
91c12ca34f 🐛 Fix change colors from selected colors 2023-03-07 10:42:58 +01:00
Alejandro Alonso
9f66e8e5d1 🐛 Fix search field shared styles 2023-03-07 10:37:11 +01:00
alonso.torres
b5be938480 🐛 Improve behavior for undo on text edition 2023-03-07 09:11:51 +01:00
alonso.torres
36583d1171 🐛 Allow selection of empty board by partial rect 2023-03-06 16:27:50 +01:00
alonso.torres
05e13ad05f 🐛 Fix problem when undoing multiple selected colors 2023-03-06 16:27:50 +01:00
alonso.torres
475ce08d3e 🐛 Fix selecting children from hidden parent layers 2023-03-06 16:27:50 +01:00
alonso.torres
6962e15b6d 🐛 Fix error streen when uploading wrong SVG 2023-03-06 16:27:50 +01:00
alonso.torres
7b72906096 🐛 Fix problem on finalize page 2023-03-06 16:27:13 +01:00
Eva Marco
9d43bb4252 Merge pull request #3011 from penpot/alotor-poc-css-modules
 Adds CSS modules to the build pipeline
2023-03-06 15:59:13 +01:00
alonso.torres
7dd24bb79b Merge remote-tracking branch 'origin/staging' into develop 2023-03-06 14:52:43 +01:00
Alejandro
82e402c271 Merge pull request #3012 from penpot/alotor-bug-redo
🐛 Fix problem with redo shortcut
2023-03-06 14:37:28 +01:00
alonso.torres
827ce6c42a 🐛 Fix problem with redo shortcut 2023-03-06 14:23:26 +01:00
alonso.torres
94a98a1866 Adds CSS modules to the build pipeline 2023-03-06 14:20:18 +01:00
Alejandro
0e585cd585 Merge pull request #3002 from penpot/alotor-fixes-rules
🐛 Fix problem with rules position on changing pages
2023-03-06 09:56:50 +01:00
alonso.torres
cd505ecced 🐛 Fix problem with rules position on changing pages 2023-03-03 14:20:53 +01:00
Alejandro
c8360b1994 Merge pull request #2996 from penpot/alotor-grid-layout
Partial merge of the grid layout infrastructure
2023-03-03 11:15:32 +01:00
alonso.torres
a12baf684c Review fixes 2023-03-03 10:53:46 +01:00
Alejandro Alonso
910352280c Merge remote-tracking branch 'origin/staging' into develop 2023-03-03 10:36:29 +01:00
Alejandro Alonso
dec854a012 🐛 Fix full screen not clickable on inspect mode after user entered full screen 2023-03-03 10:31:04 +01:00
Alejandro
03d4e97ad7 Merge pull request #2997 from penpot/alotor-fix-shadow-multi-selection
🐛 Fix problem withs shadows and blur on multiple selection
2023-03-02 16:35:24 +01:00
alonso.torres
e061ba8123 🐛 Fix problem with shadows and blur on multiple selection 2023-03-02 16:32:21 +01:00
alonso.torres
23104b28b6 Edition mode for grid 2023-03-02 14:05:51 +01:00
alonso.torres
b497de0dae UI Integration 2023-03-02 13:56:11 +01:00
Eva
284fc2acbc Add grid cell options 2023-03-02 13:56:11 +01:00
Eva
cc8347a871 Add options to sidebar 2023-03-02 13:56:11 +01:00
alonso.torres
eb425dc4f2 Edit cell panel 2023-03-02 13:56:11 +01:00
alonso.torres
4b7e93ab84 First draft of cell display 2023-03-02 13:56:11 +01:00
alonso.torres
6f99209a62 Grid layout editor interface 2023-03-02 13:51:41 +01:00
alonso.torres
a0cd94cfae Grid layout infrastructure 2023-03-02 13:51:27 +01:00
alonso.torres
2030f987db Performance improvements 2023-03-01 16:38:09 +01:00
Alejandro Alonso
94e87f8a7d Merge remote-tracking branch 'origin/staging' into develop 2023-03-01 16:08:57 +01:00
Alejandro Alonso
9a272f69c7 🐛 Fix height 100% cropped 2023-03-01 14:19:48 +01:00
Alejandro Alonso
fc1f2b2a9f 🐛 Fix some layout tooltips cropped 2023-03-01 14:19:48 +01:00
Alejandro Alonso
89fbe28ed1 🐛 Fix wrap and nowrap spelling issues 2023-03-01 14:19:48 +01:00
Alejandro Alonso
216d101e56 🐛 Fix flex layout min height bigger than board when height is 100% 2023-03-01 14:19:48 +01:00
Aitor
e57262136c Scale content now scales strokes, shadows, blur and corners 2023-03-01 14:11:03 +01:00
Alejandro
0b9bef066b Merge pull request #2989 from penpot/alotor-fix-position-absolute-auto
Fix position absolute auto
2023-03-01 08:38:43 +01:00
alonso.torres
4111cee3d6 🐛 Fix clipping overlay 2023-02-28 15:22:04 +01:00
alonso.torres
0ef5a37e33 🐛 Allow set position when position absolute 2023-02-28 15:22:04 +01:00
alonso.torres
8b5a36a49f 🐛 Fix problem with auto layout an absolute positioning 2023-02-28 15:22:04 +01:00
Eva
c6d1f80af2 🐛 Fix toggle collapse layer icon 2023-02-28 13:09:43 +01:00
Alejandro Alonso
b73b40b23c Merge remote-tracking branch 'origin/staging' into develop 2023-02-28 10:53:09 +01:00
Alejandro Alonso
ccf91a129c 🐛 Fix custom fonts not rendered correctly 2023-02-28 10:43:59 +01:00
Alejandro
1f3f6ce1e9 Merge pull request #2980 from penpot/eva-fix-paste-nested-boards
🐛 Fix copy paste a very nested boards inside itself
2023-02-28 09:51:21 +01:00
Eva
8f2e3d5fe4 🐛 Fix copy paste a very nested boards inside itself 2023-02-28 09:51:12 +01:00
Alejandro
b581752bd5 Merge pull request #2981 from penpot/alotor-small-fixes-flex
🐛 Fix problem when moving absolute positioned element
2023-02-28 09:46:06 +01:00
Alejandro
47481986a1 Merge pull request #2987 from penpot/alotor-fix-layout-from-selected
🐛 Fix problem when creating layout from selection
2023-02-28 09:40:18 +01:00
alonso.torres
9af0e6ca44 🐛 Fix problem when creating layout from selection 2023-02-27 16:43:59 +01:00
Alejandro Alonso
9c419ef114 Merge remote-tracking branch 'origin/staging' into develop 2023-02-27 10:39:11 +01:00
Andrey Antukh
9b5a321a62 📎 Fix tests 2023-02-25 10:24:41 +01:00
Andrey Antukh
738cf6407c 📎 Fix liner issue 2023-02-25 10:24:22 +01:00
Andrey Antukh
1d21ee7089 Merge remote-tracking branch 'origin/staging' into develop 2023-02-24 18:30:05 +01:00
Andrey Antukh
01b361fd3c Fix minor issue on contributing.md rendering output 2023-02-24 14:58:56 +01:00
alonso.torres
4d46460f90 🐛 Fix problem when moving absolute positioned element 2023-02-24 14:26:33 +01:00
alonso.torres
e9942e5527 🐛 Fix position absolute showing on first-level flex containers 2023-02-24 13:18:21 +01:00
Alejandro
8aa0e96377 Merge pull request #2979 from penpot/alotor-small-fixes-absolute
🐛 Fix problems with position absolute
2023-02-24 12:05:56 +01:00
alonso.torres
a12fce1c1f Change names for flex items 2023-02-24 11:24:23 +01:00
alonso.torres
e9d50eb10d 🐛 Fix problems with position absolute 2023-02-24 10:57:56 +01:00
Alejandro
8c618f95f7 Merge pull request #2976 from penpot/alotor-flex-position-absolute
Flex position absolute & z-index
2023-02-24 07:42:43 +01:00
alonso.torres
d309628e1d Add z-index option to flex items elements 2023-02-24 07:37:37 +01:00
alonso.torres
f3f1dbc2d1 Allow for absolute positioned elements inside layout 2023-02-24 07:37:35 +01:00
alonso.torres
664f73b8a5 🐛 Fix problem when converting an empty frame to layout 2023-02-24 07:37:11 +01:00
Alejandro
94f2681223 Merge pull request #2970 from penpot/palba-layout-padding-display
 Add visualization and mouse control to paddings in frames with layout
2023-02-24 07:32:48 +01:00
Eva
a182ca3ab7 🚑 Fix CI 2023-02-23 11:03:54 +01:00
Ondřej Konečný
be865af1fc ♻️ connect values with variables in CSS and remove unused code
Signed-off-by: Ondřej Konečný <ondrej.konecny@gmail.com>
2023-02-23 10:58:27 +01:00
Prithvi Tharun
c6ad8ee110 Improves tooltip content for Corner and Padding options (#2971)
Improves tooltip content for Corner and Padding options

Closes #2964

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2023-02-23 10:41:43 +01:00
Alex Howell
b814a8821c 🌐 Add translations for: Romanian.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2023-02-22 19:37:28 +01:00
Pablo Alba
4d90d36225 Add visualization and mouse control to paddings in frames with layout 2023-02-22 17:19:29 +01:00
Alejandro
fd673b39a4 Merge pull request #2959 from penpot/azazeln28-visual-feedback-scale-k
 add visual feedback to scale text
2023-02-22 12:39:19 +01:00
Aitor
1758b34eed add visual feedback to scale text 2023-02-22 12:38:55 +01:00
Alejandro Alonso
16bd5e2ebc Merge remote-tracking branch 'origin/staging' into develop 2023-02-22 11:48:41 +01:00
Eva
c892411484 📎 Update changes file 2023-02-21 15:56:23 +01:00
Locness
9dead29ac3 🌐 Add translations for: French.
Currently translated at 85.6% (1014 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2023-02-20 22:39:47 +01:00
Aitor
96ce475206 Merge pull request #2952 from penpot/eva-ally-context-3
Add new accessibility functionality to the dashboard
2023-02-20 13:30:42 +01:00
Andrey Antukh
788dc9b3f8 Merge branch 'staging' into develop 2023-02-20 13:29:03 +01:00
Andrey Antukh
80af0bb148 Merge branch 'main' into develop 2023-02-20 13:28:01 +01:00
Eva
fcb8b15ef2 Add new accessibility functionalities to dashboard 2023-02-17 15:17:24 +01:00
Alejandro
9e190d9810 Merge pull request #2945 from penpot/palba-layout-predictive-gap2
 Adds paddings and gaps prediction on layout creation
2023-02-15 13:43:01 +01:00
Pablo Alba
85a47e36b5 Adds paddings and gaps prediction on layout creation 2023-02-15 12:42:23 +01:00
Alex Howell
0dac87f2bc 🌐 Add translations for: Romanian.
Currently translated at 98.6% (1168 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2023-02-13 20:36:42 +01:00
Alejandro
4852882c28 Merge pull request #2938 from penpot/alotor-fix-size-auto-center-align
🐛 Fix problem with align center and size auto
2023-02-13 16:58:15 +01:00
alonso.torres
f3c5aed5d0 🐛 Fix problem with align center and size auto 2023-02-13 16:47:28 +01:00
Renan Castro
e4b17bdbcf 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2023-02-12 03:37:37 +01:00
Andrey Antukh
c0eb20d31d 🐛 Add missing require on rpc ns 2023-02-11 00:59:08 +01:00
Andrey Antukh
f23d29deb7 🐛 Fix unexpected exception on logger caused by log4j2 plugin 2023-02-11 00:52:23 +01:00
Vin
28b0a8f7d7 🌐 Add translations for: Russian.
Currently translated at 64.3% (762 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2023-02-10 22:35:42 +01:00
alonso.torres
cdd268afbc Merge remote-tracking branch 'origin/staging' into develop 2023-02-10 15:06:23 +01:00
Eva Marco
b2b224e5a7 Merge pull request #2923 from ondrejkonec/BUG-fix-icon-resizing-on-small-displays
🐛 Add min-width property to avoid shrinking on icons
2023-02-09 13:17:34 +01:00
Stas Haas
f9c964b65e 🌐 Add translations for: German.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-02-08 15:36:48 +01:00
Ondřej Konečný
4b9d6fc794 added width property to avoid shrinking on icons 2023-02-08 12:16:34 +01:00
Pablo Alba
c3c6e533e3 Merge pull request #2903 from ondrejkonec/a11y-udpate-change-hover-color-for-zoom-widget-button
 Added darker color for hover button in zoom widget to improve a11y
2023-02-08 11:40:38 +01:00
Pablo Alba
af30df58dc Merge pull request #2902 from glippi/drop-shadow-negative-spread
 Allow negative values for shadow spread
2023-02-08 11:31:23 +01:00
Alejandro
3587362c4a Merge pull request #2909 from penpot/niwinz-refactor-remove-legacy
🔥 Remove legacy code and internal refactor of storage module
2023-02-08 10:34:18 +01:00
Andrey Antukh
06a30316c2 🐛 Fix logging initialization on exporter 2023-02-07 19:10:57 +01:00
Andrey Antukh
8161d3ae09 🐛 Fix incorrect props cleaning on auditlog 2023-02-07 18:38:54 +01:00
Andrey Antukh
ea470068bb ⬆️ Update jdk and postgresql-client on devenv 2023-02-07 18:17:36 +01:00
Andrey Antukh
e3378181ee 📎 Update docker log4j config 2023-02-07 18:17:08 +01:00
Andrey Antukh
9162f0e1fd 🐛 Fix auth flag handling on rpc metadata 2023-02-07 18:16:55 +01:00
Andrey Antukh
69556f19ac Add more logging to OIDC providers 2023-02-07 18:16:55 +01:00
Andrey Antukh
ab3b9cba45 ♻️ Refactor storage and assets related modules
- improve internal error handling
- add more specs and more asserts
2023-02-07 18:16:55 +01:00
Andrey Antukh
4b4f78b4cc Add minor change to srepl module options
Replace unqualified attrs with fully qualified
2023-02-07 18:16:55 +01:00
Andrey Antukh
0c48f76911 Add better spec validation on http module 2023-02-07 18:16:55 +01:00
Andrey Antukh
3cf4a3facc Improve db/pool initialization and reusability 2023-02-07 18:16:55 +01:00
Andrey Antukh
41d34de9e1 🔥 Remove zmq mentions on devenv log4j config 2023-02-07 18:16:55 +01:00
Andrey Antukh
dfdebc35c8 💄 Improve readability on error report templates 2023-02-07 18:16:55 +01:00
Andrey Antukh
bd2745d1fe ♻️ Clean email related namespaces
- Remove legacy and outdated mjml directory
- Rename namespace to a proper name
- Add more specs
2023-02-07 18:16:55 +01:00
Andrey Antukh
64f2d874fe Merge remote-tracking branch 'origin/staging' into develop 2023-02-07 18:16:37 +01:00
Andrey Antukh
58668c11f3 Merge remote-tracking branch 'origin/staging' into develop 2023-02-07 14:46:18 +01:00
elhombretecla
bab1a417df Update README.md 2023-02-07 11:55:32 +01:00
elhombretecla
b16718bfe4 Update README.md 2023-02-07 11:54:48 +01:00
elhombretecla
8f58bb4f2c Update README.md 2023-02-07 11:51:16 +01:00
elhombretecla
9cdb25344b Update README.md 2023-02-07 11:49:45 +01:00
elhombretecla
96ce631784 review readme 2023-02-07 11:32:59 +01:00
Yaron Shahrabani
bd5f19cbd0 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2023-02-06 16:38:02 +01:00
Fernando Krik
cf1fa99399 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 96.4% (1142 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2023-02-06 16:38:01 +01:00
Alvaro Araoz
4c3025ab24 🌐 Add translations for: Spanish.
Currently translated at 99.8% (1182 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2023-02-06 16:38:00 +01:00
Andrey Antukh
01d463b4aa Merge branch 'cclauss-patch-1' into develop 2023-02-05 11:19:12 +01:00
Christian Clauss
58001f367a 🐛 Fix undefined name RuntimeException on manage.py script
Python defines [`RuntimeError`](https://docs.python.org/3.7/library/exceptions.html#RuntimeError)
but it does not define `RuntimeException` so a `NameError` will be raised when any of these lines
are executed.

% `python3 -c "RuntimeException('This is a test...')"`
```
Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'RuntimeException' is not defined
```

% `flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics`
```
./backend/scripts/manage.py:22:15: F821 undefined name 'RuntimeException'
        raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
              ^
./backend/scripts/manage.py:25:15: F821 undefined name 'RuntimeException'
        raise RuntimeException(f"invalid PREPL_URI: {PREPL_URI}")
              ^
./backend/scripts/manage.py:49:23: F821 undefined name 'RuntimeException'
                raise RuntimeException("unexpected response from PREPL")
                      ^
3     F821 undefined name 'RuntimeException'
3
```
2023-02-05 11:18:01 +01:00
Andrey Antukh
517210eeb5 Merge branch 'mabasic-patch-1' into develop 2023-02-05 11:15:49 +01:00
Mario Bašić
22034c22c6 🐛 Add mailcatch to penpot network on docker compose
Without this the backend complains that it cannot connect to the smtp host (when using mailcatcher). The reason is because the mailcatcher is not on the same network as the backend application.
2023-02-05 11:15:01 +01:00
im424
33a67bc61c 🌐 Add translations for: Chinese (Traditional).
Currently translated at 52.5% (622 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2023-02-04 12:38:27 +01:00
Yaron Shahrabani
b0e89ed563 🌐 Add translations for: Hebrew.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2023-02-04 12:38:27 +01:00
Stas Haas
5eb08d9c4e 🌐 Add translations for: German.
Currently translated at 99.4% (1177 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-02-04 12:38:26 +01:00
Dias Stebanäk
59ec8c5c78 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 93.6% (1109 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2023-02-04 12:38:25 +01:00
Ondřej Konečný
9fae26765a added darker color for hover button in zoom widget to improve a11y 2023-02-03 13:19:21 +01:00
Alejandro Alonso
2ea81c0114 Merge remote-tracking branch 'origin/staging' into develop 2023-02-02 18:06:59 +01:00
Alejandro
a4cef16ef2 Merge pull request #2895 from penpot/niwinz-refactor-logging
♻️ Refactor logging and error reporting
2023-02-02 18:05:47 +01:00
Andrey Antukh
1325e46192 Improve internal state validation on db module 2023-02-02 14:20:13 +01:00
Andrey Antukh
071ecca875 🐛 Fix internal executor naming issue 2023-02-02 13:38:04 +01:00
Andrey Antukh
d91e6e381e 🔧 Do not compile clj source (allow dynamic instrumentation on runtime) 2023-02-02 13:38:04 +01:00
Andrey Antukh
b54bf2bba4 📎 Add helpers for instrumenting vars 2023-02-02 13:38:04 +01:00
Andrey Antukh
32b8a2c243 ⬆️ Update dependencies on backend and common 2023-02-02 13:38:04 +01:00
Andrey Antukh
bb055a3c84 ♻️ Refactor logging subsystem and error reporting 2023-02-02 13:38:04 +01:00
glippi
57a89b733e Allow negative values for shadow spread 2023-02-02 10:56:58 +01:00
Andrey Antukh
50ee0ad3fd Merge remote-tracking branch 'origin/staging' into develop 2023-02-01 23:25:25 +01:00
Andrés Moya
b7c55b4700 💄 Move all README images to a subfolder 2023-02-01 21:09:17 +01:00
im424
9f52da90c3 🌐 Add translations for: Chinese (Traditional).
Currently translated at 48.2% (571 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2023-02-01 17:38:09 +01:00
Linerly
fac6e8a20e 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2023-02-01 17:38:09 +01:00
Stas Haas
38ebf9c3b4 🌐 Add translations for: German.
Currently translated at 99.1% (1174 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-02-01 17:38:08 +01:00
Andrey Antukh
6240323704 💄 Add minor cosmetic changes to common.spec ns 2023-02-01 16:39:59 +01:00
Andrey Antukh
d666564112 🐛 Fix loading issue on app.rpc ns 2023-02-01 16:39:59 +01:00
Andrey Antukh
f4d4559cd4 💄 Add cosmetic improvemnts on http client validation 2023-02-01 16:39:59 +01:00
Alejandro Alonso
e9c3b0567b Merge remote-tracking branch 'origin/staging' into develop 2023-02-01 13:24:39 +01:00
Andrey Antukh
56cf7064f5 Merge remote-tracking branch 'origin/staging' into develop 2023-01-31 23:04:26 +01:00
Alejandro Alonso
a905f49721 Merge remote-tracking branch 'origin/staging' into develop 2023-01-31 10:29:41 +01:00
im424
9328065954 🌐 Add translations for: Chinese (Traditional).
Currently translated at 43.4% (515 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2023-01-30 18:46:13 +01:00
nautilusx
c7a8d977ec 🌐 Add translations for: German.
Currently translated at 98.9% (1172 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-01-30 18:46:13 +01:00
Alejandro Alonso
11db7590eb Merge remote-tracking branch 'origin/staging' into develop 2023-01-30 15:39:17 +01:00
Andrés Moya
e1d1ecbc24 Merge remote-tracking branch 'origin/staging' into develop 2023-01-30 12:47:20 +01:00
im424
eb556fab10 🌐 Add translations for: Chinese (Traditional).
Currently translated at 35.5% (421 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2023-01-28 17:44:53 +01:00
Yaron Shahrabani
bf60fc6d82 🌐 Add translations for: Hebrew.
Currently translated at 94.0% (1114 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2023-01-26 17:52:24 +01:00
Linerly
53761bf802 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2023-01-26 17:52:24 +01:00
Stas Haas
3bf4ac6ea1 🌐 Add translations for: German.
Currently translated at 98.1% (1162 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2023-01-26 17:52:23 +01:00
Oğuz Ersen
45f12de546 🌐 Add translations for: Turkish.
Currently translated at 100.0% (1184 of 1184 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2023-01-26 17:52:22 +01:00
Hosted Weblate
4a46cf2ab7 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2023-01-24 15:44:15 +01:00
Andrés Moya
30725af367 📚 Validate translations 2023-01-24 15:42:34 +01:00
Andrés Moya
ece324a76f Merge remote-tracking branch 'weblate/develop' into translations 2023-01-24 15:39:47 +01:00
Amerey.eu
2ea69a84b2 🌐 Add translations for: Czech.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2023-01-24 15:27:43 +01:00
Mikel Larreategi
f2f0d292e0 🌐 Add translations for: Basque.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2023-01-24 15:27:40 +01:00
Ahmad HosseinBor
fc0fad29d0 🌐 Add translations for: Persian.
Currently translated at 59.9% (728 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2023-01-24 15:27:40 +01:00
Linerly
9a954ab430 🌐 Add translations for: Indonesian.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2023-01-24 15:27:39 +01:00
Vin
90caaaa14a 🌐 Add translations for: Russian.
Currently translated at 68.3% (831 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2023-01-24 15:27:36 +01:00
GradelerM
98360ed9e8 🌐 Add translations for: French.
Currently translated at 93.1% (1132 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2023-01-24 15:27:35 +01:00
Rubén
f64a74e7b9 🌐 Add translations for: Catalan.
Currently translated at 95.8% (1164 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2023-01-24 15:27:35 +01:00
Alejandro Alonso
aae78055c8 Merge remote-tracking branch 'origin/staging' into develop 2023-01-24 10:00:51 +01:00
Alejandro Alonso
6b25bf6c4f Merge remote-tracking branch 'origin/staging' into develop 2023-01-23 11:06:05 +01:00
Alejandro
125e6238d1 Merge pull request #2821 from penpot/niwinz-dev-bugfixes
🐛 Bugfixes + unit tests
2023-01-23 10:35:55 +01:00
Andrey Antukh
504f75a1cf 🐛 Fix health check http endpoint 2023-01-23 09:59:55 +01:00
Andrey Antukh
fa17ce5d40 📎 Avoid email index change on profile indexes migration 2023-01-23 09:56:21 +01:00
Andrey Antukh
14f39b8028 🎉 Add unit tests for access tokens rpc methods 2023-01-23 09:56:21 +01:00
Andrey Antukh
7e9a5c4a8f Merge remote-tracking branch 'origin/staging' into develop 2023-01-23 09:55:50 +01:00
Alejandro Alonso
b8043a9755 Merge remote-tracking branch 'origin/staging' into develop 2023-01-20 15:52:04 +01:00
Alejandro Alonso
da517f2d35 Merge remote-tracking branch 'origin/staging' into develop 2023-01-20 13:58:08 +01:00
Andrey Antukh
62aa6569f2 Merge remote-tracking branch 'origin/staging' into develop 2023-01-20 09:59:56 +01:00
Alejandro
42e97f8be1 Merge pull request #2793 from penpot/niwinz-access-tokens
🎉 ♻️ Refactor & Access Tokens (part 1)
2023-01-20 09:36:47 +01:00
Andrey Antukh
9a407ab714 🎉 Add namespace with a set of helpers for access throught the BREPL 2023-01-19 12:42:39 +01:00
andrés gonzález
750e00c981 Merge pull request #2803 from iprithvitharun/2791-renaming-export-board-option
 Renamed Export boards to PDF option
2023-01-19 09:55:18 +01:00
Prithvi Tharun
d2847e9507 Renamed Export boards to PDF option
Two instances of this changed

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2023-01-18 23:09:39 +05:30
Andrey Antukh
8a5afefc1c 🎉 Add prepl support
And rename the current repl to urepl (user-repl).
2023-01-18 17:14:22 +01:00
Andrey Antukh
3dd65db651 Use commands instead of mutations for assets upload
And properly deprecate media rpc mutations
2023-01-18 11:20:36 +01:00
Andrey Antukh
1e1f551383 Move share link mutations to commands 2023-01-18 10:51:58 +01:00
Andrey Antukh
4258a840ac ♻️ Use proper namespace qualified access to pool 2023-01-18 10:51:58 +01:00
Andrey Antukh
bca98f91e4 🎉 Add rpc methods for access tokens 2023-01-18 10:51:58 +01:00
Andrey Antukh
a79d2cf899 🔥 Remove deprecated teams mutations and queries 2023-01-18 10:51:58 +01:00
Andrey Antukh
6a699d7f09 Properly move viewer queries to commands
And change deprecation version on viewer queries
2023-01-18 10:51:58 +01:00
Andrey Antukh
ba2729fa4a Move fonts queries and mutations to commands 2023-01-18 10:51:58 +01:00
Andrey Antukh
dba7a9d424 Move projects queries and mutations to commands 2023-01-18 10:51:58 +01:00
Andrey Antukh
dc77c6b655 Remove deprecated code and reoganize file related methods 2023-01-18 10:51:58 +01:00
Andrey Antukh
ed87814f50 🐛 Properly handle storage features on binfile import 2023-01-18 10:51:58 +01:00
Andrey Antukh
d8faff47a8 ♻️ Move profile queries and mutations to commands 2023-01-18 10:51:58 +01:00
Andrey Antukh
ecb757bcaf 🎉 Move user feedback http handler to RPC command method 2023-01-18 10:51:58 +01:00
Andrey Antukh
73a6f0a347 📎 Update backend scripts/repl file 2023-01-18 10:51:58 +01:00
Andrey Antukh
db689d151e ♻️ Refactor profile and session handling
- makes the profile access more efficient (replace in-app joins to a
  simple select query on profile table
- add partial support for access-tokens (still missing some RPC methods)
- move router definitions to specific modules and simplify the main http
  module definitions to simple includes
- simplifiy authentication code related to access-tokens and sessions
- normalize db parameters with proper namespaced props
- more work on convert all modules initialization to use proper specs
  with fully-qualified keyword config props
2023-01-18 10:51:58 +01:00
Ahmad HosseinBor
6bdd25b5d1 🌐 Add translations for: Persian.
Currently translated at 56.3% (685 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2023-01-18 06:48:14 +01:00
andrés gonzález
a7ec9d7d1f Merge pull request #2790 from iprithvitharun/2789-inconsistent-casing-fixes
  Fixes wrong casing
2023-01-17 08:58:03 +01:00
Alejandro Alonso
7f9911f164 Merge remote-tracking branch 'origin/staging' into develop 2023-01-16 17:14:56 +01:00
Prithvi Tharun
0e07617877 Fixes wrong casing
several casings fixed

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2023-01-16 21:04:50 +05:30
Alejandro Alonso
c78cb89943 Merge remote-tracking branch 'origin/staging' into develop 2023-01-16 16:21:35 +01:00
Rubén
42b8c3669f 🌐 Add translations for: Catalan.
Currently translated at 95.5% (1161 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2023-01-16 15:50:44 +01:00
Alejandro Alonso
6c0a8afba2 Merge remote-tracking branch 'origin/staging' into develop 2023-01-13 15:00:30 +01:00
Alejandro Alonso
19bac6bd10 Merge remote-tracking branch 'origin/staging' into develop 2023-01-13 14:37:06 +01:00
andrés gonzález
1f5fb43454 Merge pull request #2771 from iprithvitharun/2770-update-position-x-y-tooltips
 Improves tooltip
2023-01-13 09:48:52 +01:00
Prithvi Tharun
cd3f1d5ded Improves tooltip
X and Y position tooltips updated to communicate more info

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2023-01-12 20:57:46 +05:30
Alejandro Alonso
47c983ed88 Merge remote-tracking branch 'origin/staging' into develop 2023-01-12 13:33:23 +01:00
Alejandro Alonso
c39c58198d Merge remote-tracking branch 'origin/staging' into develop 2023-01-12 13:14:25 +01:00
Alejandro Alonso
76c9f11922 Merge remote-tracking branch 'origin/staging' into develop 2023-01-11 12:45:05 +01:00
Alejandro Alonso
a25f069f8e Merge remote-tracking branch 'origin/staging' into develop 2023-01-10 11:27:38 +01:00
andrés gonzález
d87bc5fa1b Merge pull request #2682 from iprithvitharun/2678-updating-custom-fonts-empty-state-content
 Improves empty state content
2023-01-10 11:17:09 +01:00
andrés gonzález
5a482298e8 Merge pull request #2683 from iprithvitharun/2679-updating-empty-state-content-libraries-section
 Improves empty state content
2023-01-10 11:16:00 +01:00
Alejandro Alonso
34d874f56d Merge remote-tracking branch 'origin/staging' into develop 2023-01-09 16:41:40 +01:00
Alejandro
762681a421 Merge pull request #2747 from penpot/azazeln28-fix-import-dialog-react-duplicate-key-warning
🐛 Fix import dialog React duplicate key warning
2023-01-09 12:44:04 +01:00
Aitor Moreno
b73ab37c94 🐛 Fix import dialog React duplicate key warning 2023-01-09 12:28:41 +01:00
K.B.Dharun Krishna
0039585848 🌐 Add translations for: Tamil.
Currently translated at 2.7% (34 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ta/
2023-01-06 11:48:00 +01:00
Alejandro Alonso
df5ccb6e77 Merge remote-tracking branch 'origin/staging' into develop 2023-01-05 13:30:16 +01:00
Andrés Moya
be5e7f1536 💄 Fix line removed by merge 2023-01-05 09:59:30 +01:00
Andrés Moya
d68f53733d Merge remote-tracking branch 'origin/staging' into develop 2023-01-05 09:58:20 +01:00
Andrés Moya
dba90726c1 💄 Clean up CHANGES.md 2023-01-05 09:54:33 +01:00
Alejandro Alonso
84dcd8f89c Merge remote-tracking branch 'origin/staging' into develop 2023-01-05 09:45:14 +01:00
Alejandro Alonso
3b0bded82c Merge remote-tracking branch 'origin/staging' into develop 2023-01-05 07:40:25 +01:00
Alejandro Alonso
dd0ac64e28 Merge remote-tracking branch 'origin/staging' into develop 2023-01-04 09:25:30 +01:00
Alejandro Alonso
aa56e2cdcf Merge remote-tracking branch 'origin/staging' into develop 2023-01-02 14:18:00 +01:00
Alejandro Alonso
07ac43ec0e Merge remote-tracking branch 'origin/staging' into develop 2023-01-02 14:07:56 +01:00
Alejandro Alonso
31661d5484 Merge remote-tracking branch 'origin/staging' into develop 2023-01-02 13:48:36 +01:00
Alejandro Alonso
9c44cd343f Merge remote-tracking branch 'origin/staging' into develop 2022-12-30 07:46:38 +01:00
Alejandro Alonso
2d22f575a0 Merge remote-tracking branch 'origin/staging' into develop 2022-12-29 14:26:00 +01:00
Alejandro Alonso
87a264ae40 Merge remote-tracking branch 'origin/staging' into develop 2022-12-29 13:15:15 +01:00
Alejandro
690090acb4 Merge pull request #2706 from penpot/alotor-flex-improvements
Flex improvements
2022-12-29 12:06:47 +01:00
alonso.torres
3c5be31222 Reorder layers through keys in flex layout 2022-12-29 11:16:54 +01:00
alonso.torres
a66b40d79e Changes to the margin-item and min/max width/height 2022-12-29 11:11:37 +01:00
alonso.torres
7e31c55e37 Support hidden elements in flex layout 2022-12-29 11:11:36 +01:00
Alejandro Alonso
9e30f974ef Merge remote-tracking branch 'origin/staging' into develop 2022-12-29 10:07:20 +01:00
Fernando Krik
5e6d079fea 🌐 Add translations for: Portuguese (Portugal).
Currently translated at 99.9% (1214 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2022-12-29 00:47:37 +01:00
Alejandro Alonso
4cc841d629 Merge remote-tracking branch 'origin/staging' into develop 2022-12-28 13:17:11 +01:00
Alejandro Alonso
1497e8ef0f Merge remote-tracking branch 'origin/staging' into develop 2022-12-28 11:55:30 +01:00
Fernando Krik
4c392e3a31 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2022-12-27 23:22:16 +01:00
Alejandro Alonso
145d6f831a 📎 Prepare new development cycle 2022-12-27 12:04:02 +01:00
matl-17
8aa1f29865 🌐 Add translations for: Czech.
Currently translated at 15.8% (192 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2022-12-22 01:45:30 +01:00
Midka
62b730f5f0 🌐 Add translations for: Finnish (fin_FI).
Currently translated at 5.0% (61 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fin_FI/
2022-12-20 21:48:51 +01:00
Prithvi Tharun
f35095e053 Improves empty state content
Better instructions explaining multiple ways people can add files to library. Also, reads in neutral tone withour blaming and alarming the users

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2022-12-20 23:30:37 +05:30
Prithvi Tharun
9e3515619d Improves empty state content
Reads in neutral content without alarming the users

Signed-off-by: Prithvi Tharun <ptrithu8@gmail.com>
2022-12-20 22:35:05 +05:30
andy
de7fb393c9 🌐 Added translation for: Finnish (fin_FI). 2022-12-19 10:22:15 +01:00
Ahmad HosseinBor
fed320be36 🌐 Add translations for: Persian.
Currently translated at 55.3% (673 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-12-17 17:49:47 +01:00
Ahmad HosseinBor
1b30d023ef 🌐 Add translations for: Persian.
Currently translated at 54.9% (668 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2022-12-10 16:48:15 +01:00
Alex Howell
806a818cb3 🌐 Add translations for: Romanian.
Currently translated at 99.5% (1210 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2022-12-10 16:48:15 +01:00
GradelerM
4014fec195 🌐 Add translations for: French.
Currently translated at 92.0% (1119 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-12-10 16:48:14 +01:00
Pablo Alba
cae0311db6 🌐 Added translation for: Korean. 2022-12-09 16:36:55 +01:00
María Ozámiz
7c6dfef1c6 🌐 Add translations for: Galician.
Currently translated at 33.9% (412 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/gl/
2022-12-06 22:48:04 +01:00
Alex Howell
51440964a7 🌐 Add translations for: Romanian.
Currently translated at 76.4% (929 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2022-12-06 22:48:04 +01:00
Ally Tiago
f7a819fd57 🌐 Add translations for: Portuguese (Brazil).
Currently translated at 99.9% (1214 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2022-12-06 22:48:03 +01:00
Salman Hossain Saif
378b9f3f67 🌐 Add translations for: Bengali.
Currently translated at 1.5% (19 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/bn/
2022-12-03 15:47:26 +01:00
María Ozámiz
cb3a7a1da0 🌐 Add translations for: Galician.
Currently translated at 31.1% (379 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/gl/
2022-12-03 15:47:25 +01:00
andy
6f4b533fc7 🌐 Added translation for: Bengali. 2022-12-02 14:10:07 +01:00
GradelerM
dbdc656e3e 🌐 Add translations for: French.
Currently translated at 89.1% (1083 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-12-01 11:47:02 +01:00
Dário
797aa68bfa 🌐 Add translations for: Chinese (Simplified).
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2022-11-29 15:48:08 +01:00
HIYIZI
80c17e5dcf 🌐 Add translations for: Chinese (Simplified).
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2022-11-29 15:48:07 +01:00
K.B.Dharun Krishna
7083c4e111 🌐 Add translations for: Tamil.
Currently translated at 2.1% (26 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ta/
2022-11-28 15:06:27 +01:00
HIYIZI
e0e0f0a9b1 🌐 Add translations for: Chinese (Simplified).
Currently translated at 99.5% (1210 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2022-11-28 15:06:27 +01:00
Maemolee
b57c5ec92a 🌐 Add translations for: Chinese (Simplified).
Currently translated at 99.5% (1210 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2022-11-28 15:06:27 +01:00
Bogi Napoleon Wennerstrøm
08eb2bceb1 🌐 Add translations for: Faroese.
Currently translated at 14.1% (172 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2022-11-18 20:48:29 +01:00
Tummas Jóhan Sigvardsen
f439d10128 🌐 Add translations for: Faroese.
Currently translated at 13.2% (161 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2022-11-17 20:21:01 +01:00
Bogi Napoleon Wennerstrøm
b87022ef28 🌐 Add translations for: Faroese.
Currently translated at 13.2% (161 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2022-11-17 20:21:01 +01:00
Stas Haas
17d1c16d9c 🌐 Add translations for: German.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-11-05 11:09:39 +01:00
Tummas Jóhan Sigvardsen
0e3675ce1f 🌐 Add translations for: Faroese.
Currently translated at 11.6% (141 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2022-11-01 15:29:29 +01:00
Bogi Napoleon Wennerstrøm
92cd4693f4 🌐 Add translations for: Faroese.
Currently translated at 11.6% (141 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2022-11-01 15:29:28 +01:00
Tatsuto Yamamoto
7905b9fbeb 🌐 Add translations for: Japanese (jpn_JP).
Currently translated at 21.8% (265 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/jpn_JP/
2022-11-01 15:29:28 +01:00
Mikel Larreategi
0b4318b32c 🌐 Add translations for: Basque.
Currently translated at 99.5% (1210 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2022-10-29 15:03:18 +02:00
Stas Haas
0fd80bedf2 🌐 Add translations for: German.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-10-29 15:03:17 +02:00
Marius
380f297af3 🌐 Add translations for: German.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-10-29 15:03:17 +02:00
Henrik Steffens
f8f0944816 🌐 Add translations for: German.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-10-29 15:03:17 +02:00
nautilusx
a5f833759a 🌐 Add translations for: German.
Currently translated at 100.0% (1215 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2022-10-29 15:03:16 +02:00
Tummas Jóhan Sigvardsen
7ab90c6b6f 🌐 Add translations for: Faroese.
Currently translated at 9.0% (110 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fo/
2022-10-27 15:02:46 +02:00
Pablo Alba
a5a0d51ca7 🌐 Add translations for: French.
Currently translated at 87.9% (1069 of 1215 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2022-10-27 15:02:46 +02:00
803 changed files with 46682 additions and 21748 deletions

View File

@@ -2,6 +2,7 @@
{promesa.core/let clojure.core/let
promesa.core/->> clojure.core/->>
promesa.core/-> clojure.core/->
promesa.exec.csp/go-loop clojure.core/loop
rumext.v2/defc clojure.core/defn
rumext.v2/fnc clojure.core/fn
app.common.data/export clojure.core/def

View File

@@ -1,4 +1,192 @@
# CHANGELOG
## :rocket: 1.19.0
### :boom: Breaking changes & Deprecations
### :sparkles: New features
- Default naming of text layers [Taiga #2836](https://tree.taiga.io/project/penpot/us/2836)
- Create typography style from a selected text layer[Taiga #3041](https://tree.taiga.io/project/penpot/us/3041)
- Board as ruler origin [Taiga #4833](https://tree.taiga.io/project/penpot/us/4833)
- Access tokens support [Taiga #4460](https://tree.taiga.io/project/penpot/us/4460)
- Show interactions setting at the view mode [Taiga #1330](https://tree.taiga.io/project/penpot/issue/1330)
### :bug: Bugs fixed
- Fix files can be opened from multiple urls [Taiga #5310](https://tree.taiga.io/project/penpot/issue/5310)
- Fix asset color item was created from the selected layer [Taiga #5180](https://tree.taiga.io/project/penpot/issue/5180)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
- Palettes (color, typographies) empty state (by @akshay-gupta7) [Github #3160](https://github.com/penpot/penpot/pull/3160)
- Duplicate objects via drag + alt (by @akshay-gupta7) [Github #3147](https://github.com/penpot/penpot/pull/3147)
- Set line-height to auto as 1.2 (by @akshay-gupta7) [Github #3185](https://github.com/penpot/penpot/pull/3185)
- Click to select full values at the design sidebar (by @akshay-gupta7) [Github #3179](https://github.com/penpot/penpot/pull/3179)
- Fix rect filter bounds math (by @ryanbreen) [Github #3180](https://github.com/penpot/penpot/pull/3180)
- Removed sizing variables from radius (by @ondrejkonec) [Github #3184](https://github.com/penpot/penpot/pull/3184)
- Dashboard search, set focus after shortcut (by @akshay-gupta7) [Github #3196](https://github.com/penpot/penpot/pull/3196)
- Library name dropdown arrow is overlapped by library name (by @ondrejkonec) [Taiga #5200](https://tree.taiga.io/project/penpot/issue/5200)
- Reorder shadows (by @akshay-gupta7) [Github #3236](https://github.com/penpot/penpot/pull/3236)
- Open project in new tab from workspace (by @akshay-gupta7) [Github #3246](https://github.com/penpot/penpot/pull/3246)
- Distribute fix enabled when two elements were selected (by @dfelinto) [Github #3266](https://github.com/penpot/penpot/pull/3266)
- Distribute vertical spacing failing for overlapped text (by @dfelinto) [Github #3267](https://github.com/penpot/penpot/pull/3267)
## 1.18.6
### :bug: Bugs fixed
- Fix comments navigation from workspace [Taiga #5504](https://tree.taiga.io/project/penpot/issue/5504)
### :sparkles: Enhancements
- Add the ability to overwrite internal resolver with `PENPOT_INTERNAL_RESOLVER` environment
variable [GH #3310](https://github.com/penpot/penpot/issues/3310)
## 1.18.5
### :bug: Bugs fixed
- Fix add flow option in contextual menu for frames
- Fix issues related with invitations
- Fix problem with undefined gaps
- Add deleted fonts auto match mechanism
## 1.18.4
### :bug: Bugs fixed
- Fix zooming while color picker breaks UI [GH #3214](https://github.com/penpot/penpot/issues/3214)
- Fix problem with layout not reflowing on shape deletion [Taiga #5289](https://tree.taiga.io/project/penpot/issue/5289)
- Fix extra long typography names on assets and palette [Taiga #5199](https://tree.taiga.io/project/penpot/issue/5199)
- Fix background-color property on inspect code [Taiga #5300](https://tree.taiga.io/project/penpot/issue/5300)
- Preview layer blend modes (by @akshay-gupta7) [Github #3235](https://github.com/penpot/penpot/pull/3235)
## 1.18.3
### :bug: Bugs fixed
- Fix problem with rulers not placing correctly [Taiga #5093](https://tree.taiga.io/project/penpot/issue/5093)
- Fix page context menu [Taiga #5145](https://tree.taiga.io/project/penpot/issue/5145)
- Fix project file count [Taiga #5148](https://tree.taiga.io/project/penpot/issue/5148)
- Fix OIDC roles checking mechanism [GH #3152](https://github.com/penpot/penpot/issues/3152)
- Import updated translation strings from weblate
### :arrow_up: Deps updates
## 1.18.2
### :bug: Bugs fixed
- Fix problem with frame title rotation
- Fix first level board "Show in view mode" is automatically unchecked [Taiga #5136](https://tree.taiga.io/project/penpot/issue/5136)
## 1.18.1
### :bug: Bugs fixed
- Fix problems with imported SVG shadows [Taiga #4922](https://tree.taiga.io/project/penpot/issue/4922)
- Fix problems with imported SVG embedded images and transforms [Taiga #4639](https://tree.taiga.io/project/penpot/issue/4639)
## 1.18.0
### :sparkles: New features
- Adds more accessibility improvements in dashboard [Taiga #4577](https://tree.taiga.io/project/penpot/us/4577)
- Adds paddings and gaps prediction on layout creation [Taiga #4838](https://tree.taiga.io/project/penpot/task/4838)
- Add visual feedback when proportionally scaling text elements with **K** [Taiga #3415](https://tree.taiga.io/project/penpot/us/3415)
- Add visualization and mouse control to paddings, margins and gaps in frames with layout [Taiga #4839](https://tree.taiga.io/project/penpot/task/4839)
- Allow for absolute positioned elements inside layout [Taiga #4834](https://tree.taiga.io/project/penpot/us/4834)
- Add z-index option for flex layout items [Taiga #2980](https://tree.taiga.io/project/penpot/us/2980)
- Scale content proportionally affects strokes, shadows, blurs and corners [Taiga #1951](https://tree.taiga.io/project/penpot/us/1951)
- Use tabulators to navigate layers [Taiga #5010](https://tree.taiga.io/project/penpot/issue/5010)
### :bug: Bugs fixed
- Fix problem with rules position on changing pages [Taiga #4847](https://tree.taiga.io/project/penpot/issue/4847)
- Fix error streen when uploading wrong SVG [#2995](https://github.com/penpot/penpot/issues/2995)
- Fix selecting children from hidden parent layers [Taiga #4934](https://tree.taiga.io/project/penpot/issue/4934)
- Fix problem when undoing multiple selected colors [Taiga #4920](https://tree.taiga.io/project/penpot/issue/4920)
- Allow selection of empty board by partial rect [Taiga #4806](https://tree.taiga.io/project/penpot/issue/4806)
- Improve behavior for undo on text edition [Taiga #4693](https://tree.taiga.io/project/penpot/issue/4693)
- Improve deeps selection of nested arboards [Taiga #4913](https://tree.taiga.io/project/penpot/issue/4913)
- Fix problem on selection numeric inputs on Firefox [#2991](https://github.com/penpot/penpot/issues/2991)
- Changed the text dominant-baseline to use ideographic [Taiga #4791](https://tree.taiga.io/project/penpot/issue/4791)
- Viewer wrong translations [Github #3035](https://github.com/penpot/penpot/issues/3035)
- Fix problem with text editor in Safari
- Fix unlink library color when blur color picker input [#3026](https://github.com/penpot/penpot/issues/3026)
- Fix snap pixel when moving path points on high zoom [#2930](https://github.com/penpot/penpot/issues/2930)
- Fix shortcuts for zoom now take into account the mouse position [#2924](https://github.com/penpot/penpot/issues/2924)
- Fix close colorpicker on Firefox when mouse-up is outside the picker [#2911](https://github.com/penpot/penpot/issues/2911)
- Fix problems with touch devices and Wacom tablets [#2216](https://github.com/penpot/penpot/issues/2216)
- Fix problem with board titles misplaced [Taiga #4738](https://tree.taiga.io/project/penpot/issue/4738)
- Fix problem with alt getting stuck when alt+tab [Taiga #5013](https://tree.taiga.io/project/penpot/issue/5013)
- Fix problem with z positioning of elements [Taiga #5014](https://tree.taiga.io/project/penpot/issue/5014)
- Fix problem in Firefox with scroll jumping when changin pages [#3052](https://github.com/penpot/penpot/issues/3052)
- Fix nested frame interaction created flow in wrong frame [Taiga #5043](https://tree.taiga.io/project/penpot/issue/5043)
- Font-Kerning does not work on Artboard Export to PNG/JPG/PDF [#3029](https://github.com/penpot/penpot/issues/3029)
- Fix manipulate duplicated project (delete, duplicate, rename, pin/unpin...) [Taiga #5027](https://tree.taiga.io/project/penpot/issue/5027)
- Fix deleted files appear in search results [Taiga #5002](https://tree.taiga.io/project/penpot/issue/5002)
- Fix problem with selected colors and texts [Taiga #5051](https://tree.taiga.io/project/penpot/issue/5051)
- Fix problem when assigning color from palette or assets [Taiga #5050](https://tree.taiga.io/project/penpot/issue/5050)
- Fix shortcuts for alignment [Taiga #5030](https://tree.taiga.io/project/penpot/issue/5030)
- Fix path options not showing when editing rects or ellipses [Taiga #5053](https://tree.taiga.io/project/penpot/issue/5053)
- Fix tooltips for some alignment options are truncated on design tab [Taiga #5040](https://tree.taiga.io/project/penpot/issue/5040)
- Fix horizontal margins drag don't always start from place [Taiga #5020](https://tree.taiga.io/project/penpot/issue/5020)
- Fix multiplayer username sometimes is not displayed correctly [Taiga #4400](https://tree.taiga.io/project/penpot/issue/4400)
- Show warning when trying to invite a user that is already in members [Taiga #4147](https://tree.taiga.io/project/penpot/issue/4147)
- Fix problem with text out of borders when changing from auto-width to fixed [Taiga #4308](https://tree.taiga.io/project/penpot/issue/4308)
- Fix header not showing when exiting fullscreen mode in viewer [Taiga #4244](https://tree.taiga.io/project/penpot/issue/4244)
- Fix visual problem in select options [Taiga #5028](https://tree.taiga.io/project/penpot/issue/5028)
- Forbid empty names for assets [Taiga #5056](https://tree.taiga.io/project/penpot/issue/5056)
- Select children after ungroup action [Taiga #4917](https://tree.taiga.io/project/penpot/issue/4917)
- Fix problem with guides not showing when moving over nested frames [Taiga #4905](https://tree.taiga.io/project/penpot/issue/4905)
- Fix change email and password for users signed in via social login [Taiga #4273](https://tree.taiga.io/project/penpot/issue/4273)
- Fix drag and drop files from browser or file explorer under circumstances [Taiga #5054](https://tree.taiga.io/project/penpot/issue/5054)
- Fix problem when copy/pasting shapes [Taiga #4931](https://tree.taiga.io/project/penpot/issue/4931)
- Fix problem with color picker not able to change hue [Taiga #5065](https://tree.taiga.io/project/penpot/issue/5065)
- Fix problem with outer stroke in texts [Taiga #5078](https://tree.taiga.io/project/penpot/issue/5078)
- Fix problem with text carring over next line when changing to fixed [Taiga #5067](https://tree.taiga.io/project/penpot/issue/5067)
- Fix don't show invite user hero to users with editor role [Taiga #5086](https://tree.taiga.io/project/penpot/issue/5086)
- Fix enter emails on onboarding new user creating team [Taiga #5089](https://tree.taiga.io/project/penpot/issue/5089)
- Fix invalid files amount after moving on dashboard [Taiga #5080](https://tree.taiga.io/project/penpot/issue/5080)
- Fix dashboard left sidebar, the [x] overlaps the field [Taiga #5064](https://tree.taiga.io/project/penpot/issue/5064)
- Fix expanded typography on assets sidebar is moving [Taiga #5063](https://tree.taiga.io/project/penpot/issue/5063)
- Fix spelling mistake in confirmation after importing only 1 file [Taiga #5095](https://tree.taiga.io/project/penpot/issue/5095)
- Fix problem with selection colors and texts [Taiga #5079](https://tree.taiga.io/project/penpot/issue/5079)
- Remove "show in view mode" flag when moving frame to frame [Taiga #5091](https://tree.taiga.io/project/penpot/issue/5091)
- Fix problem creating files in project page [Taiga #5060](https://tree.taiga.io/project/penpot/issue/5060)
- Disable empty names on rename files [Taiga #5088](https://tree.taiga.io/project/penpot/issue/5088)
- Fix problem with SVG and flex layout [Taiga #](https://tree.taiga.io/project/penpot/issue/5099)
- Fix unpublish and delete shared library warning messages [Taiga #5090](https://tree.taiga.io/project/penpot/issue/5090)
- Fix last update project timer update after creating new file [Taiga #5096](https://tree.taiga.io/project/penpot/issue/5096)
- Fix dashboard scrolling using 'Page Up' and 'Page Down' [Taiga #5081](https://tree.taiga.io/project/penpot/issue/5081)
- Fix view mode header buttons overlapping in small resolutions [Taiga #5058](https://tree.taiga.io/project/penpot/issue/5058)
- Fix precision for wrap in flex [Taiga #5072](https://tree.taiga.io/project/penpot/issue/5072)
- Fix relative position overlay positioning [Taiga #5092](https://tree.taiga.io/project/penpot/issue/5092)
- Fix hide grid keyboard shortcut [Github #3071](https://github.com/penpot/penpot/pull/3071)
- Fix problem with opacity in imported SVG's [Taiga #4923](https://tree.taiga.io/project/penpot/issue/4923)
### :heart: Community contributions by (Thank you!)
- To @ondrejkonec: for contributing to the code with:
- Refactor CSS variables [Github #2948](https://github.com/penpot/penpot/pull/2948)
## 1.17.3
### :bug: Bugs fixed
- Fix copy and paste very nested inside itself [Taiga #4848](https://tree.taiga.io/project/penpot/issue/4848)
- Fix custom fonts not rendered correctly [Taiga #4874](https://tree.taiga.io/project/penpot/issue/4874)
- Fix problem with shadows and blur on multiple selection
- Fix problem with redo shortcut
- Fix Component texts not displayed in assets panel [Taiga #4907](https://tree.taiga.io/project/penpot/issue/4907)
- Fix search field has implemented shared styles for "close icon" and "search icon" [Taiga #4927](https://tree.taiga.io/project/penpot/issue/4927)
- Fix Handling correctly slashes "/" in emails [Taiga #4906](https://tree.taiga.io/project/penpot/issue/4906)
- Fix Change text color from selected colors [Taiga #4933](https://tree.taiga.io/project/penpot/issue/4933)
### :sparkles: Enhancements
- Adds environment variables for specifying the export and backend URI for the frontend docker image, thanks to @Supernova3339 for the initial PR and suggestion [Github #2984](https://github.com/penpot/penpot/issues/2984)
## 1.17.2
### :bug: Bugs fixed
@@ -8,10 +196,6 @@
- Fix correct behaviour for space-around and added space-evenly option
- Fix duplicate with alt and undo only undo one step [Taiga #4746](https://tree.taiga.io/project/penpot/issue/4746)
- Fix problem creating frames inside layout [Taiga #4844](https://tree.taiga.io/project/penpot/issue/4844)
## 1.17.2
### :bug: Bugs fixed
- Fix paste board inside itself [Taiga #4775](https://tree.taiga.io/project/penpot/issue/4775)
- Fix middle button panning can drag guides [Taiga #4266](https://tree.taiga.io/project/penpot/issue/4266)

View File

@@ -101,14 +101,14 @@ Each commit should have:
Examples of good commit messages:
- :bug: Fix unexpected error on launching modal
- :bug: Set proper error message on generic error
- :sparkles: Enable new modal for profile
- :zap: Improve performance of dashboard navigation
- :wrench: Update default backend configuration
- :books: Add more documentation for authentication process
- :ambulance: Fix critical bug on user registration process
- :tada: Add new approach for user registration
- `:bug: Fix unexpected error on launching modal`
- `:bug: Set proper error message on generic error`
- `:sparkles: Enable new modal for profile`
- `:zap: Improve performance of dashboard navigation`
- `:wrench: Update default backend configuration`
- `:books: Add more documentation for authentication process`
- `:ambulance: Fix critical bug on user registration process`
- `:tada: Add new approach for user registration`
## Code of conduct ##

View File

@@ -4,7 +4,7 @@
<h1 align="center">
<br>
<img src="https://penpot.app/images/readme/readme-logo.jpg" alt="PENPOT">
<img src="https://penpot.app/images/readme/git-readme-header.png" alt="PENPOT">
</h1>
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
@@ -26,6 +26,8 @@
![feature-readme](https://user-images.githubusercontent.com/1045247/189871786-0b44f7cf-3a0a-4445-a87b-9919ec398bf7.gif)
**:tada: [Important Notice!] :tada:** Our very first **Penpot Fest** is happening on June 28-30, Barcelona (Spain). **Secure yourself a ticket** to know everything about the present and future of Penpot and be part of the conversation! See details on the amazing venue and speakers lineup at [penpotfest.org](https://penpotfest.org)! :zap:
Penpot is the first **Open Source** design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return.
## Table of contents ##
@@ -50,7 +52,7 @@ Being web based, Penpot is not dependent on operating systems or local installat
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/readme/open-source.png" alt="Open Source">
<img src="https://penpot.app/images/readme/git-open.png" alt="Open Source" style="width: 65%;">
</p>
@@ -74,7 +76,7 @@ Heres a step-by-step guide on [getting started with Docker.](https://help.pen
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">
<img src="https://penpot.app/images/readme/git-self-host.png" alt="Getting started" style="width: 65%;">
</p>
## Community ##
@@ -93,7 +95,7 @@ You will find the following categories:
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
<p align="center">
<img src="https://penpot.app/images/readme/cross-teams.webp" alt="Community">
<img src="https://penpot.app/images/readme/git-collaborate.png" alt="Communnity" style="width: 65%;">
</p>
## Contributing ##
@@ -111,7 +113,7 @@ Every sort of contribution will be very helpful to enhance Penpot. How youll
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">
<img src="https://penpot.app/images/readme/git-community.png" alt="Contributing" style="width: 65%;">
</p>
## Resources ##
@@ -124,7 +126,7 @@ You can ask and answer questions, have open-ended conversations, and follow alon
✏️ [Tutorials](https://www.youtube.com/playlist?list=PLgcCPfOv5v54WpXhHmNO7T-YC7AE-SRsr)
🏘️ [Architecture](https://help.penpot.app/technical-guide/architecture/)
🏘️ [Architecture](https://help.penpot.app/technical-guide/developer/architecture/)
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)

View File

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

View File

@@ -16,16 +16,11 @@
{:src-dirs ["src" "resources"]
:target-dir class-dir})
(b/compile-clj
{:basis basis
:src-dirs ["src"]
:class-dir class-dir})
(b/uber
{:class-dir class-dir
:uber-file jar-file
:main 'clojure.main
:exclude [#"goog.*" #"^javasist.*"]
:exclude [#".*Log4j2Plugins\.dat$"]
:basis basis}))
(defn compile [_]

View File

@@ -1,13 +1,12 @@
{:deps
{:mvn/repos
{"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/core.async {:mvn/version "1.6.673"}
;; Logging
org.zeromq/jeromq {:mvn/version "0.5.3"}
com.github.luben/zstd-jni {:mvn/version "1.5.2-5"}
org.clojure/data.fressian {:mvn/version "1.0.0"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -22,14 +21,16 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti
{:git/tag "v9.12"
:git/sha "51646d8"
{:git/tag "v9.15"
:git/sha "aa9b967"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
metosin/reitit-core {:mvn/version "0.5.18"}
org.postgresql/postgresql {:mvn/version "42.5.1"}
org.postgresql/postgresql {:mvn/version "42.6.0"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
io.whitfin/siphash {:mvn/version "2.0.0"}
@@ -37,7 +38,7 @@
buddy/buddy-hashers {:mvn/version "1.8.158"}
buddy/buddy-sign {:mvn/version "3.4.333"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.2"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.5"}
org.jsoup/jsoup {:mvn/version "1.15.3"}
org.im4java/im4java
@@ -55,7 +56,7 @@
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.19.8"}
software.amazon.awssdk/s3 {:mvn/version "2.19.29"}
}
:paths ["src" "resources" "target/classes"]
@@ -70,10 +71,9 @@
mockery/mockery {:mvn/version "RELEASE"}}
:extra-paths ["test" "dev"]}
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.0" :git/sha "8c93e0c"}}
{io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"}}
:ns-default build}
:test

View File

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

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +0,0 @@
<mjml>
<mj-head>
<mj-font name="Source Sans Pro" href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" />
<mj-attributes>
<mj-text font-family="Source Sans Pro, sans-serif" font-size="16px" color="#000000" line-height="150%" />
<mj-button background-color="#31EFB8" color="#1F1F1F" font-family="Source Sans Pro, sans-serif" font-size="16px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E5E5E5">
<mj-section padding="0">
<mj-column>
<mj-image src="{{ public-uri }}/images/email/uxbox-title.png"
width="97px" height="32px" align="left" padding="16px" />
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF">
<mj-column>
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
<mj-text>We received a request to change your current email to {{ pending-email }}.</mj-text>
<mj-text>Click to the link below to confirm the change:</mj-text>
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
Confirm email change
</mj-button>
<mj-text>
If you received this email by mistake, please consider changing your password
for security reasons.
</mj-text>
<mj-text>Enjoy!</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-social icon-size="24px" mode="horizontal">
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>
</mg-body>
</mjml>

View File

@@ -1,59 +0,0 @@
<mjml>
<mj-head>
<mj-font name="Source Sans Pro" href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" />
<mj-attributes>
<mj-text font-family="Source Sans Pro, sans-serif" font-size="16px" color="#000000" line-height="150%" />
<mj-button background-color="#31EFB8" color="#1F1F1F" font-family="Source Sans Pro, sans-serif" font-size="16px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E5E5E5">
<mj-section padding="0">
<mj-column>
<mj-image src="{{ public-uri }}/images/email/uxbox-title.png"
width="97px" height="32px" align="left" padding="16px" />
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF">
<mj-column>
<mj-text font-size="24px" font-weight="600">Hello!</mj-text>
<mj-text>
{{invited-by}} has invited you to join the team “{{ team }}”.
</mj-text>
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
Accept invite
</mj-button>
<mj-text>Enjoy!</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-social icon-size="24px" mode="horizontal">
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>
</mg-body>
</mjml>

View File

@@ -1,68 +0,0 @@
<mjml>
<mj-head>
<mj-font name="Source Sans Pro" href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" />
<mj-attributes>
<mj-text font-family="Source Sans Pro, sans-serif" font-size="16px" color="#000000" line-height="150%" />
<mj-button background-color="#31EFB8" color="#1F1F1F" font-family="Source Sans Pro, sans-serif" font-size="16px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E5E5E5">
<mj-section padding="0">
<mj-column>
<mj-image src="{{ public-uri }}/images/email/uxbox-title.png"
width="97px" height="32px" align="left" padding="16px" />
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF">
<mj-column>
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
<mj-text>
We have received a request to reset your password. Click the link
below to choose a new one:
</mj-text>
<mj-button href="{{ public-uri }}/#/auth/recovery?token={{token}}">
Reset password
</mj-button>
<mj-text>
If you received this email by mistake, you can safely ignore
it. Your password won't be changed.
</mj-text>
<mj-text>Enjoy!</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-social icon-size="24px" mode="horizontal">
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>
</mg-body>
</mjml>

View File

@@ -1,65 +0,0 @@
<mjml>
<mj-head>
<mj-font name="Source Sans Pro" href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" />
<mj-attributes>
<mj-text font-family="Source Sans Pro, sans-serif" font-size="16px" color="#000000" line-height="150%" />
<mj-button background-color="#31EFB8" color="#1F1F1F" font-family="Source Sans Pro, sans-serif" font-size="16px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#E5E5E5">
<mj-section padding="0">
<mj-column>
<mj-image src="{{ public-uri }}/images/email/uxbox-title.png"
width="97px" height="32px" align="left" padding="16px" />
</mj-column>
</mj-section>
<mj-section background-color="#FFFFFF">
<mj-column>
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
<mj-text>
Thanks for signing up for your Penpot account! Please verify your
email using the link below and get started building mockups and
prototypes today!
</mj-text>
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
Verify email
</mj-button>
<mj-text>Enjoy!</mj-text>
<mj-text>The Penpot team.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px 0 0 0">
<mj-column width="425px">
<mj-text align="center" font-size="14px" color="#64666A">
Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.
</mj-text>
</mj-column>
</mj-section>
<mj-section padding="0">
<mj-column>
<mj-social icon-size="24px" mode="horizontal">
<mj-social-element src="{{ public-uri }}/images/email/logo-uxbox.png" href="https://penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-twitter.png" href="https://twitter.com/penpotapp" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-github.png" href="https://github.com/penpot/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-instagram.png" href="https://www.instagram.com/penpot.app/" padding="0 8px" />
<mj-social-element src="{{ public-uri }}/images/email/logo-taiga.png" href="https://tree.taiga.io/project/penpot" padding="0 8px" />
</mj-social>
</mj-column>
</mj-section>
<mj-section padding="0 0 24px 0">
<mj-column>
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
Penpot © 2020 | Made with &lt;3 and Open Source
</mj-text>
</mj-column>
</mj-section>
</mg-body>
</mjml>

View File

@@ -1,4 +1,8 @@
[{:id "tutorial-for-beginners"
[{:id "material-design-3"
:name "Material Design 3"
:thumbnail-uri "https://penpot.app/images/libraries/cover-md3.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/main/Material%20Design%203.penpot"}
{:id "tutorial-for-beginners"
:name "Tutorial for beginners"
:thumbnail-uri "https://penpot.app/images/libraries/tutorial-for-beginners.jpg"
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
@@ -29,8 +33,4 @@
{: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"}]
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}]

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,112 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error report v2 {{id}}
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error">⮜</a>]</div>
<div>[<a href="#message">message</a>]</div>
<div>[<a href="#props">props</a>]</div>
<div>[<a href="#context">context</a>]</div>
{% if params %}
<div>[<a href="#params">request params</a>]</div>
{% endif %}
{% if data %}
<div>[<a href="#edata">error data</a>]</div>
{% endif %}
{% if spec-explain %}
<div>[<a href="#spec-explain">spec explain</a>]</div>
{% endif %}
{% if spec-problems %}
<div>[<a href="#spec-problems">spec problems</a>]</div>
{% endif %}
{% if spec-value %}
<div>[<a href="#spec-value">spec value</a>]</div>
{% endif %}
{% if trace %}
<div>[<a href="#trace">error trace</a>]</div>
{% endif %}
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="message" class="table-key">MESSAGE: </div>
<div class="table-val">
<h1>{{hint}}</h1>
</div>
</div>
<div class="table-row multiline">
<div id="props" class="table-key">LOG PROPS: </div>
<div class="table-val">
<pre>{{props}}</pre>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
{% if params %}
<div class="table-row multiline">
<div id="params" class="table-key">REQUEST PARAMS: </div>
<div class="table-val">
<pre>{{params}}</pre>
</div>
</div>
{% endif %}
{% if data %}
<div class="table-row multiline">
<div id="edata" class="table-key">ERROR DATA: </div>
<div class="table-val">
<pre>{{data}}</pre>
</div>
</div>
{% endif %}
{% if spec-explain %}
<div class="table-row multiline">
<div id="spec-explain" class="table-key">SPEC EXPLAIN: </div>
<div class="table-val">
<pre>{{spec-explain}}</pre>
</div>
</div>
{% endif %}
{% if spec-problems %}
<div class="table-row multiline">
<div id="spec-problems" class="table-key">SPEC PROBLEMS: </div>
<div class="table-val">
<pre>{{spec-problems}}</pre>
</div>
</div>
{% endif %}
{% if spec-value %}
<div class="table-row multiline">
<div id="spec-value" class="table-key">SPEC VALUE: </div>
<div class="table-val">
<pre>{{spec-value}}</pre>
</div>
</div>
{% endif %}
{% if trace %}
<div class="table-row multiline">
<div id="trace" class="table-key">TRACE:</div>
<div class="table-val">
<pre>{{trace}}</pre>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,100 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
penpot - error report v2 {{id}}
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error">⮜</a>]</div>
<div>[<a href="#message">message</a>]</div>
<div>[<a href="#props">props</a>]</div>
<div>[<a href="#context">context</a>]</div>
{% if params %}
<div>[<a href="#params">params</a>]</div>
{% endif %}
{% if data %}
<div>[<a href="#edata">data</a>]</div>
{% endif %}
{% if explain %}
<div>[<a href="#explain">explain</a>]</div>
{% endif %}
{% if value %}
<div>[<a href="#value">value</a>]</div>
{% endif %}
{% if trace %}
<div>[<a href="#trace">trace</a>]</div>
{% endif %}
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="message" class="table-key">MESSAGE: </div>
<div class="table-val">
<h1>{{hint}}</h1>
</div>
</div>
<div class="table-row multiline">
<div id="props" class="table-key">LOG PROPS: </div>
<div class="table-val">
<pre>{{props}}</pre>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
{% if params %}
<div class="table-row multiline">
<div id="params" class="table-key">PARAMS: </div>
<div class="table-val">
<pre>{{params}}</pre>
</div>
</div>
{% endif %}
{% if data %}
<div class="table-row multiline">
<div id="edata" class="table-key">DATA: </div>
<div class="table-val">
<pre>{{data}}</pre>
</div>
</div>
{% endif %}
{% if value %}
<div class="table-row multiline">
<div id="value" class="table-key">VALIDATION VALUE: </div>
<div class="table-val">
<pre>{{value}}</pre>
</div>
</div>
{% endif %}
{% if explain %}
<div class="table-row multiline">
<div id="explain" class="table-key">EXPLAIN: </div>
<div class="table-val">
<pre>{{explain}}</pre>
</div>
</div>
{% endif %}
{% if trace %}
<div class="table-row multiline">
<div id="trace" class="table-key">TRACE:</div>
<div class="table-val">
<pre>{{trace}}</pre>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock %}

View File

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

View File

@@ -23,6 +23,10 @@ input[type=text], input[type=submit] {
padding: 3px;
}
pre {
white-space: pre-wrap;
}
main {
margin: 20px;
}

View File

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

View File

@@ -3,22 +3,17 @@
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
alwaysWriteExceptions="false" />
alwaysWriteExceptions="true" />
</Console>
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
alwaysWriteExceptions="false" />
alwaysWriteExceptions="true" />
<Policies>
<SizeBasedTriggeringPolicy size="50M"/>
</Policies>
<DefaultRolloverStrategy max="9"/>
</RollingFile>
<JeroMQ name="zmq">
<Property name="endpoint">tcp://localhost:45556</Property>
<JsonLayout complete="false" compact="true" includeTimeMillis="true" stacktraceAsString="true" properties="true" />
</JeroMQ>
</Appenders>
<Loggers>
@@ -37,17 +32,12 @@
<Logger name="app.rpc.climit" level="info" />
<Logger name="app.rpc.mutations.files" level="info" />
<Logger name="app.cli" level="debug" additivity="false">
<AppenderRef ref="console"/>
</Logger>
<Logger name="app.loggers" level="debug" additivity="false">
<AppenderRef ref="main" level="debug" />
</Logger>
<Logger name="app" level="all" additivity="false">
<AppenderRef ref="main" level="trace" />
<AppenderRef ref="zmq" level="debug" />
</Logger>
<Logger name="user" level="trace" additivity="false">

View File

@@ -12,6 +12,7 @@
<Logger name="com.zaxxer.hikari" level="error" />
<Logger name="org.postgresql" level="error" />
<Logger name="app.util" level="info" />
<Logger name="app" level="info" additivity="false">
<AppenderRef ref="console" />
</Logger>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,15 +6,18 @@
(ns app.auth
(:require
[buddy.hashers :as hashers]))
[buddy.hashers :as hashers]
[promesa.exec :as px]))
(def default-params
{:alg :argon2id
:memory (* 32768 2)
:iterations 5
:parallelism (px/get-available-processors)})
(defn derive-password
[password]
(hashers/derive password
{:alg :argon2id
:memory 16384
:iterations 20
:parallelism 2}))
(hashers/derive password default-params))
(defn verify-password
[attempt password]

View File

@@ -17,22 +17,18 @@
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.http.middleware :as hmw]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc.queries.profile :as profile]
[app.rpc.commands.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]))
[yetti.response :as-alias yrs]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
@@ -165,21 +161,23 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- retrieve-github-email
[cfg tdata info]
(or (some-> info :email p/resolved)
(->> (http/req! cfg
{:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get})
(p/map (fn [{:keys [status body] :as response}]
(when-not (s/int-in-range? 200 300 status)
(ex/raise :type :internal
:code :unable-to-retrieve-github-emails
:hint "unable to retrieve github emails"
:http-status status
:http-body body))
(->> response :body json/decode (filter :primary) first :email))))))
[cfg tdata props]
(or (some-> props :github/email)
(let [params {:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}
{:keys [status body]} (http/req! cfg params {:sync? true})]
(when-not (s/int-in-range? 200 300 status)
(ex/raise :type :internal
:code :unable-to-retrieve-github-emails
:hint "unable to retrieve github emails"
:http-status status
:http-body body))
(->> body json/decode (filter :primary) first :email))))
(defmethod ig/pre-init-spec ::providers/github [_]
(s/keys :req [::http/client]))
@@ -196,7 +194,7 @@
;; Additional hooks for provider specific way of
;; retrieve emails.
:get-email-fn (partial retrieve-github-email cfg)}]
:get-email-fn (partial retrieve-github-email cfg)}]
(when (contains? cf/flags :login-with-github)
(if (and (string? (:client-id opts))
@@ -246,6 +244,11 @@
;; HANDLERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- parse-attr-path
[provider path]
(let [[fitem & items] (str/split path "__")]
(into [(keyword (:name provider) fitem)] (map keyword) items)))
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
(let [public (u/uri (cf/get :public-uri))]
@@ -290,80 +293,77 @@
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(->> (http/req! cfg req)
(p/map (fn [{:keys [status body] :as res}]
(l/trace :hint "access token response"
:status status
:body body)
(if (= status 200)
(let [data (json/decode body)]
{:token (get data :access_token)
:type (get data :token_type)})
(ex/raise :type :internal
:code :unable-to-retrieve-token
:http-status status
:http-body body)))))))
(let [{:keys [status body]} (http/req! cfg req {:sync? true})]
(l/trace :hint "access token response" :status status :body body)
(if (= status 200)
(let [data (json/decode body)]
{:token (get data :access_token)
:type (get data :token_type)})
(ex/raise :type :internal
:code :unable-to-retrieve-token
:hint "unable to retrieve token"
:http-status status
:http-body body)))))
(defn- retrieve-user-info
[{:keys [provider] :as cfg} tdata]
(letfn [(retrieve []
(l/trace :hint "request user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token tdata))
:token-type (:type tdata))
(http/req! cfg
{:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}))
(validate-response [response]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
response)
(get-email [info]
(letfn [(get-email [props]
;; Allow providers hook into this for custom email
;; retrieval method.
(if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata info)
(let [attr-kw (cf/get :oidc-email-attr :email)]
(p/resolved (get info attr-kw)))))
(get-name [info]
(let [attr-kw (cf/get :oidc-name-attr :name)]
(get info attr-kw)))
(if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata props)
(let [attr-kw (cf/get :oidc-email-attr "email")
attr-ph (parse-attr-path provider attr-kw)]
(get-in props attr-ph))))
(get-name [props]
(let [attr-kw (cf/get :oidc-name-attr "name")
attr-ph (parse-attr-path provider attr-kw)]
(get-in props attr-ph)))
(process-response [response]
(p/let [info (-> response :body json/decode)
email (get-email info)]
(let [info (-> response :body json/decode)
props (qualify-props provider info)
email (get-email props)]
{:backend (:name provider)
:fullname (or (get-name props) email)
:email email
:fullname (or (get-name info) email)
:props (->> (dissoc info :name :email)
(qualify-props provider))}))
:props props}))]
(validate-info [info]
(l/trace :hint "authentication info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
:info (pr-str info))
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
info)]
(l/trace :hint "request user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token tdata))
:token-type (:type tdata))
(->> (retrieve)
(p/fmap validate-response)
(p/mcat process-response)
(p/fmap validate-info))))
(let [request {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}
response (http/req! cfg request {:sync? true})]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
(let [info (process-response response)]
(l/trace :hint "authentication info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
info))))
(s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string)
@@ -375,65 +375,59 @@
::fullname
::props]))
(defn retrieve-info
(defn get-info
[{:keys [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 #{}))]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
;; check if profile has a configured set of roles
(when-not (set/subset? provider-roles profile-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enough permissions"))))
info)
(let [state (get params :state)
code (get params :code)
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})
token (retrieve-access-token cfg code)
info (retrieve-user-info cfg token)]
(post-process [state info]
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state))
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
(when (and (= "oidc" (:name provider))
(seq (:roles provider)))
;; 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))))]
(let [expected-roles (into #{} (:roles provider))
current-roles (let [roles-kw (cf/get :oidc-roles-attr "roles")
roles-ph (parse-attr-path provider roles-kw)
roles (get-in (:props info) roles-ph)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
:error-id error
:error-desc (get params :error_description)))
;; check if profile has a configured set of roles
(when-not (set/subset? expected-roles current-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enough permissions"))))
(let [state (get params :state)
code (get params :code)
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})]
(-> (p/resolved code)
(p/then #(retrieve-access-token cfg %))
(p/then #(retrieve-user-info cfg %))
(p/then' validate-oidc)
(p/then' (partial post-process state))))))
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state))
(defn- retrieve-profile
[{:keys [::db/pool ::wrk/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)))))
;; If state token comes with props, merge them. The state token
;; props can contain pm_ and utm_ prefixed query params.
(map? (:props state))
(update :props merge (:props state)))))
(defn- get-profile
[{:keys [::db/pool] :as cfg} info]
(dm/with-open [conn (db/open pool)]
(some->> (:email info)
(profile/get-profile-by-email conn))))
(defn- redirect-response
[uri]
(yrs/response :status 302 :headers {"location" (str uri)}))
{::yrs/status 302
::yrs/headers {"location" (str uri)}})
(defn- generate-error-redirect
[_ error]
@@ -443,9 +437,9 @@
(redirect-response uri)))
(defn- generate-redirect
[{:keys [::session/session] :as cfg} request info profile]
[cfg request info profile]
(if profile
(let [sxf (session/create-fn session (:id profile))
(let [sxf (session/create-fn cfg (:id profile))
token (or (:invitation-token info)
(tokens/generate (::main/props cfg)
{:iss :auth
@@ -460,11 +454,11 @@
(ex/raise :type :restriction
:code :profile-blocked))
(audit/submit! cfg {:type "command"
:name "login-with-password"
:profile-id (:id profile)
:ip-addr (audit/parse-client-ip request)
:props (audit/profile->props profile)})
(audit/submit! cfg {::audit/type "command"
::audit/name "login-with-oidc"
::audit/profile-id (:id profile)
::audit/ip-addr (audit/parse-client-ip request)
::audit/props (audit/profile->props profile)})
(->> (redirect-response uri)
(sxf request)))
@@ -480,6 +474,7 @@
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))))
(defn- auth-handler
@@ -491,27 +486,24 @@
:props props
:exp (dt/in-future "4h")})
uri (build-auth-uri cfg state)]
(yrs/response 200 {:redirect-uri uri})))
{::yrs/status 200
::yrs/body {:redirect-uri uri}}))
(defn- callback-handler
[cfg request]
(letfn [(process-request []
(p/let [info (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))))
(try
(let [info (get-info cfg request)
profile (get-profile cfg info)]
(generate-redirect cfg request info profile))
(catch Throwable cause
(l/error :hint "error on oauth process" :cause cause)
(generate-error-redirect cfg cause))))
(def provider-lookup
{:compile
(fn [& _]
(fn [handler]
(fn [{:keys [::providers] :as cfg} request]
(fn [handler {:keys [::providers] :as cfg}]
(fn [request]
(let [provider (some-> request :path-params :provider keyword)]
(if-let [provider (get providers provider)]
(handler (assoc cfg :provider provider) request)
@@ -549,23 +541,21 @@
(s/def ::providers (s/map-of ::us/keyword (s/nilable ::provider)))
(s/def ::routes vector?)
(defmethod ig/pre-init-spec ::routes
[_]
(s/keys :req [::http/client
::wrk/executor
(s/keys :req [::session/manager
::http/client
::main/props
::db/pool
::providers
::session/session]))
::providers]))
(defmethod ig/init-key ::routes
[_ {:keys [::wrk/executor ::session/session] :as cfg}]
[_ cfg]
(let [cfg (update cfg :provider d/without-nils)]
["" {:middleware [[(:middleware session)]
[hmw/with-dispatch executor]
[hmw/with-config cfg]
[provider-lookup]
]}
["" {:middleware [[session/authz cfg]
[provider-lookup cfg]]}
["/auth/oauth"
["/:provider"
{:handler auth-handler
@@ -573,4 +563,3 @@
["/:provider/callback"
{:handler callback-handler
:allowed-methods #{:get}}]]]))

View File

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

View File

@@ -51,7 +51,6 @@
:database-password "penpot"
:default-blob-version 5
:loggers-zmq-uri "tcp://localhost:45556"
:rpc-rlimit-config (fs/path "resources/rlimit.edn")
:rpc-climit-config (fs/path "resources/climit.edn")
@@ -126,6 +125,7 @@
(s/def ::database-max-pool-size ::us/integer)
(s/def ::quotes-teams-per-profile ::us/integer)
(s/def ::quotes-access-tokens-per-profile ::us/integer)
(s/def ::quotes-projects-per-team ::us/integer)
(s/def ::quotes-invitations-per-team ::us/integer)
(s/def ::quotes-profiles-per-team ::us/integer)
@@ -153,9 +153,9 @@
(s/def ::oidc-user-uri ::us/string)
(s/def ::oidc-scopes ::us/set-of-strings)
(s/def ::oidc-roles ::us/set-of-strings)
(s/def ::oidc-roles-attr ::us/keyword)
(s/def ::oidc-email-attr ::us/keyword)
(s/def ::oidc-name-attr ::us/keyword)
(s/def ::oidc-roles-attr ::us/string)
(s/def ::oidc-email-attr ::us/string)
(s/def ::oidc-name-attr ::us/string)
(s/def ::host ::us/string)
(s/def ::http-server-port ::us/integer)
(s/def ::http-server-host ::us/string)
@@ -174,8 +174,6 @@
(s/def ::ldap-ssl ::us/boolean)
(s/def ::ldap-starttls ::us/boolean)
(s/def ::ldap-user-query ::us/string)
(s/def ::loggers-loki-uri ::us/string)
(s/def ::loggers-zmq-uri ::us/string)
(s/def ::media-directory ::us/string)
(s/def ::media-uri ::us/string)
(s/def ::profile-bounce-max-age ::dt/duration)
@@ -271,8 +269,6 @@
::ldap-starttls
::ldap-user-query
::local-assets-uri
::loggers-loki-uri
::loggers-zmq-uri
::media-max-file-size
::profile-bounce-max-age
::profile-bounce-threshold
@@ -281,6 +277,7 @@
::public-uri
::quotes-teams-per-profile
::quotes-access-tokens-per-profile
::quotes-projects-per-team
::quotes-invitations-per-team
::quotes-profiles-per-team
@@ -326,6 +323,7 @@
(def default-flags
[:enable-backend-api-doc
:enable-backend-openapi-doc
:enable-backend-worker
:enable-secure-session-cookies
:enable-email-verification])
@@ -355,7 +353,7 @@
(merge defaults)
(us/conform ::config))
(catch Throwable e
(when (ex/ex-info? e)
(when (ex/error? e)
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
(println "Error on validating configuration:")
(println (some-> e ex-data ex/explain))

View File

@@ -17,7 +17,6 @@
[app.db.sql :as sql]
[app.metrics :as mtx]
[app.util.json :as json]
[app.util.migrations :as mg]
[app.util.time :as dt]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
@@ -32,7 +31,6 @@
io.whitfin.siphash.SipHasherContainer
java.io.InputStream
java.io.OutputStream
java.lang.AutoCloseable
java.sql.Connection
java.sql.Savepoint
org.postgresql.PGConnection
@@ -50,12 +48,9 @@
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare apply-migrations!)
(s/def ::connection-timeout ::us/integer)
(s/def ::max-size ::us/integer)
(s/def ::min-size ::us/integer)
(s/def ::migrations map?)
(s/def ::name keyword?)
(s/def ::password ::us/string)
(s/def ::uri ::us/not-empty-string)
@@ -64,26 +59,26 @@
(s/def ::read-only? ::us/boolean)
(s/def ::pool-options
(s/keys :opt-un [::uri ::name
::min-size
::max-size
::connection-timeout
::validation-timeout
::migrations
::username
::password
::mtx/metrics
::read-only?]))
(s/keys :opt [::uri
::name
::min-size
::max-size
::connection-timeout
::validation-timeout
::username
::password
::mtx/metrics
::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})
{::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]
@@ -93,39 +88,23 @@
(defmethod ig/pre-init-spec ::pool [_] ::pool-options)
(defmethod ig/init-key ::pool
[_ {: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)
(do
(l/warn :hint "unable to initialize pool, missing url"
:name (d/name (:name cfg))
:read-only read-only?)
nil)))
[_ {:keys [::uri ::read-only?] :as cfg}]
(when uri
(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))
(create-pool cfg)))
(defmethod ig/halt-key! ::pool
[_ pool]
(when pool
(.close ^HikariDataSource pool)))
(defn- apply-migrations!
[pool migrations]
(with-open [conn ^AutoCloseable (open pool)]
(mg/setup! conn)
(doseq [[name steps] migrations]
(mg/migrate! conn {:name (d/name name) :steps steps}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API & Impl
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -135,19 +114,19 @@
"SET idle_in_transaction_session_timeout = 300000;"))
(defn- create-datasource-config
[{:keys [metrics uri] :as cfg}]
[{:keys [::mtx/metrics ::uri] :as cfg}]
(let [config (HikariConfig.)]
(doto config
(.setJdbcUrl (str "jdbc:" uri))
(.setPoolName (d/name (:name cfg)))
(.setPoolName (d/name (::name cfg)))
(.setAutoCommit true)
(.setReadOnly (:read-only? cfg))
(.setConnectionTimeout (:connection-timeout cfg))
(.setValidationTimeout (:validation-timeout cfg))
(.setIdleTimeout (:idle-timeout cfg))
(.setMaxLifetime (:max-lifetime cfg))
(.setMinimumIdle (:min-size cfg))
(.setMaximumPoolSize (:max-size cfg))
(.setReadOnly (::read-only? cfg))
(.setConnectionTimeout (::connection-timeout cfg))
(.setValidationTimeout (::validation-timeout cfg))
(.setIdleTimeout (::idle-timeout cfg))
(.setMaxLifetime (::max-lifetime cfg))
(.setMinimumIdle (::min-size cfg))
(.setMaximumPoolSize (::max-size cfg))
(.setConnectionInitSql initsql)
(.setInitializationFailTimeout -1))
@@ -157,8 +136,8 @@
(PrometheusMetricsTrackerFactory.)
(.setMetricsTrackerFactory config)))
(some->> ^String (:username cfg) (.setUsername config))
(some->> ^String (:password cfg) (.setPassword config))
(some->> ^String (::username cfg) (.setUsername config))
(some->> ^String (::password cfg) (.setPassword config))
config))
@@ -166,11 +145,9 @@
[v]
(instance? javax.sql.DataSource v))
(s/def ::pool pool?)
(s/def ::conn some?)
;; DEPRECATED: to be removed in 1.18
(s/def ::conn-or-pool some?)
(s/def ::nilable-pool (s/nilable ::pool))
(s/def ::pool pool?)
(s/def ::pool-or-conn some?)
(defn closed?
@@ -178,8 +155,18 @@
(.isClosed ^HikariDataSource pool))
(defn read-only?
[pool]
(.isReadOnly ^HikariDataSource pool))
[pool-or-conn]
(cond
(instance? HikariDataSource pool-or-conn)
(.isReadOnly ^HikariDataSource pool-or-conn)
(instance? Connection pool-or-conn)
(.isReadOnly ^Connection pool-or-conn)
:else
(ex/raise :type :internal
:code :invalid-connection
:hint "invalid connection provided")))
(defn create-pool
[cfg]
@@ -237,44 +224,46 @@
[pool]
(jdbc/get-connection pool))
(def ^:private default-opts
{:builder-fn sql/as-kebab-maps})
(defn exec!
([ds sv]
(exec! ds sv {}))
(jdbc/execute! ds sv default-opts))
([ds sv opts]
(jdbc/execute! ds sv (assoc opts :builder-fn sql/as-kebab-maps))))
(jdbc/execute! ds sv (merge default-opts opts))))
(defn exec-one!
([ds sv] (exec-one! ds sv {}))
([ds sv]
(jdbc/execute-one! ds sv default-opts))
([ds sv opts]
(jdbc/execute-one! ds sv (assoc opts :builder-fn sql/as-kebab-maps))))
(jdbc/execute-one! ds sv
(-> (merge default-opts opts)
(assoc :return-keys (::return-keys? opts false))))))
(defn insert!
([ds table params] (insert! ds table params nil))
([ds table params opts]
(exec-one! ds
(sql/insert table params opts)
(merge {:return-keys true} opts))))
[ds table params & {:as opts}]
(exec-one! ds
(sql/insert table params opts)
(merge {::return-keys? true} opts)))
(defn insert-multi!
([ds table cols rows] (insert-multi! ds table cols rows nil))
([ds table cols rows opts]
(exec! ds
(sql/insert-multi table cols rows opts)
(merge {:return-keys true} opts))))
[ds table cols rows & {:as opts}]
(exec! ds
(sql/insert-multi table cols rows opts)
(merge {::return-keys? true} opts)))
(defn update!
([ds table params where] (update! ds table params where nil))
([ds table params where opts]
(exec-one! ds
(sql/update table params where opts)
(merge {:return-keys true} opts))))
[ds table params where & {:as opts}]
(exec-one! ds
(sql/update table params where opts)
(merge {::return-keys? true} opts)))
(defn delete!
([ds table params] (delete! ds table params nil))
([ds table params opts]
(exec-one! ds
(sql/delete table params opts)
(assoc opts :return-keys true))))
[ds table params & {:as opts}]
(exec-one! ds
(sql/delete table params opts)
(merge {::return-keys? true} opts)))
(defn is-row-deleted?
[{:keys [deleted-at]}]
@@ -283,56 +272,34 @@
(inst-ms (dt/now)))))
(defn get*
"Internal function for retrieve a single row from database that
matches a simple filters."
([ds table params]
(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))))
"Retrieve a single row from database that matches a simple filters. Do
not raises exceptions."
[ds table params & {:as opts}]
(let [rows (exec! ds (sql/select table params opts))
rows (cond->> rows
(::remove-deleted? opts true)
(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
:code :object-not-found
:table table
:hint "database object not found"))
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
:code :object-not-found
:table table
:hint "database object not found"))
row)))
"Retrieve a single row from database that matches a simple
filters. Raises :not-found exception if no object is found."
[ds table params & {:as opts}]
(let [row (get* ds table params opts)]
(when (and (not row) (::check-deleted? opts true))
(ex/raise :type :not-found
:code :object-not-found
:table table
:hint "database object not found"))
row))
(defn get-by-id
([ds table id]
(get ds table {:id id} nil))
([ds table id opts]
(let [opts (cond-> opts
(contains? opts :check-not-found)
(assoc :check-deleted? (:check-not-found opts)))]
(get ds table {:id id} opts))))
[ds table id & {:as opts}]
(get ds table {:id id} opts))
(defn query
([ds table params]
(query ds table params nil))
([ds table params opts]
(exec! ds (sql/select table params opts))))
[ds table params & {:as opts}]
(exec! ds (sql/select table params opts)))
(defn pgobject?
([v]
@@ -394,12 +361,20 @@
[data]
(org.postgresql.util.PGInterval. ^String data))
(defn connection?
[conn]
(instance? Connection conn))
(defn savepoint
([^Connection conn]
(.setSavepoint conn))
([^Connection conn label]
(.setSavepoint conn (name label))))
(defn release!
[^Connection conn ^Savepoint sp ]
(.releaseSavepoint conn sp))
(defn rollback!
([^Connection conn]
(.rollback conn))
@@ -475,6 +450,11 @@
(.setType "jsonb")
(.setValue (json/encode-str data)))))
(defn get-update-count
[result]
(:next.jdbc/update-count result))
;; --- Locks
(def ^:private siphash-state

View File

@@ -7,6 +7,7 @@
(ns app.db.sql
(:refer-clojure :exclude [update])
(:require
[app.db :as-alias db]
[clojure.string :as str]
[next.jdbc.optional :as jdbc-opt]
[next.jdbc.sql.builder :as sql]))
@@ -43,8 +44,10 @@
([table where-params opts]
(let [opts (merge default-opts opts)
opts (cond-> opts
(:for-update opts) (assoc :suffix "FOR UPDATE")
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
(::db/for-update? opts) (assoc :suffix "FOR UPDATE")
(::db/for-share? opts) (assoc :suffix "FOR KEY SHARE")
(:for-update opts) (assoc :suffix "FOR UPDATE")
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
(sql/for-query table where-params opts))))
(defn update

View File

@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.emails
(ns app.email
"Main api for send emails."
(:require
[app.common.exceptions :as ex]
@@ -14,7 +14,7 @@
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.emails.invite-to-team :as-alias emails.invite-to-team]
[app.email.invite-to-team :as-alias email.invite-to-team]
[app.metrics :as mtx]
[app.util.template :as tmpl]
[app.worker :as wrk]
@@ -37,6 +37,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- parse-address
^"[Ljakarta.mail.internet.InternetAddress;"
[v]
(InternetAddress/parse ^String v))
@@ -64,14 +65,14 @@
(some? bcc) (assign-recipient :bcc bcc)))
(defn- assign-from
[mmsg {:keys [default-from]} {:keys [from] :as props}]
[mmsg {:keys [::default-from] :as cfg} {:keys [from] :as params}]
(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}]
[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)]
@@ -127,9 +128,8 @@
mmsg))
(defn- opts->props
[{:keys [username tls host port timeout default-from]
:or {timeout 30000}
:as opts}]
[{:keys [::username ::tls ::host ::port ::timeout ::default-from]
:or {timeout 30000}}]
(reduce-kv
(fn [^Properties props k v]
(if (nil? v)
@@ -150,8 +150,9 @@
"mail.smtp.connectiontimeout" timeout}))
(defn- create-smtp-session
[opts]
(let [props (opts->props opts)]
^Session
[cfg]
(let [props (opts->props cfg)]
(Session/getInstance props)))
(defn- create-smtp-message
@@ -171,7 +172,7 @@
;; TEMPLATE EMAIL IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private email-path "app/emails/%(id)s/%(lang)s.%(type)s")
(def ^:private email-path "app/email/%(id)s/%(lang)s.%(type)s")
(defn- render-email-template-part
[type id context]
@@ -283,14 +284,14 @@
(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]))
(s/keys :opt [::username
::password
::tls
::ssl
::host
::port
::default-from
::default-reply-to]))
(declare send-to-logger!)
@@ -306,8 +307,8 @@
(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))
^String (::username cfg)
^String (::password cfg))
(let [^MimeMessage message (create-smtp-message cfg session params)]
(.sendMessage ^Transport transport
@@ -319,10 +320,10 @@
(send-to-logger! cfg params))))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::sendmail ::mtx/metrics]))
(s/keys :req [::sendmail ::mtx/metrics]))
(defmethod ig/init-key ::handler
[_ {:keys [sendmail]}]
[_ {:keys [::sendmail]}]
(fn [{:keys [props] :as task}]
(sendmail props)))
@@ -380,14 +381,14 @@
"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 ::email.invite-to-team/invited-by ::us/string)
(s/def ::email.invite-to-team/team ::us/string)
(s/def ::email.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]))
(s/keys :req-un [::email.invite-to-team/invited-by
::email.invite-to-team/token
::email.invite-to-team/team]))
(def invite-to-team
"Teams member invitation email."

View File

@@ -6,23 +6,34 @@
(ns app.http
(:require
[app.auth.oidc :as-alias oidc]
[app.common.data :as d]
[app.common.logging :as l]
[app.common.transit :as t]
[app.db :as-alias db]
[app.http.access-token :as actoken]
[app.http.assets :as-alias assets]
[app.http.awsns :as-alias awsns]
[app.http.debug :as-alias debug]
[app.http.errors :as errors]
[app.http.middleware :as mw]
[app.http.session :as session]
[app.http.websocket :as-alias ws]
[app.main :as-alias main]
[app.metrics :as mtx]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]
[reitit.core :as r]
[reitit.middleware :as rr]
[yetti.adapter :as yt]
[yetti.request :as yrq]
[yetti.response :as yrs]))
[yetti.response :as-alias yrs]))
(declare wrap-router)
(declare router-handler)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP SERVER
@@ -37,140 +48,131 @@
(s/def ::max-body-size integer?)
(s/def ::max-multipart-body-size integer?)
(s/def ::io-threads integer?)
(s/def ::worker-threads integer?)
(defmethod ig/prep-key ::server
[_ cfg]
(merge {:name "http"
:port 6060
:host "0.0.0.0"
:max-body-size (* 1024 1024 30) ; 30 MiB
:max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
(merge {::port 6060
::host "0.0.0.0"
::max-body-size (* 1024 1024 30) ; 30 MiB
::max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
(d/without-nils cfg)))
(defmethod ig/pre-init-spec ::server [_]
(s/and
(s/keys :req-un [::port ::host ::name ::max-body-size ::max-multipart-body-size]
:opt-un [::router ::handler ::io-threads ::worker-threads ::wrk/executor])
(fn [cfg]
(or (contains? cfg :router)
(contains? cfg :handler)))))
(s/keys :req [::port ::host]
:opt [::max-body-size
::max-multipart-body-size
::router
::handler
::io-threads
::wrk/executor]))
(defmethod ig/init-key ::server
[_ {:keys [handler router port name host] :as cfg}]
(l/info :hint "starting http server" :port port :host host :name name)
[_ {:keys [::handler ::router ::host ::port] :as cfg}]
(l/info :hint "starting http server" :port port :host host)
(let [options {:http/port port
:http/host host
:http/max-body-size (:max-body-size cfg)
:http/max-multipart-body-size (:max-multipart-body-size cfg)
:xnio/io-threads (:io-threads cfg)
:xnio/worker-threads (:worker-threads cfg)
:xnio/dispatch (:executor cfg)
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/io-threads (or (::io-threads cfg)
(max 3 (px/get-available-processors)))
:xnio/worker-threads (or (::worker-threads cfg)
(max 6 (px/get-available-processors)))
:xnio/dispatch true
:socket/backlog 4069
:ring/async true}
handler (if (some? router)
(wrap-router router)
handler (cond
(some? router)
(router-handler router)
handler)
server (yt/server handler (d/without-nils options))]
(assoc cfg :server (yt/start! server))))
(some? handler)
handler
:else
(throw (UnsupportedOperationException. "handler or router are required")))
options (d/without-nils options)
server (yt/server handler options)]
(assoc cfg ::server (yt/start! server))))
(defmethod ig/halt-key! ::server
[_ {:keys [server name port] :as cfg}]
(l/info :msg "stopping http server" :name name :port port)
[_ {:keys [::server ::port] :as cfg}]
(l/info :msg "stopping http server" :port port)
(yt/stop! server))
(defn- not-found-handler
[_ respond _]
(respond (yrs/response 404)))
(respond {::yrs/status 404}))
(defn- wrap-router
(defn- router-handler
[router]
(letfn [(handler [request respond raise]
(letfn [(resolve-handler [request]
(if-let [match (r/match-by-path router (yrq/path request))]
(let [params (:path-params match)
result (:result match)
handler (or (:handler result) not-found-handler)
request (assoc request :path-params params)]
(handler request respond raise))
(not-found-handler request respond raise)))
(partial handler request))
(partial not-found-handler request)))
(on-error [cause request respond]
(on-error [cause request]
(let [{:keys [body] :as response} (errors/handle cause request)]
(respond
(cond-> response
(map? body)
(-> (update :headers assoc "content-type" "application/transit+json")
(assoc :body (t/encode-str body {:type :json-verbose})))))))]
(cond-> response
(map? body)
(-> (update ::yrs/headers assoc "content-type" "application/transit+json")
(assoc ::yrs/body (t/encode-str body {:type :json-verbose}))))))]
(fn [request respond _]
(try
(handler request respond #(on-error % request respond))
(catch Throwable cause
(on-error cause request respond))))))
(let [handler (resolve-handler request)
exchange (yrq/exchange request)]
(handler
(fn [response]
(yt/dispatch! exchange (partial respond response)))
(fn [cause]
(let [response (on-error cause request)]
(yt/dispatch! exchange (partial respond response)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP ROUTER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::assets 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 ::session/session)
(s/def ::storage map?)
(s/def ::ws fn?)
(defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::mtx/metrics
::ws
::storage
::assets
::session
::feedback
::awsns-handler
::debug-routes
::oidc-routes
::rpc-routes
::doc-routes]))
(s/keys :req [::session/manager
::ws/routes
::rpc/routes
::rpc.doc/routes
::oidc/routes
::main/props
::assets/routes
::debug/routes
::db/pool
::mtx/routes
::awsns/routes]))
(defmethod ig/init-key ::router
[_ {:keys [ws session metrics assets feedback] :as cfg}]
[_ cfg]
(rr/router
[["" {:middleware [[mw/server-timing]
[mw/format-response]
[mw/params]
[mw/format-response]
[mw/parse-request]
[session/middleware-1 session]
[session/soft-auth cfg]
[actoken/soft-auth cfg]
[mw/errors errors/handle]
[mw/restrict-methods]]}
[mw/restrict-methods]
[mw/with-dispatch :vthread]]}
["/metrics" {:handler (::mtx/handler metrics)
:allowed-methods #{:get}}]
["/assets" {:middleware [[session/middleware-2 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)}]]
(:debug-routes cfg)
(::mtx/routes cfg)
(::assets/routes cfg)
(::debug/routes cfg)
["/webhooks"
["/sns" {:handler (:awsns-handler cfg)
:allowed-methods #{:post}}]]
(::awsns/routes cfg)]
["/ws/notifications" {:middleware [[session/middleware-2 session]]
:handler ws
:allowed-methods #{:get}}]
(::ws/routes cfg)
["/api" {:middleware [[mw/cors]
[session/middleware-2 session]]}
["/feedback" {:handler feedback
:allowed-methods #{:post}}]
(:doc-routes cfg)
(:oidc-routes cfg)
(:rpc-routes cfg)]]]))
["/api" {:middleware [[mw/cors]]}
(::oidc/routes cfg)
(::rpc.doc/routes cfg)
(::rpc/routes cfg)]]]))

View File

@@ -0,0 +1,84 @@
;; 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.http.access-token
(:require
[app.common.logging :as l]
[app.config :as cf]
[app.db :as db]
[app.main :as-alias main]
[app.tokens :as tokens]
[yetti.request :as yrq]))
(def header-re #"^Token\s+(.*)")
(defn- get-token
[request]
(some->> (yrq/get-header request "authorization")
(re-matches header-re)
(second)))
(defn- decode-token
[props token]
(when token
(tokens/verify props {:token token :iss "access-token"})))
(def sql:get-token-data
"SELECT perms, profile_id, expires_at
FROM access_token
WHERE id = ?
AND (expires_at IS NULL
OR (expires_at > now()));")
(defn- get-token-data
[pool token-id]
(when-not (db/read-only? pool)
(some-> (db/exec-one! pool [sql:get-token-data token-id])
(update :perms db/decode-pgarray #{}))))
(defn- wrap-soft-auth
"Soft Authentication, will be executed synchronously on the undertow
worker thread."
[handler {:keys [::main/props]}]
(letfn [(handle-request [request]
(try
(let [token (get-token request)
claims (decode-token props token)]
(cond-> request
(map? claims)
(assoc ::id (:tid claims))))
(catch Throwable cause
(l/trace :hint "exception on decoding malformed token" :cause cause)
request)))]
(fn [request respond raise]
(let [request (handle-request request)]
(handler request respond raise)))))
(defn- wrap-authz
"Authorization middleware, will be executed synchronously on vthread."
[handler {:keys [::db/pool]}]
(fn [request]
(let [{:keys [perms profile-id expires-at]} (some->> (::id request) (get-token-data pool))]
(handler (cond-> request
(some? perms)
(assoc ::perms perms)
(some? profile-id)
(assoc ::profile-id profile-id)
(some? expires-at)
(assoc ::expires-at expires-at))))))
(def soft-auth
{:name ::soft-auth
:compile (fn [& _]
(when (contains? cf/flags :access-tokens)
wrap-soft-auth))})
(def authz
{:name ::authz
:compile (fn [& _]
(when (contains? cf/flags :access-tokens)
wrap-authz))})

View File

@@ -7,19 +7,16 @@
(ns app.http.assets
"Assets related handlers."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uri :as u]
[app.db :as db]
[app.metrics :as mtx]
[app.storage :as sto]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.response :as yrs]))
[yetti.response :as-alias yrs]))
(def ^:private cache-max-age
(dt/duration {:hours 24}))
@@ -27,105 +24,87 @@
(def ^:private signature-max-age
(dt/duration {:hours 24 :minutes 15}))
(defn coerce-id
[id]
(let [res (parse-uuid id)]
(when-not (uuid? res)
(defn get-id
[{:keys [path-params]}]
(or (some-> path-params :id d/parse-uuid)
(ex/raise :type :not-found
:hint "object not found"))
res))
:hunt "object not found")))
(defn- get-file-media-object
[{:keys [pool executor] :as storage} id]
(px/with-dispatch executor
(let [id (coerce-id id)
mobj (db/exec-one! pool ["select * from file_media_object where id=?" id])]
(when-not mobj
(ex/raise :type :not-found
:hint "object does not found"))
mobj)))
[pool id]
(db/get pool :file-media-object {:id id}))
(defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj]
(let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
{::yrs/status 307
::yrs/headers {"location" (str url)
"x-host" (cond-> host port (str ":" port))
"x-mtype" (-> obj meta :content-type)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}}))
(defn- serve-object-from-fs
[{:keys [::path]} obj]
(let [purl (u/join (u/uri path)
(sto/object->relative-path obj))
mdata (meta obj)
headers {"x-accel-redirect" (:path purl)
"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
{::yrs/status 204
::yrs/headers headers}))
(defn- serve-object
"Helper function that returns the appropriate response depending on
the storage object backend type."
[{:keys [storage] :as cfg} obj]
(let [mdata (meta obj)
backend (sto/resolve-backend storage (:backend obj))]
(case (:type backend)
: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
(p/let [purl (u/uri (:assets-path cfg))
purl (u/join purl (sto/object->relative-path obj))]
(yrs/response :status 204
:headers {"x-accel-redirect" (:path purl)
"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))})))))
[{:keys [::sto/storage] :as cfg} {:keys [backend] :as obj}]
(let [backend (sto/resolve-backend storage backend)]
(case (::sto/type backend)
:s3 (serve-object-from-s3 cfg obj)
:fs (serve-object-from-fs cfg obj))))
(defn objects-handler
"Handler that servers storage objects by id."
[{:keys [storage executor] :as cfg} request respond raise]
(-> (px/with-dispatch executor
(p/let [id (get-in request [:path-params :id])
id (coerce-id id)
obj (sto/get-object storage id)]
(if obj
(serve-object cfg obj)
(yrs/response 404))))
(p/bind p/wrap)
(p/then' respond)
(p/catch raise)))
[{:keys [::sto/storage] :as cfg} request]
(let [id (get-id request)
obj (sto/get-object storage id)]
(if obj
(serve-object cfg obj)
{::yrs/status 404})))
(defn- generic-handler
"A generic handler helper/common code for file-media based handlers."
[{:keys [storage] :as cfg} request kf]
(p/let [id (get-in request [:path-params :id])
mobj (get-file-media-object storage id)
obj (sto/get-object storage (kf mobj))]
(if obj
(serve-object cfg obj)
(yrs/response 404))))
[{:keys [::sto/storage] :as cfg} request kf]
(let [pool (::db/pool storage)
id (get-id request)
mobj (get-file-media-object pool id)
sobj (sto/get-object storage (kf mobj))]
(if sobj
(serve-object cfg sobj)
{::yrs/status 404})))
(defn file-objects-handler
"Handler that serves storage objects by file media id."
[cfg request respond raise]
(-> (generic-handler cfg request :media-id)
(p/then respond)
(p/catch raise)))
[cfg request]
(generic-handler cfg request :media-id))
(defn file-thumbnails-handler
"Handler that serves storage objects by thumbnail-id and quick
fallback to file-media-id if no thumbnail is available."
[cfg request respond raise]
(-> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
(p/then respond)
(p/catch raise)))
[cfg request]
(generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))))
;; --- Initialization
(s/def ::storage some?)
(s/def ::assets-path ::us/string)
(s/def ::cache-max-age ::dt/duration)
(s/def ::signature-max-age ::dt/duration)
(s/def ::path ::us/string)
(s/def ::routes vector?)
(defmethod ig/pre-init-spec ::handlers [_]
(s/keys :req-un [::storage
::wrk/executor
::mtx/metrics
::assets-path
::cache-max-age
::signature-max-age]))
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::sto/storage ::path]))
(defmethod ig/init-key ::handlers
(defmethod ig/init-key ::routes
[_ cfg]
{:objects-handler (partial objects-handler cfg)
:file-objects-handler (partial file-objects-handler cfg)
:file-thumbnails-handler (partial file-thumbnails-handler cfg)})
["/assets"
["/by-id/:id" {:handler (partial objects-handler cfg)}]
["/by-file-media-id/:id" {:handler (partial file-objects-handler cfg)}]
["/by-file-media-id/:id/thumbnail" {:handler (partial file-thumbnails-handler cfg)}]])

View File

@@ -21,25 +21,27 @@
[jsonista.core :as j]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
[yetti.response :as-alias yrs]))
(declare parse-json)
(declare handle-request)
(declare parse-notification)
(declare process-report)
(defmethod ig/pre-init-spec ::handler [_]
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::http/client
::main/props
::db/pool
::wrk/executor]))
(defmethod ig/init-key ::handler
(defmethod ig/init-key ::routes
[_ {:keys [::wrk/executor] :as cfg}]
(fn [request respond _]
(let [data (-> request yrq/body slurp)]
(px/run! executor #(handle-request cfg data)))
(respond (yrs/response 200))))
(letfn [(handler [request respond _]
(let [data (-> request yrq/body slurp)]
(px/run! executor #(handle-request cfg data)))
(respond {::yrs/status 200}))]
["/sns" {:handler handler
:allowed-methods #{:post}}]))
(defn handle-request
[cfg data]
@@ -105,8 +107,7 @@
[cfg headers]
(let [tdata (get headers "x-penpot-data")]
(when-not (str/empty? tdata)
(let [sprops (::main/props cfg)
result (tokens/verify sprops {:token tdata :iss :profile-identity})]
(let [result (tokens/verify (::main/props cfg) {:token tdata :iss :profile-identity})]
(:profile-id result)))))
(defn- parse-notification

View File

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

View File

@@ -13,15 +13,14 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.middleware :as mw]
[app.http.session :as session]
[app.rpc.commands.binfile :as binf]
[app.rpc.commands.files.create :refer [create-file]]
[app.rpc.queries.profile :as profile]
[app.rpc.commands.files-create :refer [create-file]]
[app.rpc.commands.profile :as profile]
[app.storage :as-alias sto]
[app.util.blob :as blob]
[app.util.template :as tmpl]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.io :as io]
@@ -39,36 +38,40 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn authorized?
[pool {:keys [profile-id]}]
[pool {:keys [::session/profile-id]}]
(or (= "devenv" (cf/get :host))
(let [profile (ex/ignoring (profile/retrieve-profile-data pool profile-id))
(let [profile (ex/ignoring (profile/get-profile pool profile-id))
admins (or (cf/get :admins) #{})]
(contains? admins (:email profile)))))
(defn prepare-response
[body]
(let [headers {"content-type" "application/transit+json"}]
(yrs/response :status 200 :body body :headers headers)))
{::yrs/status 200
::yrs/body body
::yrs/headers headers}))
(defn prepare-download-response
[body filename]
(let [headers {"content-disposition" (str "attachment; filename=" filename)
"content-type" "application/octet-stream"}]
(yrs/response :status 200 :body body :headers headers)))
{::yrs/status 200
::yrs/body body
::yrs/headers headers}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INDEX
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn index-handler
[{:keys [pool]} request]
[{:keys [::db/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 "app/templates/debug.tmpl")
(tmpl/render {}))))
{::yrs/status 200
::yrs/headers {"content-type" "text/html"}
::yrs/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {}))})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES
@@ -81,7 +84,7 @@
"select revn, changes, data from file_change where file_id=? and revn = ?")
(defn- retrieve-file-data
[{:keys [pool]} {:keys [params profile-id] :as request}]
[{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
@@ -107,17 +110,19 @@
(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)]
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)
data (blob/decode data)]
(create-file pool {:id (uuid/next)
:name (str "Cloned file: " filename)
:project-id project-id
:profile-id profile-id
:data data})
(yrs/response 201 "OK CREATED"))
{::yrs/status 201
::yrs/body "OK CREATED"})
:else
(prepare-response (some-> data blob/decode))))))
(prepare-response (blob/decode data))))))
(defn- is-file-exists?
[pool id]
@@ -125,8 +130,9 @@
(-> (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)
[{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}]
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)
data (some-> params :file :path io/read-as-bytes blob/decode)]
(if (and data project-id)
@@ -141,7 +147,8 @@
(db/update! pool :file
{:data (blob/encode data)}
{:id file-id})
(yrs/response 200 "OK UPDATED"))
{::yrs/status 200
::yrs/body "OK UPDATED"})
(do
(create-file pool {:id file-id
@@ -149,9 +156,11 @@
:project-id project-id
:profile-id profile-id
:data data})
(yrs/response 201 "OK CREATED"))))
{::yrs/status 201
::yrs/body "OK CREATED"})))
(yrs/response 500 "ERROR"))))
{::yrs/status 500
::yrs/body "ERROR"})))
(defn file-data-handler
[cfg request]
@@ -162,7 +171,7 @@
:code :method-not-found)))
(defn file-changes-handler
[{:keys [pool]} {:keys [params] :as request}]
[{:keys [::db/pool]} {:keys [params] :as request}]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
@@ -202,46 +211,53 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn error-handler
[{:keys [pool]} request]
(letfn [(parse-id [request]
(let [id (get-in request [:path-params :id])
id (parse-uuid id)]
(when (uuid? id)
id)))
(retrieve-report [id]
[{:keys [::db/pool]} request]
(letfn [(get-report [{:keys [path-params]}]
(ex/ignoring
(some-> (db/get-by-id pool :server-error-report id) :content db/decode-transit-pgobject)))
(let [report-id (some-> path-params :id parse-uuid)]
(some-> (db/get-by-id pool :server-error-report report-id)
(update :content db/decode-transit-pgobject)))))
(render-template [report]
(let [context (dissoc report
(render-template-v1 [{:keys [content]}]
(let [context (dissoc content
:trace :cause :params :data :spec-problems :message
:spec-explain :spec-value :error :explain :hint)
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)
:data (:data report)
:trace (or (:trace report)
(some-> report :error :trace))
:params (:params report)}]
:hint (:hint content)
:spec-explain (:spec-explain content)
:spec-problems (:spec-problems content)
:spec-value (:spec-value content)
:data (:data content)
:trace (or (:trace content)
(some-> content :error :trace))
:params (:params content)}]
(-> (io/resource "app/templates/error-report.tmpl")
(tmpl/render params))))]
(tmpl/render params))))
(render-template-v2 [{report :content}]
(-> (io/resource "app/templates/error-report.v2.tmpl")
(tmpl/render report)))
(render-template-v3 [{report :content}]
(-> (io/resource "app/templates/error-report.v3.tmpl")
(tmpl/render report)))
]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [result (some-> (parse-id request)
(retrieve-report)
(render-template))]
(if result
(yrs/response :status 200
:body result
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})
(yrs/response 404 "not found")))))
(if-let [report (get-report request)]
(let [result (case (:version report)
1 (render-template-v1 report)
2 (render-template-v2 report)
3 (render-template-v3 report))]
{::yrs/status 200
::yrs/body result
::yrs/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}})
{::yrs/status 404
::yrs/body "not found"})))
(def sql:error-reports
"SELECT id, created_at,
@@ -251,24 +267,24 @@
LIMIT 100")
(defn error-list-handler
[{:keys [pool]} request]
[{:keys [::db/pool]} request]
(when-not (authorized? pool request)
(ex/raise :type :authentication
:code :only-admins-allowed))
(let [items (->> (db/exec! pool [sql:error-reports])
(map #(update % :created-at dt/format-instant :rfc1123)))]
(yrs/response :status 200
:body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items}))
:headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})))
{::yrs/status 200
::yrs/body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items}))
::yrs/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXPORT/IMPORT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn export-handler
[{:keys [pool] :as cfg} {:keys [params profile-id] :as request}]
[{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}]
(let [file-ids (->> (:file-ids params)
(remove empty?)
@@ -287,7 +303,8 @@
(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)]
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)]
(binf/import!
(assoc cfg
::binf/input path
@@ -296,28 +313,28 @@
::binf/profile-id profile-id
::binf/project-id project-id))
(yrs/response
:status 200
:headers {"content-type" "text/plain"}
:body "OK CLONED"))
{::yrs/status 200
::yrs/headers {"content-type" "text/plain"}
::yrs/body "OK CLONED"})
{::yrs/status 200
::yrs/body (io/input-stream path)
::yrs/headers {"content-type" "application/octet-stream"
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}}))))
(yrs/response
:status 200
:headers {"content-type" "application/octet-stream"
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}
:body (io/input-stream path))))))
(defn import-handler
[{:keys [pool] :as cfg} {:keys [params profile-id] :as request}]
[{:keys [::db/pool] :as cfg} {:keys [params ::session/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)
(let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)
overwrite? (contains? params :overwrite)
migrate? (contains? params :migrate)
migrate? (contains? params :migrate)
ignore-index-errors? (contains? params :ignore-index-errors)]
(when-not project-id
@@ -334,10 +351,9 @@
::binf/profile-id profile-id
::binf/project-id project-id))
(yrs/response
:status 200
:headers {"content-type" "text/plain"}
:body "OK")))
{::yrs/status 200
::yrs/headers {"content-type" "text/plain"}
::yrs/body "OK"}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS
@@ -345,15 +361,16 @@
(defn health-handler
"Mainly a task that performs a health check."
[{:keys [pool]} _]
(db/with-atomic [conn pool]
(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")))))
[{:keys [::db/pool]} _]
(try
(db/exec-one! pool ["select count(*) as count from server_prop;"])
{::yrs/status 200
::yrs/body "OK"}
(catch Throwable cause
(l/warn :hint "unable to execute query on health handler"
:cause cause)
{::yrs/status 503
::yrs/body "KO"})))
(defn changelog-handler
[_ _]
@@ -362,10 +379,11 @@
(md->html [text]
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
(if-let [clog (io/resource "changelog.md")]
(yrs/response :status 200
:headers {"content-type" "text/html; charset=utf-8"}
:body (-> clog slurp md->html))
(yrs/response :status 404 :body "NOT FOUND"))))
{::yrs/status 200
::yrs/headers {"content-type" "text/html; charset=utf-8"}
::yrs/body (-> clog slurp md->html)}
{::yrs/status 404
::yrs/body "NOT FOUND"})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INIT
@@ -375,32 +393,26 @@
{:compile
(fn [& _]
(fn [handler pool]
(fn [request respond raise]
(fn [request]
(if (authorized? pool request)
(handler request respond raise)
(raise (ex/error :type :authentication
:code :only-admins-allowed))))))})
(handler request)
(ex/raise :type :authentication
:code :only-admins-allowed)))))})
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req-un [::db/pool ::wrk/executor ::session/session]))
(s/keys :req [::db/pool ::session/manager]))
(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 [[session/middleware-2 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}]]])
[_ {:keys [::db/pool] :as cfg}]
[["/readyz" {:handler (partial health-handler cfg)}]
["/dbg" {:middleware [[session/authz cfg]
[with-authorization pool]]}
["" {:handler (partial index-handler cfg)}]
["/health" {:handler (partial health-handler cfg)}]
["/changelog" {:handler (partial changelog-handler cfg)}]
["/error/:id" {:handler (partial error-handler cfg)}]
["/error" {:handler (partial error-list-handler cfg)}]
["/file/export" {:handler (partial export-handler cfg)}]
["/file/import" {:handler (partial import-handler cfg)}]
["/file/data" {:handler (partial file-data-handler cfg)}]
["/file/changes" {:handler (partial file-changes-handler cfg)}]]])

View File

@@ -7,37 +7,37 @@
(ns app.http.errors
"A errors handling for the http server."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.session :as-alias session]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(def ^:dynamic *context* {})
(defn- parse-client-ip
[request]
(or (some-> (yrq/get-header request "x-forwarded-for") (str/split ",") first)
(yrq/get-header request "x-real-ip")
(yrq/remote-addr request)))
(defn get-context
(defn request->context
"Extracts error report relevant context data from request."
[request]
(let [claims (:session-token-claims request)]
(merge
*context*
{:path (:path request)
:method (:method request)
:params (:params request)
:ip-addr (parse-client-ip request)}
(d/without-nils
{:user-agent (yrq/get-header request "user-agent")
:frontend-version (or (yrq/get-header request "x-frontend-version")
"unknown")
:profile-id (:uid claims)}))))
(let [claims (-> {}
(into (::session/token-claims request))
(into (::actoken/token-claims request)))]
{:path (:path request)
:method (:method request)
:params (:params request)
:ip-addr (parse-client-ip request)
:user-agent (yrq/get-header request "user-agent")
:profile-id (:uid claims)
:version (or (yrq/get-header request "x-frontend-version")
"unknown")}))
(defmulti handle-exception
(fn [err & _rest]
@@ -47,16 +47,30 @@
(defmethod handle-exception :authentication
[err _]
(yrs/response 401 (ex-data err)))
{::yrs/status 401
::yrs/body (ex-data err)})
(defmethod handle-exception :authorization
[err _]
{::yrs/status 403
::yrs/body (ex-data err)})
(defmethod handle-exception :restriction
[err _]
(yrs/response 400 (ex-data err)))
{::yrs/status 400
::yrs/body (ex-data err)})
(defmethod handle-exception :rate-limit
[err _]
(let [headers (-> err ex-data ::http/headers)]
(yrs/response :status 429 :body "" :headers headers)))
{::yrs/status 429
::yrs/headers headers}))
(defmethod handle-exception :concurrency-limit
[err _]
(let [headers (-> err ex-data ::http/headers)]
{::yrs/status 429
::yrs/headers headers}))
(defmethod handle-exception :validation
[err _]
@@ -64,74 +78,99 @@
(cond
(= code :spec-validation)
(let [explain (ex/explain data)]
(yrs/response :status 400
:body (-> data
(dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain)))))
{::yrs/status 400
::yrs/body (-> data
(dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain)))})
(= code :params-validation)
(let [explain (::sm/explain data)
payload (sm/humanize-data explain)]
{::yrs/status 400
::yrs/body (-> data
(dissoc ::sm/explain)
(assoc :data payload))})
(= code :request-body-too-large)
(yrs/response :status 413 :body data)
{::yrs/status 413 ::yrs/body data}
:else
(yrs/response :status 400 :body data))))
{::yrs/status 400 ::yrs/body data})))
(defmethod handle-exception :assertion
[error request]
(let [edata (ex-data error)
explain (ex/explain edata)]
(l/error :hint (ex-message error)
:cause error
::l/context (get-context request))
(yrs/response :status 500
:body {:type :server-error
:code :assertion
:data (-> edata
(dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))})))
(binding [l/*context* (request->context request)]
(let [{:keys [code] :as data} (ex-data error)]
(cond
(= code :data-validation)
(let [explain (::sm/explain data)
payload (sm/humanize-data explain)]
(l/error :hint "Data assertion error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :assertion
:data (-> data
(dissoc ::sm/explain)
(assoc :data payload))}})
(= code :spec-validation)
(let [explain (ex/explain data)]
(l/error :hint "Spec assertion error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :assertion
:data (-> data
(dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))}})
:else
(do
(l/error :hint "Assertion error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :assertion
:data data}})))))
(defmethod handle-exception :not-found
[err _]
(yrs/response 404 (ex-data err)))
{::yrs/status 404
::yrs/body (ex-data err)})
(defmethod handle-exception :internal
[error request]
(let [{:keys [code] :as edata} (ex-data error)]
(cond
(= :concurrency-limit-reached code)
(yrs/response 429)
:else
(do
(l/error :hint (ex-message error)
:cause error
::l/context (get-context request))
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
(binding [l/*context* (request->context request)]
(l/error :hint "Internal error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :unhandled
:hint (ex-message error)
:data (ex-data error)}}))
(defmethod handle-exception org.postgresql.util.PSQLException
[error request]
(let [state (.getSQLState ^java.sql.SQLException error)]
(l/error :hint (ex-message error)
:cause error
::l/context (get-context request))
(cond
(= state "57014")
(yrs/response 504 {:type :server-error
:code :statement-timeout
:hint (ex-message error)})
(binding [l/*context* (request->context request)]
(l/error :hint "PSQL error" :message (ex-message error) :cause error)
(cond
(= state "57014")
{::yrs/status 504
::yrs/body {:type :server-error
:code :statement-timeout
:hint (ex-message error)}}
(= state "25P03")
(yrs/response 504 {:type :server-error
:code :idle-in-transaction-timeout
:hint (ex-message error)})
(= state "25P03")
{::yrs/status 504
::yrs/body {:type :server-error
:code :idle-in-transaction-timeout
:hint (ex-message error)}}
:else
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)
:state state}))))
:else
{::yrs/status 500
::yrs/body {:type :server-error
:code :unexpected
:hint (ex-message error)
:state state}}))))
(defmethod handle-exception :default
[error request]
@@ -139,13 +178,12 @@
(cond
;; This means that exception is not a controlled exception.
(nil? edata)
(do
(l/error :hint (ex-message error)
:cause error
::l/context (get-context request))
(yrs/response 500 {:type :server-error
:code :unexpected
:hint (ex-message error)}))
(binding [l/*context* (request->context request)]
(l/error :hint "Unexpected error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :unexpected
:hint (ex-message error)}})
;; This is a special case for the idle-in-transaction error;
;; when it happens, the connection is automatically closed and
@@ -157,27 +195,17 @@
(handle-exception (:handling edata) request)
:else
(do
(l/error :hint (ex-message error)
:cause error
::l/context (get-context request))
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
(binding [l/*context* (request->context request)]
(l/error :hint "Unhandled error" :message (ex-message error) :cause error)
{::yrs/status 500
::yrs/body {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata}}))))
(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)]
(binding [*context* context]
(handle-exception cause request)))
:else
(if (or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))
(handle-exception (ex-cause cause) request)
(handle-exception cause request)))

View File

@@ -1,80 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.http.feedback
"A general purpose feedback module."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.rpc.queries.profile :as profile]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
(declare ^:private send-feedback)
(declare ^:private handler)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::wrk/executor]))
(defmethod ig/init-key ::handler
[_ {:keys [executor] :as cfg}]
(let [enabled? (contains? cf/flags :user-feedback)]
(if enabled?
(fn [request respond raise]
(-> (px/submit! executor #(handler cfg request))
(p/then' respond)
(p/catch raise)))
(fn [_ _ raise]
(raise (ex/error :type :validation
:code :feedback-disabled
:hint "feedback module is disabled"))))))
(defn- handler
[{:keys [pool] :as cfg} {:keys [profile-id] :as request}]
(let [ftoken (cf/get :feedback-token ::no-token)
token (yrq/get-header request "x-feedback-token")
params (d/merge (:params request)
(:body-params request))]
(cond
(uuid? profile-id)
(let [profile (profile/retrieve-profile-data pool profile-id)
params (assoc params :from (:email profile))]
(send-feedback pool profile params))
(= token ftoken)
(send-feedback cfg nil params))
(yrs/response 204)))
(s/def ::content ::us/string)
(s/def ::from ::us/email)
(s/def ::subject ::us/string)
(s/def ::feedback
(s/keys :req-un [::from ::subject ::content]))
(defn- send-feedback
[pool profile params]
(let [params (us/conform ::feedback params)
destination (cf/get :feedback-destination)]
(eml/send! {::eml/conn pool
::eml/factory eml/feedback
:from destination
:to destination
:profile profile
:reply-to (:from params)
:email (:from params)
:subject (:subject params)
:content (:content params)})
nil))

View File

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

View File

@@ -9,17 +9,17 @@
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.http.session.tasks :as-alias tasks]
[app.main :as-alias main]
[app.tokens :as tokens]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -45,97 +45,83 @@
(defprotocol ISessionManager
(read [_ key])
(decode [_ key])
(write! [_ key data])
(update! [_ data])
(delete! [_ key]))
(s/def ::session #(satisfies? ISessionManager %))
(s/def ::manager #(satisfies? ISessionManager %))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; STORAGE IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::session-params
(s/keys :req-un [::user-agent
::profile-id
::created-at]))
(defn- prepare-session-params
[props data]
(let [profile-id (:profile-id data)
user-agent (:user-agent data)
created-at (or (:created-at data) (dt/now))
token (tokens/generate props {:iss "authentication"
:iat created-at
:uid profile-id})]
{:user-agent user-agent
:profile-id profile-id
:created-at created-at
:updated-at created-at
:id token}))
[key params]
(us/assert! ::us/not-empty-string key)
(us/assert! ::session-params params)
{:user-agent (:user-agent params)
:profile-id (:profile-id params)
:created-at (:created-at params)
:updated-at (:created-at params)
:id key})
(defn- database-manager
[{:keys [::db/pool ::wrk/executor ::main/props]}]
[pool]
(reify ISessionManager
(read [_ token]
(px/with-dispatch executor
(db/exec-one! pool (sql/select :http-session {:id token}))))
(db/exec-one! pool (sql/select :http-session {:id token})))
(decode [_ token]
(px/with-dispatch executor
(tokens/verify props {:token token :iss "authentication"})))
(write! [_ key params]
(let [params (prepare-session-params key params)]
(db/insert! pool :http-session params)
params))
(write! [_ _ data]
(px/with-dispatch executor
(let [params (prepare-session-params props data)]
(db/insert! pool :http-session params)
params)))
(update! [_ data]
(update! [_ params]
(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))))
(db/update! pool :http-session
{:updated-at updated-at}
{:id (:id params)})
(assoc params :updated-at updated-at)))
(delete! [_ token]
(px/with-dispatch executor
(db/delete! pool :http-session {:id token})
nil))))
(db/delete! pool :http-session {:id token})
nil)))
(defn inmemory-manager
[{:keys [::wrk/executor ::main/props]}]
[]
(let [cache (atom {})]
(reify ISessionManager
(read [_ token]
(p/do (get @cache token)))
(get @cache token))
(decode [_ token]
(px/with-dispatch executor
(tokens/verify props {:token token :iss "authentication"})))
(write! [_ key params]
(let [params (prepare-session-params key params)]
(swap! cache assoc key params)
params))
(write! [_ _ data]
(p/do
(let [{:keys [token] :as params} (prepare-session-params props data)]
(swap! cache assoc token params)
params)))
(update! [_ data]
(p/do
(let [updated-at (dt/now)]
(swap! cache update (:id data) assoc :updated-at updated-at)
(assoc data :updated-at updated-at))))
(update! [_ params]
(let [updated-at (dt/now)]
(swap! cache update (:id params) assoc :updated-at updated-at)
(assoc params :updated-at updated-at)))
(delete! [_ token]
(p/do
(swap! cache dissoc token)
nil)))))
(swap! cache dissoc token)
nil))))
(defmethod ig/pre-init-spec ::manager [_]
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::manager
[_ {:keys [::db/pool] :as cfg}]
[_ {:keys [::db/pool]}]
(if (db/read-only? pool)
(inmemory-manager cfg)
(database-manager cfg)))
(inmemory-manager)
(database-manager pool)))
(defmethod ig/halt-key! ::manager
[_ _])
@@ -144,101 +130,111 @@
;; MANAGER IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare assign-auth-token-cookie)
(declare assign-authenticated-cookie)
(declare clear-auth-token-cookie)
(declare clear-authenticated-cookie)
(declare ^:private assign-auth-token-cookie)
(declare ^:private assign-authenticated-cookie)
(declare ^:private clear-auth-token-cookie)
(declare ^:private clear-authenticated-cookie)
(declare ^:private gen-token)
(defn create-fn
[manager profile-id]
[{:keys [::manager ::main/props]} profile-id]
(us/assert! ::manager manager)
(us/assert! ::us/uuid profile-id)
(fn [request response]
(let [uagent (yrq/get-header request "user-agent")
params {:profile-id profile-id
:user-agent uagent}]
(-> (write! manager nil params)
(p/then (fn [session]
(l/trace :hint "create" :profile-id profile-id)
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session))))))))
:user-agent uagent
:created-at (dt/now)}
token (gen-token props params)
session (write! manager token params)]
(l/trace :hint "create" :profile-id (str profile-id))
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)))))
(defn delete-fn
[manager]
(letfn [(delete [{:keys [profile-id] :as request}]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
cookie (yrq/get-cookie request cname)]
(l/trace :hint "delete" :profile-id profile-id)
(some->> (:value cookie) (delete! manager))))]
(fn [request response]
(p/do
(delete request)
(-> response
(assoc :status 204)
(assoc :body nil)
(clear-auth-token-cookie)
(clear-authenticated-cookie))))))
[{:keys [::manager]}]
(us/assert! ::manager manager)
(fn [request response]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
cookie (yrq/get-cookie request cname)]
(l/trace :hint "delete" :profile-id (:profile-id request))
(some->> (:value cookie) (delete! manager))
(-> response
(assoc :status 204)
(assoc :body nil)
(clear-auth-token-cookie)
(clear-authenticated-cookie)))))
(def middleware-1
(letfn [(decode-cookie [manager cookie]
(if-let [value (:value cookie)]
(decode manager value)
(p/resolved nil)))
(defn- gen-token
[props {:keys [profile-id created-at]}]
(tokens/generate props {:iss "authentication"
:iat created-at
:uid profile-id}))
(defn- decode-token
[props token]
(when token
(tokens/verify props {:token token :iss "authentication"})))
(wrap-handler [manager handler request respond raise]
(let [cookie (some->> (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
(yrq/get-cookie request))]
(->> (decode-cookie manager cookie)
(p/fnly (fn [claims _]
(cond-> request
(some? claims) (assoc :session-token-claims claims)
:always (handler respond raise)))))))]
{:name :session-1
:compile (fn [& _]
(fn [handler manager]
(partial wrap-handler manager handler)))}))
(defn- get-token
[request]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
cookie (some-> (yrq/get-cookie request cname) :value)]
(when-not (str/empty? cookie)
cookie)))
(def middleware-2
(letfn [(wrap-handler [manager handler request respond raise]
(-> (retrieve-session manager request)
(p/finally (fn [session cause]
(cond
(some? cause)
(raise cause)
(defn- get-session
[manager token]
(some->> token (read manager)))
(nil? session)
(handler request respond raise)
(defn- 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)))))
:else
(let [request (-> request
(assoc :profile-id (:profile-id session))
(assoc :session-id (:id session)))
respond (cond-> respond
(renew-session? session)
(wrap-respond manager session))]
(handler request respond raise)))))))
(defn- wrap-soft-auth
[handler {:keys [::manager ::main/props]}]
(us/assert! ::manager manager)
(letfn [(handle-request [request]
(try
(let [token (get-token request)
claims (decode-token props token)]
(cond-> request
(map? claims)
(-> (assoc ::token-claims claims)
(assoc ::token token))))
(catch Throwable cause
(l/trace :hint "exception on decoding malformed token" :cause cause)
request)))]
(retrieve-session [manager request]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
cookie (yrq/get-cookie request cname)]
(some->> (:value cookie) (read manager))))
(fn [request respond raise]
(let [request (handle-request request)]
(handler request respond raise)))))
(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)))))
(defn- wrap-authz
[handler {:keys [::manager]}]
(us/assert! ::manager manager)
(fn [request]
(let [session (get-session manager (::token request))
request (cond-> request
(some? session)
(assoc ::profile-id (:profile-id session)
::id (:id session)))]
;; Wrap respond with session renewal code
(wrap-respond [respond manager session]
(fn [response]
(p/let [session (update! manager session)]
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)
(respond)))))]
(cond-> (handler request)
(renew-session? session)
(-> (assign-auth-token-cookie session)
(assign-authenticated-cookie session))))))
{:name :session-2
:compile (fn [& _]
(fn [handler manager]
(partial wrap-handler manager handler)))}))
(def soft-auth
{:name ::soft-auth
:compile (constantly wrap-soft-auth)})
(def authz
{:name ::authz
:compile (constantly wrap-authz)})
;; --- IMPL
@@ -264,13 +260,16 @@
(defn- assign-authenticated-cookie
[response {updated-at :updated-at}]
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
domain (cf/get :authenticated-cookie-domain)
cname (cf/get :authenticated-cookie-name "authenticated")
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 "/"
@@ -280,41 +279,46 @@
:secure secure?}]
(cond-> response
(string? domain)
(update :cookies assoc name cookie))))
(update :cookies assoc cname cookie))))
(defn- clear-auth-token-cookie
[response]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
(update response :cookies assoc cname {:path "/" :value "" :max-age -1})))
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
(defn- clear-authenticated-cookie
[response]
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
domain (cf/get :authenticated-cookie-domain)]
(cond-> response
(string? domain)
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age -1}))))
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK: SESSION GC
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare sql:delete-expired)
(s/def ::tasks/max-age ::dt/duration)
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::tasks/gc [_]
(s/keys :req [::db/pool]
:opt [::tasks/max-age]))
(defmethod ig/pre-init-spec ::gc-task [_]
(s/keys :req-un [::db/pool]
:opt-un [::max-age]))
(defmethod ig/prep-key ::gc-task
(defmethod ig/prep-key ::tasks/gc
[_ cfg]
(merge {:max-age default-cookie-max-age}
(d/without-nils cfg)))
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)]
(merge {::tasks/max-age max-age} (d/without-nils cfg))))
(defmethod ig/init-key ::gc-task
[_ {:keys [pool max-age] :as cfg}]
(def ^:private
sql:delete-expired
"delete from http_session
where updated_at < now() - ?::interval
or (updated_at is null and
created_at < now() - ?::interval)")
(defmethod ig/init-key ::tasks/gc
[_ {:keys [::db/pool ::tasks/max-age] :as cfg}]
(l/debug :hint "initializing session gc task" :max-age max-age)
(fn [_]
(db/with-atomic [conn pool]
@@ -326,9 +330,3 @@
:deleted result)
result))))
(def ^:private
sql:delete-expired
"delete from http_session
where updated_at < now() - ?::interval
or (updated_at is null and
created_at < now() - ?::interval)")

View File

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

View File

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

View File

@@ -7,16 +7,18 @@
(ns app.loggers.database
"A specific logger impl that persists errors on the database."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.uuid :as uuid]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.loggers.zmq :as lzmq]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.exec :as px]))
[promesa.exec :as px]
[promesa.exec.csp :as sp]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Error Listener
@@ -27,73 +29,86 @@
(defonce enabled (atom true))
(defn- persist-on-database!
[{:keys [::db/pool] :as cfg} {:keys [id] :as event}]
[pool id report]
(when-not (db/read-only? pool)
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
(db/insert! pool :server-error-report
{:id id
:version 3
:content (db/tjson report)})))
(defn- parse-event-data
[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)))
{}
event))
(defn record->report
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
(us/assert! ::l/record record)
(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))
(assoc :version (:full cf/version))
(update :id #(or % (uuid/next)))))
(let [data (ex-data cause)]
(merge
{:context (-> context
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (cf/get :public-uri))
(assoc :version (:full cf/version))
(assoc :logger-name logger)
(assoc :logger-level level)
(dissoc :params)
(pp/pprint-str :width 200))
:props (pp/pprint-str props :width 200)
:hint (or (ex-message cause) @message)
:trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)}
(when-let [params (:params context)]
{:params (pp/pprint-str params :width 200)})
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
{:data (pp/pprint-str data :width 200)})
(when-let [value (-> data ::sm/explain :value)]
{:value (pp/pprint-str value :width 200)})
(when-let [explain (ex/explain data)]
{:explain explain}))))
(defn error-record?
[{:keys [::l/level ::l/cause]}]
(and (= :error level)
(ex/exception? cause)))
(defn- handle-event
[cfg event]
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
(try
(let [event (parse-event event)
uri (cf/get :public-uri)]
(let [uri (cf/get :public-uri)
report (-> record record->report d/without-nils)]
(l/debug :hint "registering error on database" :id id
:uri (str uri "/dbg/error/" id))
(l/debug :hint "registering error on database" :id (:id event)
:uri (str uri "/dbg/error/" (:id event)))
(persist-on-database! cfg event))
(persist-on-database! pool id report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn- error-event?
[event]
(= "error" (:logger/level event)))
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req [::db/pool ::lzmq/receiver]))
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::reporter
[_ {:keys [::lzmq/receiver] :as cfg}]
(px/thread
{:name "penpot/database-reporter"}
(l/info :hint "initializing database error persistence")
[_ cfg]
(let [input (sp/chan :buf (sp/sliding-buffer 32)
:xf (filter error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(let [input (a/chan (a/sliding-buffer 5)
(filter error-event?))]
(px/thread {:name "penpot/database-reporter" :virtual true}
(l/info :hint "initializing database error persistence")
(try
(lzmq/sub! receiver input)
(loop []
(when-let [msg (a/<!! input)]
(handle-event cfg msg))
(recur))
(when-let [record (sp/take! input)]
(handle-event cfg record)
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(a/close! input)
(sp/close! input)
(remove-watch l/log-record ::reporter)
(l/info :hint "reporter terminated"))))))
(defmethod ig/halt-key! ::reporter

View File

@@ -1,89 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.loggers.loki
"A Loki integration."
(:require
[app.common.logging :as l]
[app.config :as cf]
[app.http.client :as http]
[app.loggers.zmq :as lzmq]
[app.util.json :as json]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]))
(declare ^:private handle-event)
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req [::http/client
::lzmq/receiver]))
(defmethod ig/init-key ::reporter
[_ cfg]
(when-let [uri (cf/get :loggers-loki-uri)]
(px/thread
{:name "penpot/loki-reporter"}
(l/info :hint "reporter started" :uri uri)
(let [input (a/chan (a/dropping-buffer 2048))
cfg (assoc cfg ::uri uri)]
(try
(lzmq/sub! (::lzmq/receiver cfg) input)
(loop []
(when-let [msg (a/<!! input)]
(handle-event cfg msg)
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected exception"
:cause cause))
(finally
(a/close! input)
(l/info :hint "reporter terminated")))))))
(defmethod ig/halt-key! ::reporter
[_ thread]
(some-> thread px/interrupt!))
(defn- prepare-payload
[event]
(let [labels {:host (cf/get :host)
:tenant (cf/get :tenant)
:version (:full cf/version)
:logger (:logger/name event)
:level (:logger/level event)}]
{:streams
[{:stream labels
:values [[(str (* (inst-ms (:created-at event)) 1000000))
(str (:message event)
(when-let [error (:trace event)]
(str "\n" error)))]]}]}))
(defn- make-request
[{:keys [::uri] :as cfg} payload]
(http/req! cfg
{:uri uri
:timeout 3000
:method :post
:headers {"content-type" "application/json"}
:body (json/encode payload)}
{:sync? true}))
(defn- handle-event
[cfg event]
(try
(let [payload (prepare-payload event)
response (make-request cfg payload)]
(when-not (= 204 (:status response))
(l/error :hint "error on sending log to loki (unexpected response)"
:response (pr-str response))))
(catch Throwable cause
(l/error :hint "error on sending log to loki (unexpected exception)"
:cause cause))))

View File

@@ -7,24 +7,35 @@
(ns app.loggers.mattermost
"A mattermost integration for error reporting."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[app.http.client :as http]
[app.loggers.database :as ldb]
[app.loggers.zmq :as lzmq]
[app.util.json :as json]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]))
[promesa.exec :as px]
[promesa.exec.csp :as sp]))
(defonce enabled (atom true))
(defn- send-mattermost-notification!
[cfg {:keys [host id public-uri] :as event}]
(let [text (str "Exception on (host: " host ", url: " public-uri "/dbg/error/" id ")\n"
(when-let [pid (:profile-id event)]
(str "- profile-id: #uuid-" pid "\n")))
[cfg {:keys [id public-uri] :as report}]
(let [text (str "Exception: " public-uri "/dbg/error/" id " "
(when-let [pid (:profile-id report)]
(str "(pid: #uuid-" pid ")"))
"\n"
"```\n"
"- host: `" (:host report) "`\n"
"- tenant: `" (:tenant report) "`\n"
"- version: `" (:version report) "`\n"
"\n"
"Trace:\n"
(:trace report)
"```")
resp (http/req! cfg
{:uri (cf/get :error-report-webhook)
:method :post
@@ -36,32 +47,42 @@
(l/warn :hint "error on sending data"
:response (pr-str resp)))))
(defn record->report
[{:keys [::l/context ::l/id ::l/cause] :as record}]
(us/assert! ::l/record record)
{:id id
:tenant (cf/get :tenant)
:host (cf/get :host)
:public-uri (cf/get :public-uri)
:version (:full cf/version)
:profile-id (:profile-id context)
:trace (ex/format-throwable cause :detail? false :header? false)})
(defn handle-event
[cfg event]
[cfg record]
(when @enabled
(try
(let [event (ldb/parse-event event)]
(send-mattermost-notification! cfg event))
(let [report (record->report record)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error"
:cause cause)))))
(l/warn :hint "unhandled error" :cause cause)))))
(defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req [::http/client
::lzmq/receiver]))
(s/keys :req [::http/client]))
(defmethod ig/init-key ::reporter
[_ cfg]
(when-let [uri (cf/get :error-report-webhook)]
(px/thread
{:name "penpot/mattermost-reporter"}
(l/info :msg "initializing error reporter" :uri uri)
(let [input (a/chan (a/sliding-buffer 128)
(filter #(= (:logger/level %) "error")))]
{:name "penpot/mattermost-reporter"
:virtual true}
(l/info :hint "initializing error reporter" :uri uri)
(let [input (sp/chan :buf (sp/sliding-buffer 128)
:xf (filter ldb/error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(try
(lzmq/sub! (::lzmq/receiver cfg) input)
(loop []
(when-let [msg (a/<!! input)]
(when-let [msg (sp/take! input)]
(handle-event cfg msg)
(recur)))
(catch InterruptedException _
@@ -69,7 +90,8 @@
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(a/close! input)
(sp/close! input)
(remove-watch l/log-record ::reporter)
(l/info :hint "reporter terminated")))))))
(defmethod ig/halt-key! ::reporter

View File

@@ -111,7 +111,7 @@
" where id=?")
err
(:id whook)]
res (db/exec-one! pool sql {:return-keys true})]
res (db/exec-one! pool sql {::db/return-keys? true})]
(when (>= (:error-count res) max-errors)
(db/update! pool :webhook {:is-active false} {:id (:id whook)})))

View File

@@ -1,130 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.loggers.zmq
"A generic ZMQ listener."
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.config :as cf]
[app.loggers.zmq.receiver :as-alias receiver]
[app.util.json :as json]
[app.util.time :as dt]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.exec :as px])
(:import
org.zeromq.SocketType
org.zeromq.ZMQ$Socket
org.zeromq.ZContext))
(declare prepare)
(declare start-rcv-loop)
(defmethod ig/init-key ::receiver
[_ cfg]
(let [uri (cf/get :loggers-zmq-uri)
buffer (a/chan 1)
output (a/chan 1 (comp (filter map?)
(keep prepare)))
mult (a/mult output)
thread (when uri
(px/thread
{:name "penpot/zmq-receiver"
:daemon false}
(l/info :hint "receiver started")
(try
(start-rcv-loop buffer uri)
(catch InterruptedException _
(l/debug :hint "receiver interrupted"))
(catch java.lang.IllegalStateException cause
(if (= "errno 4" (ex-message cause))
(l/debug :hint "receiver interrupted")
(l/error :hint "unhandled error" :cause cause)))
(catch Throwable cause
(l/error :hint "unhandled error" :cause cause))
(finally
(l/info :hint "receiver terminated")))))]
(a/pipe buffer output)
(-> cfg
(assoc ::receiver/mult mult)
(assoc ::receiver/thread thread)
(assoc ::receiver/output output)
(assoc ::receiver/buffer buffer))))
(s/def ::receiver/mult some?)
(s/def ::receiver/thread #(instance? Thread %))
(s/def ::receiver/output some?)
(s/def ::receiver/buffer some?)
(s/def ::receiver
(s/keys :req [::receiver/mult
::receiver/thread
::receiver/output
::receiver/buffer]))
(defn sub!
[{:keys [::receiver/mult]} ch]
(a/tap mult ch))
(defmethod ig/halt-key! ::receiver
[_ {:keys [::receiver/buffer ::receiver/thread]}]
(some-> thread px/interrupt!)
(some-> buffer a/close!))
(def ^:private json-mapper
(json/mapper
{:encode-key-fn str/camel
:decode-key-fn (comp keyword str/kebab)}))
(defn- start-rcv-loop
[output endpoint]
(let [zctx (ZContext. 1)
socket (.. zctx (createSocket SocketType/SUB))]
(try
(.. socket (connect ^String endpoint))
(.. socket (subscribe ""))
(.. socket (setReceiveTimeOut 5000))
(loop []
(let [msg (.recv ^ZMQ$Socket socket)
msg (ex/ignoring (json/decode msg json-mapper))
msg (if (nil? msg) :empty msg)]
(when (a/>!! output msg)
(recur))))
(finally
(.close ^java.lang.AutoCloseable socket)
(.destroy ^ZContext zctx)))))
(s/def ::logger-name string?)
(s/def ::level string?)
(s/def ::thread string?)
(s/def ::time-millis integer?)
(s/def ::message string?)
(s/def ::context-map map?)
(s/def ::thrown map?)
(s/def ::log4j-event
(s/keys :req-un [::logger-name ::level ::thread ::time-millis ::message]
:opt-un [::context-map ::thrown]))
(defn- prepare
[event]
(if (s/valid? ::log4j-event event)
(merge {:message (:message event)
:created-at (dt/instant (:time-millis event))
:logger/name (:logger-name event)
:logger/level (str/lower (:level event))}
(when-let [trace (-> event :thrown :extended-stack-trace)]
{:trace trace})
(:context-map event))
(do
(l/warn :hint "invalid event" :event event)
nil)))

View File

@@ -12,20 +12,32 @@
[app.common.logging :as l]
[app.config :as cf]
[app.db :as-alias db]
[app.email :as-alias email]
[app.http :as-alias http]
[app.http.assets :as-alias http.assets]
[app.http.awsns :as http.awsns]
[app.http.client :as-alias http.client]
[app.http.session :as-alias http.session]
[app.http.debug :as-alias http.debug]
[app.http.session :as-alias session]
[app.http.session.tasks :as-alias session.tasks]
[app.http.websocket :as http.ws]
[app.loggers.audit.tasks :as-alias audit.tasks]
[app.loggers.webhooks :as-alias webhooks]
[app.loggers.zmq :as-alias lzmq]
[app.metrics :as-alias mtx]
[app.metrics.definition :as-alias mdef]
[app.msgbus :as-alias mbus]
[app.redis :as-alias rds]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc]
[app.srepl :as-alias srepl]
[app.storage :as-alias sto]
[app.storage.fs :as-alias sto.fs]
[app.storage.s3 :as-alias sto.s3]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[cuerdas.core :as str]
[integrant.core :as ig])
[integrant.core :as ig]
[promesa.exec :as px])
(:gen-class))
(def default-metrics
@@ -90,15 +102,15 @@
::mdef/labels ["name"]
::mdef/type :summary}
:rpc-climit-queue-size
{::mdef/name "penpot_rpc_climit_queue_size"
::mdef/help "Current number of queued submissions on the CLIMIT."
:rpc-climit-queue
{::mdef/name "penpot_rpc_climit_queue"
::mdef/help "Current number of queued submissions."
::mdef/labels ["name"]
::mdef/type :gauge}
:rpc-climit-concurrency
{::mdef/name "penpot_rpc_climit_concurrency"
::mdef/help "Current number of used concurrency capacity on the CLIMIT"
:rpc-climit-permits
{::mdef/name "penpot_rpc_climit_permits"
::mdef/help "Current number of available permits"
::mdef/labels ["name"]
::mdef/type :gauge}
@@ -152,22 +164,18 @@
(def system-config
{::db/pool
{:uri (cf/get :database-uri)
:username (cf/get :database-username)
:password (cf/get :database-password)
:read-only (cf/get :database-readonly false)
:metrics (ig/ref ::mtx/metrics)
: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 60)}
{::db/uri (cf/get :database-uri)
::db/username (cf/get :database-username)
::db/password (cf/get :database-password)
::db/read-only? (cf/get :database-readonly false)
::db/min-size (cf/get :database-min-pool-size 0)
::db/max-size (cf/get :database-max-pool-size 60)
::mtx/metrics (ig/ref ::mtx/metrics)}
;; Default thread pool for IO operations
::wrk/executor
{::wrk/parallelism (cf/get :default-executor-parallelism 100)}
::wrk/scheduled-executor
{::wrk/parallelism (cf/get :scheduled-executor-parallelism 20)}
{::wrk/parallelism (cf/get :default-executor-parallelism
(+ 3 (* (px/get-available-processors) 3)))}
::wrk/monitor
{::mtx/metrics (ig/ref ::mtx/metrics)
@@ -175,62 +183,56 @@
::wrk/executor (ig/ref ::wrk/executor)}
:app.migrations/migrations
{}
{::db/pool (ig/ref ::db/pool)}
::mtx/metrics
{:default default-metrics}
:app.migrations/all
{:main (ig/ref :app.migrations/migrations)}
::mtx/routes
{::mtx/metrics (ig/ref ::mtx/metrics)}
::rds/redis
{::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)}
{::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)}
:app.msgbus/msgbus
{:backend (cf/get :msgbus-backend :redis)
:executor (ig/ref ::wrk/executor)
:redis (ig/ref ::rds/redis)}
::mbus/msgbus
{::wrk/executor (ig/ref ::wrk/executor)
::rds/redis (ig/ref ::rds/redis)}
:app.storage.tmp/cleaner
{::wrk/executor (ig/ref ::wrk/executor)
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
::sto/gc-deleted-task
{:pool (ig/ref ::db/pool)
:storage (ig/ref ::sto/storage)
:executor (ig/ref ::wrk/executor)}
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
::sto/gc-touched-task
{:pool (ig/ref ::db/pool)}
{::db/pool (ig/ref ::db/pool)}
::http.client/client
{::wrk/executor (ig/ref ::wrk/executor)}
:app.http.session/manager
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)}
::session/manager
{::db/pool (ig/ref ::db/pool)}
:app.http.session/gc-task
{:pool (ig/ref ::db/pool)
:max-age (cf/get :auth-token-cookie-max-age)}
::session.tasks/gc
{::db/pool (ig/ref ::db/pool)}
:app.http.awsns/handler
::http.awsns/routes
{::props (ig/ref :app.setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)
::wrk/executor (ig/ref ::wrk/executor)}
:app.http/server
{:port (cf/get :http-server-port)
:host (cf/get :http-server-host)
:router (ig/ref :app.http/router)
:metrics (ig/ref ::mtx/metrics)
:executor (ig/ref ::wrk/executor)
:io-threads (cf/get :http-server-io-threads)
:max-body-size (cf/get :http-server-max-body-size)
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
::http/server
{::http/port (cf/get :http-server-port)
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::wrk/executor (ig/ref ::wrk/executor)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
::ldap/provider
{:host (cf/get :ldap-host)
@@ -259,85 +261,71 @@
{::http.client/client (ig/ref ::http.client/client)}
::oidc/routes
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::props (ig/ref :app.setup/props)
::wrk/executor (ig/ref ::wrk/executor)
::oidc/providers {:google (ig/ref ::oidc.providers/google)
:github (ig/ref ::oidc.providers/github)
:gitlab (ig/ref ::oidc.providers/gitlab)
:oidc (ig/ref ::oidc.providers/generic)}
::http.session/session (ig/ref :app.http.session/manager)}
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::props (ig/ref :app.setup/props)
::oidc/providers {:google (ig/ref ::oidc.providers/google)
:github (ig/ref ::oidc.providers/github)
:gitlab (ig/ref ::oidc.providers/gitlab)
:oidc (ig/ref ::oidc.providers/generic)}
::session/manager (ig/ref ::session/manager)}
;; 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/manager)
:awsns-handler (ig/ref :app.http.awsns/handler)
:debug-routes (ig/ref :app.http.debug/routes)
:oidc-routes (ig/ref ::oidc/routes)
:ws (ig/ref :app.http.websocket/handler)
:metrics (ig/ref ::mtx/metrics)
:public-uri (cf/get :public-uri)
:storage (ig/ref ::sto/storage)
:rpc-routes (ig/ref :app.rpc/routes)
:doc-routes (ig/ref :app.rpc.doc/routes)
:executor (ig/ref ::wrk/executor)}
{::session/manager (ig/ref ::session/manager)
::db/pool (ig/ref ::db/pool)
::rpc/routes (ig/ref ::rpc/routes)
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
::props (ig/ref :app.setup/props)
::mtx/routes (ig/ref ::mtx/routes)
::oidc/routes (ig/ref ::oidc/routes)
::http.debug/routes (ig/ref ::http.debug/routes)
::http.assets/routes (ig/ref ::http.assets/routes)
::http.ws/routes (ig/ref ::http.ws/routes)
::http.awsns/routes (ig/ref ::http.awsns/routes)}
:app.http.debug/routes
{:pool (ig/ref ::db/pool)
:executor (ig/ref ::wrk/executor)
:storage (ig/ref ::sto/storage)
:session (ig/ref :app.http.session/manager)
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager)
::sto/storage (ig/ref ::sto/storage)}
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::sto/storage (ig/ref ::sto/storage)}
::http.ws/routes
{::db/pool (ig/ref ::db/pool)
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus)
::session/manager (ig/ref ::session/manager)}
:app.http.websocket/handler
{:pool (ig/ref ::db/pool)
:metrics (ig/ref ::mtx/metrics)
:msgbus (ig/ref :app.msgbus/msgbus)}
:app.http.assets/handlers
{:metrics (ig/ref ::mtx/metrics)
:assets-path (cf/get :assets-path)
:storage (ig/ref ::sto/storage)
:executor (ig/ref ::wrk/executor)
:cache-max-age (dt/duration {:hours 24})
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
:app.http.feedback/handler
{:pool (ig/ref ::db/pool)
:executor (ig/ref ::wrk/executor)}
:app.http.assets/routes
{::http.assets/path (cf/get :assets-path)
::http.assets/cache-max-age (dt/duration {:hours 24})
::http.assets/cache-max-agesignature-max-age (dt/duration {:hours 24 :minutes 5})
::sto/storage (ig/ref ::sto/storage)
::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/climit
{:metrics (ig/ref ::mtx/metrics)
:executor (ig/ref ::wrk/executor)}
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/rlimit
{:executor (ig/ref ::wrk/executor)
:scheduled-executor (ig/ref ::wrk/scheduled-executor)}
{::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/methods
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
::sto/storage (ig/ref ::sto/storage)
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus)
::rds/redis (ig/ref ::rds/redis)
::rpc/climit (ig/ref ::rpc/climit)
::rpc/rlimit (ig/ref ::rpc/rlimit)
::props (ig/ref :app.setup/props)
:pool (ig/ref ::db/pool)
:session (ig/ref :app.http.session/manager)
:sprops (ig/ref :app.setup/props)
:metrics (ig/ref ::mtx/metrics)
:storage (ig/ref ::sto/storage)
:msgbus (ig/ref :app.msgbus/msgbus)
:public-uri (cf/get :public-uri)
:redis (ig/ref ::rds/redis)
:http-client (ig/ref ::http.client/client)
:climit (ig/ref :app.rpc/climit)
:rlimit (ig/ref :app.rpc/rlimit)
:executor (ig/ref ::wrk/executor)
:templates (ig/ref :app.setup/builtin-templates)
}
@@ -345,12 +333,16 @@
{:methods (ig/ref :app.rpc/methods)}
:app.rpc/routes
{:methods (ig/ref :app.rpc/methods)}
{::rpc/methods (ig/ref :app.rpc/methods)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager)
::props (ig/ref :app.setup/props)}
::wrk/registry
{:metrics (ig/ref ::mtx/metrics)
:tasks
{:sendmail (ig/ref :app.emails/handler)
{::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/tasks
{:sendmail (ig/ref ::email/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)
@@ -358,7 +350,7 @@
:storage-gc-touched (ig/ref ::sto/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)
:session-gc (ig/ref ::session.tasks/gc)
:audit-log-archive (ig/ref ::audit.tasks/archive)
:audit-log-gc (ig/ref ::audit.tasks/gc)
@@ -367,34 +359,33 @@
:run-webhook
(ig/ref ::webhooks/run-webhook-handler)}}
::email/sendmail
{::email/host (cf/get :smtp-host)
::email/port (cf/get :smtp-port)
::email/ssl (cf/get :smtp-ssl)
::email/tls (cf/get :smtp-tls)
::email/username (cf/get :smtp-username)
::email/password (cf/get :smtp-password)
::email/default-reply-to (cf/get :smtp-default-reply-to)
::email/default-from (cf/get :smtp-default-from)}
: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)
: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 ::mtx/metrics)}
::email/handler
{::email/sendmail (ig/ref ::email/sendmail)
::mtx/metrics (ig/ref ::mtx/metrics)}
:app.tasks.tasks-gc/handler
{:pool (ig/ref ::db/pool)
:max-age cf/deletion-delay}
{::db/pool (ig/ref ::db/pool)}
:app.tasks.objects-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-gc/handler
{:pool (ig/ref ::db/pool)}
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-xlog-gc/handler
{:pool (ig/ref ::db/pool)}
{::db/pool (ig/ref ::db/pool)}
:app.tasks.telemetry/handler
{::db/pool (ig/ref ::db/pool)
@@ -402,22 +393,23 @@
::props (ig/ref :app.setup/props)}
[::srepl/urepl ::srepl/server]
{:port (cf/get :urepl-port 6062)
:host (cf/get :urepl-host "localhost")}
{::srepl/port (cf/get :urepl-port 6062)
::srepl/host (cf/get :urepl-host "localhost")}
[::srepl/prepl ::srepl/server]
{:port (cf/get :prepl-port 6063)
:host (cf/get :prepl-host "localhost")}
{::srepl/port (cf/get :prepl-port 6063)
::srepl/host (cf/get :prepl-host "localhost")}
:app.setup/builtin-templates
{::http.client/client (ig/ref ::http.client/client)}
:app.setup/props
{:pool (ig/ref ::db/pool)
:key (cf/get :secret-key)}
{::db/pool (ig/ref ::db/pool)
::key (cf/get :secret-key)
::lzmq/receiver
{}
;; NOTE: this dependency is only necessary for proper initialization ordering, props
;; module requires the migrations to run before initialize.
::migrations (ig/ref :app.migrations/migrations)}
::audit.tasks/archive
{::props (ig/ref :app.setup/props)
@@ -435,45 +427,33 @@
{::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)}
:app.loggers.loki/reporter
{::lzmq/receiver (ig/ref ::lzmq/receiver)
::http.client/client (ig/ref ::http.client/client)}
:app.loggers.mattermost/reporter
{::lzmq/receiver (ig/ref ::lzmq/receiver)
::http.client/client (ig/ref ::http.client/client)}
{::http.client/client (ig/ref ::http.client/client)}
:app.loggers.database/reporter
{::lzmq/receiver (ig/ref :app.loggers.zmq/receiver)
::db/pool (ig/ref ::db/pool)}
{::db/pool (ig/ref ::db/pool)}
::sto/storage
{:pool (ig/ref ::db/pool)
:executor (ig/ref ::wrk/executor)
:backends
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::sto/backends
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
;; keep this for backward compatibility
:s3 (ig/ref [::assets :app.storage.s3/backend])
:fs (ig/ref [::assets :app.storage.fs/backend])}}
:assets-fs (ig/ref [::assets :app.storage.fs/backend])}}
[::assets :app.storage.s3/backend]
{:region (cf/get :storage-assets-s3-region)
:endpoint (cf/get :storage-assets-s3-endpoint)
:bucket (cf/get :storage-assets-s3-bucket)
:executor (ig/ref ::wrk/executor)}
{::sto.s3/region (cf/get :storage-assets-s3-region)
::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint)
::sto.s3/bucket (cf/get :storage-assets-s3-bucket)
::wrk/executor (ig/ref ::wrk/executor)}
[::assets :app.storage.fs/backend]
{:directory (cf/get :storage-assets-fs-directory)}
{::sto.fs/directory (cf/get :storage-assets-fs-directory)}
})
(def worker-config
{::wrk/cron
{::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)
::wrk/registry (ig/ref ::wrk/registry)
{::wrk/registry (ig/ref ::wrk/registry)
::db/pool (ig/ref ::db/pool)
::wrk/entries
[{:cron #app/cron "0 0 * * * ?" ;; hourly

View File

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

View File

@@ -87,13 +87,26 @@
::definitions definitions
::registry registry}))
(defn- handler
[registry _ respond _]
[registry _]
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
writer (StringWriter.)]
(TextFormat/write004 writer samples)
(respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)})))
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)}))
(s/def ::routes vector?)
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::metrics]))
(defmethod ig/init-key ::routes
[_ {:keys [::metrics]}]
(let [registry (::registry metrics)]
["/metrics" {:handler (partial handler registry)
:allowed-methods #{:get}}]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation

View File

@@ -6,8 +6,12 @@
(ns app.migrations
(:require
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.db :as db]
[app.migrations.clj.migration-0023 :as mg0023]
[app.util.migrations :as mg]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(def migrations
@@ -302,7 +306,38 @@
{:name "0098-add-quotes-table"
:fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")}
])
{:name "0099-add-access-token-table"
:fn (mg/resource "app/migrations/sql/0099-add-access-token-table.sql")}
{:name "0100-mod-profile-indexes"
:fn (mg/resource "app/migrations/sql/0100-mod-profile-indexes.sql")}
(defmethod ig/init-key ::migrations [_ _] migrations)
{:name "0101-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0101-mod-server-error-report-table.sql")}
{:name "0102-mod-access-token-table"
:fn (mg/resource "app/migrations/sql/0102-mod-access-token-table.sql")}
{:name "0103-mod-file-object-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0103-mod-file-object-thumbnail-table.sql")}
{:name "0104-mod-file-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0104-mod-file-thumbnail-table.sql")}
])
(defn apply-migrations!
[pool name migrations]
(dm/with-open [conn (db/open pool)]
(mg/setup! conn)
(mg/migrate! conn {:name name :steps migrations})))
(defmethod ig/pre-init-spec ::migrations
[_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::migrations
[module {:keys [::db/pool]}]
(when-not (db/read-only? pool)
(l/info :hint "running migrations" :module module)
(some->> (seq migrations) (apply-migrations! pool "main"))))

View File

@@ -0,0 +1,19 @@
DROP TABLE IF EXISTS access_token;
CREATE TABLE access_token (
id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
name text NOT NULL,
token text NOT NULL,
perms text[] NULL
);
ALTER TABLE access_token
ALTER COLUMN name SET STORAGE external,
ALTER COLUMN token SET STORAGE external,
ALTER COLUMN perms SET STORAGE external;
CREATE INDEX access_token__profile_id__idx ON access_token(profile_id);

View File

@@ -0,0 +1,31 @@
ALTER TABLE profile
ADD COLUMN default_project_id uuid NULL REFERENCES project(id) ON DELETE SET NULL DEFERRABLE,
ADD COLUMN default_team_id uuid NULL REFERENCES team(id) ON DELETE SET NULL DEFERRABLE;
CREATE INDEX profile__default_project__idx ON profile(default_project_id);
CREATE INDEX profile__default_team__idx ON profile(default_team_id);
with profiles as (
select p.id,
tpr.team_id as default_team_id,
ppr.project_id as default_project_id
from profile as p
join team_profile_rel as tpr
on (tpr.profile_id = p.id and
tpr.is_owner is true)
join project_profile_rel as ppr
on (ppr.profile_id = p.id and
ppr.is_owner is true)
join project as pj
on (pj.id = ppr.project_id)
join team as tm
on (tm.id = tpr.team_id)
where pj.is_default is true
and tm.is_default is true
and pj.team_id = tm.id
)
update profile
set default_team_id = p.default_team_id,
default_project_id = p.default_project_id
from profiles as p
where profile.id = p.id;

View File

@@ -0,0 +1,2 @@
ALTER TABLE server_error_report
ADD COLUMN version integer DEFAULT 1;

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,17 +8,21 @@
"The msgbus abstraction implemented using redis as underlying backend."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.metrics :as mtx]
[app.redis.script :as-alias rscript]
[app.util.cache :as cache]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.core :as c]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.core :as p])
[promesa.core :as p]
[promesa.exec :as px])
(:import
clojure.lang.IDeref
clojure.lang.MapEntry
@@ -87,7 +91,7 @@
(s/def ::connect? ::us/boolean)
(s/def ::io-threads ::us/integer)
(s/def ::worker-threads ::us/integer)
(s/def ::cache #(instance? clojure.lang.Atom %))
(s/def ::cache some?)
(s/def ::redis
(s/keys :req [::resources
@@ -99,11 +103,11 @@
(defmethod ig/prep-key ::redis
[_ cfg]
(let [runtime (Runtime/getRuntime)
cpus (.availableProcessors ^Runtime runtime)]
(let [cpus (px/get-available-processors)
threads (max 1 (int (* cpus 0.2)))]
(merge {::timeout (dt/duration "10s")
::io-threads (max 3 cpus)
::worker-threads (max 3 cpus)}
::io-threads (max 3 threads)
::worker-threads (max 3 threads)}
(d/without-nils cfg))))
(defmethod ig/pre-init-spec ::redis [_]
@@ -129,6 +133,15 @@
(def string-codec
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
(defn- create-cache
[{:keys [::wrk/executor] :as cfg}]
(letfn [(on-remove [key val cause]
(l/trace :hint "evict connection (cache)" :key key :reason cause)
(some-> val d/close!))]
(cache/create :executor executor
:on-remove on-remove
:keepalive "5m")))
(defn- initialize-resources
"Initialize redis connection resources"
[{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}]
@@ -145,19 +158,21 @@
(timer ^Timer timer)
(build))
redis-uri (RedisURI/create ^String uri)]
redis-uri (RedisURI/create ^String uri)
cfg (-> cfg
(assoc ::resources resources)
(assoc ::timer timer)
(assoc ::redis-uri redis-uri))]
(-> cfg
(assoc ::resources resources)
(assoc ::timer timer)
(assoc ::cache (atom {}))
(assoc ::redis-uri redis-uri))))
(assoc cfg ::cache (create-cache cfg))))
(defn- shutdown-resources
[{:keys [::resources ::cache ::timer]}]
(run! d/close! (vals @cache))
(cache/invalidate-all! cache)
(when resources
(.shutdown ^ClientResources resources))
(when timer
(.stop ^Timer timer)))
@@ -173,6 +188,7 @@
:default (.connect ^RedisClient client ^RedisCodec codec)
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
(l/trc :hint "connect" :hid (hash client))
(.setTimeout ^StatefulConnection conn ^Duration timeout)
(reify
IDeref
@@ -180,8 +196,9 @@
AutoCloseable
(close [_]
(.close ^StatefulConnection conn)
(.shutdown ^RedisClient client)))))
(ex/ignoring (.close ^StatefulConnection conn))
(ex/ignoring (.shutdown ^RedisClient client))
(l/trc :hint "disconnect" :hid (hash client))))))
(defn connect
[state & {:as opts}]
@@ -193,19 +210,14 @@
(defn get-or-connect
[{:keys [::cache] :as state} key options]
(-> state
(assoc ::connection
(or (get @cache key)
(-> (swap! cache (fn [cache]
(when-let [prev (get cache key)]
(d/close! prev))
(assoc cache key (connect* state options))))
(get key))))
(dissoc ::cache)))
(us/assert! ::redis state)
(let [connection (cache/get cache key (fn [_] (connect* state options)))]
(-> state
(dissoc ::cache)
(assoc ::connection connection))))
(defn add-listener!
[{:keys [::connection] :as conn} listener]
(us/assert! ::connection-holder conn)
(us/assert! ::pubsub-connection connection)
(us/assert! ::pubsub-listener listener)
(.addListener ^StatefulRedisPubSubConnection @connection
@@ -213,10 +225,9 @@
conn)
(defn publish!
[{:keys [::connection] :as conn} topic message]
[{:keys [::connection]} topic message]
(us/assert! ::us/string topic)
(us/assert! ::us/bytes message)
(us/assert! ::connection-holder conn)
(us/assert! ::default-connection connection)
(let [pcomm (.async ^StatefulRedisConnection @connection)]
@@ -224,8 +235,7 @@
(defn subscribe!
"Blocking operation, intended to be used on a thread/agent thread."
[{:keys [::connection] :as conn} & topics]
(us/assert! ::connection-holder conn)
[{:keys [::connection]} & topics]
(us/assert! ::pubsub-connection connection)
(try
(let [topics (into-array String (map str topics))
@@ -236,8 +246,7 @@
(defn unsubscribe!
"Blocking operation, intended to be used on a thread/agent thread."
[{:keys [::connection] :as conn} & topics]
(us/assert! ::connection-holder conn)
[{:keys [::connection]} & topics]
(us/assert! ::pubsub-connection connection)
(try
(let [topics (into-array String (map str topics))
@@ -247,8 +256,8 @@
(throw (InterruptedException. (ex-message cause))))))
(defn rpush!
[{:keys [::connection] :as conn} key payload]
(us/assert! ::connection-holder conn)
[{:keys [::connection]} key payload]
(us/assert! ::default-connection connection)
(us/assert! (or (and (vector? payload)
(every? bytes? payload))
(bytes? payload)))
@@ -270,8 +279,8 @@
(throw (InterruptedException. (ex-message cause))))))
(defn blpop!
[{:keys [::connection] :as conn} timeout & keys]
(us/assert! ::connection-holder conn)
[{:keys [::connection]} timeout & keys]
(us/assert! ::default-connection connection)
(try
(let [keys (into-array Object (map str keys))
cmd (.sync ^StatefulRedisConnection @connection)
@@ -286,8 +295,7 @@
(throw (InterruptedException. (ex-message cause))))))
(defn open?
[{:keys [::connection] :as conn}]
(us/assert! ::connection-holder conn)
[{:keys [::connection]}]
(us/assert! ::pubsub-connection connection)
(.isOpen ^StatefulConnection @connection))
@@ -335,7 +343,7 @@
(defn eval!
[{:keys [::mtx/metrics ::connection] :as state} script]
(us/assert! ::redis state)
(us/assert! ::connection-holder state)
(us/assert! ::default-connection connection)
(us/assert! ::rscript/script script)
(let [cmd (.async ^StatefulRedisConnection @connection)
@@ -348,7 +356,7 @@
(do
(l/error :hint "no script found" :name sname :cause cause)
(->> (load-script)
(p/mapcat eval-script)))
(p/mcat eval-script)))
(if-let [on-error (::rscript/on-error script)]
(on-error cause)
(p/rejected cause))))
@@ -379,15 +387,16 @@
(load-script []
(l/trace :hint "load script" :name sname)
(->> (.scriptLoad ^RedisScriptingAsyncCommands cmd
^String (read-script))
(p/map (fn [sha]
(swap! scripts-cache assoc sname sha)
sha))))]
^String (read-script))
(p/fmap (fn [sha]
(swap! scripts-cache assoc sname sha)
sha))))]
(if-let [sha (get @scripts-cache sname)]
(eval-script sha)
(->> (load-script)
(p/mapcat eval-script))))))
(p/await!
(if-let [sha (get @scripts-cache sname)]
(eval-script sha)
(->> (load-script)
(p/mapcat eval-script)))))))
(defn timeout-exception?
[cause]

View File

@@ -7,17 +7,19 @@
(ns app.rpc
(:require
[app.auth.ldap :as-alias ldap]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
[app.http.access-token :as actoken]
[app.http.client :as-alias http.client]
[app.http.session :as-alias http.session]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.loggers.webhooks :as-alias webhooks]
[app.main :as-alias main]
[app.metrics :as mtx]
[app.msgbus :as-alias mbus]
[app.rpc.climit :as climit]
@@ -32,7 +34,6 @@
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq]
[yetti.response :as yrs]))
@@ -44,12 +45,10 @@
(defn- handle-response-transformation
[response request mdata]
(let [transform-fn (reduce (fn [res-fn transform-fn]
(fn [request response]
(p/then (res-fn request response) #(transform-fn request %))))
(constantly response)
(::response-transform-fns mdata))]
(transform-fn request response)))
(reduce (fn [response transform-fn]
(transform-fn request response))
response
(::response-transform-fns mdata)))
(defn- handle-before-comple-hook
[response mdata]
@@ -60,306 +59,209 @@
(defn- handle-response
[request result]
(if (fn? result)
(p/wrap (result request))
(result request)
(let [mdata (meta result)]
(p/-> (yrs/response {:status (::http/status mdata 200)
:headers (::http/headers mdata {})
:body (rph/unwrap result)})
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata)))))
(-> {::yrs/status (::http/status mdata 200)
::yrs/headers (::http/headers mdata {})
::yrs/body (rph/unwrap result)}
(handle-response-transformation request mdata)
(handle-before-comple-hook mdata)))))
(defn- rpc-query-handler
"Ring handler that dispatches query requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
(let [type (keyword (:type path-params))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request))
data (if profile-id
(-> data
(assoc :profile-id profile-id)
(assoc ::profile-id profile-id)
(assoc ::session-id session-id))
(dissoc data :profile-id ::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))))))))
(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 path-params params] :as request} respond raise]
(let [type (keyword (:type path-params))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request))
data (if profile-id
(-> data
(assoc :profile-id profile-id)
(assoc ::profile-id profile-id)
(assoc ::session-id session-id))
(dissoc data :profile-id ::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))))))))
(defn- rpc-command-handler
(defn- rpc-handler
"Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id path-params params] :as request} respond raise]
(let [cmd (keyword (:type path-params))
etag (yrq/get-header request "if-none-match")
[methods {:keys [params path-params] :as request}]
(let [type (keyword (:type path-params))
etag (yrq/get-header request "if-none-match")
profile-id (or (::session/profile-id request)
(::actoken/profile-id request))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::http/request request)
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)
(-> (assoc ::profile-id profile-id)
(assoc ::session-id session-id))))
data (-> params
(assoc ::request-at (dt/now))
(assoc ::session/id (::session/id request))
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)
(assoc ::profile-id profile-id)))
data (vary-meta data assoc ::http/request request)
method (get methods type default-handler)]
method (get methods cmd default-handler)]
(binding [cond/*enabled* true]
(-> (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)))))))))
(let [response (method data)]
(handle-response request response)))))
(defn- wrap-metrics
"Wrap service method with metrics measurement."
[{:keys [metrics ::metrics-id]} f mdata]
[{:keys [::mtx/metrics ::metrics-id]} f mdata]
(let [labels (into-array String [(::sv/name mdata)])]
(fn [cfg params]
(let [tp (dt/tpoint)]
(p/finally
(try
(f cfg params)
(fn [_ _]
(finally
(mtx/run! metrics
: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 [executor] :as cfg} f mdata]
(with-meta
(fn [cfg params]
(->> (px/submit! executor (px/wrap-bindings #(f cfg params)))
(p/mapcat p/wrap)
(p/map rph/wrap)))
mdata))
(defn- wrap-authentication
[_ f mdata]
(fn [cfg params]
(let [profile-id (::profile-id params)]
(if (and (::auth mdata true) (not (uuid? profile-id)))
(ex/raise :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint")
(f cfg params)))))
(defn- wrap-audit
[cfg f mdata]
[_ f mdata]
(if (or (contains? cf/flags :webhooks)
(contains? cf/flags :audit-log))
(letfn [(handle-audit [params result]
(let [resultm (meta result)
request (::http/request params)
profile-id (or (::audit/profile-id resultm)
(:profile-id result)
(if (= (::type cfg) "command")
(::profile-id params)
(:profile-id params))
uuid/zero)
props (-> (or (::audit/replace-props resultm)
(-> params
(merge (::audit/props resultm))
(dissoc :profile-id)
(dissoc :type)))
(audit/clean-props))
event {:type (or (::audit/type resultm)
(::type cfg))
:name (or (::audit/name resultm)
(::sv/name mdata))
:profile-id profile-id
:ip-addr (some-> request audit/parse-client-ip)
:props props
;; NOTE: for batch-key lookup we need the params as-is
;; because the rpc api does not need to know the
;; audit/webhook specific object layout.
::params (dissoc params ::http/request)
::webhooks/batch-key
(or (::webhooks/batch-key mdata)
(::webhooks/batch-key resultm))
::webhooks/batch-timeout
(or (::webhooks/batch-timeout mdata)
(::webhooks/batch-timeout resultm))
::webhooks/event?
(or (::webhooks/event? mdata)
(::webhooks/event? resultm)
false)}]
(audit/submit! cfg event)))
(handle-request [cfg params]
(->> (f cfg params)
(p/fnly (fn [result cause]
(when-not cause
(handle-audit params result))))))]
(if-not (::audit/skip mdata)
(with-meta handle-request mdata)
f))
(if-not (::audit/skip mdata)
(fn [cfg params]
(let [result (f cfg params)]
(->> (audit/prepare-event cfg mdata params result)
(audit/submit! cfg))
result))
f)
f))
(defn- wrap-spec-conform
[_ f mdata]
;; NOTE: skip spec conform operation on rpc methods that already
;; uses malli validation mechanism.
(if (contains? mdata ::sm/params)
f
(if-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
(fn [cfg params]
(f cfg (us/conform spec params)))
f)))
(defn- wrap-params-validation
[_ f mdata]
(if-let [schema (::sm/params mdata)]
(let [schema (sm/schema schema)
valid? (sm/validator schema)
explain (sm/explainer schema)
decode (sm/decoder schema sm/default-transformer)]
(fn [cfg params]
(let [params (decode params)]
(if (valid? params)
(f cfg params)
(ex/raise :type :validation
:code :params-validation
::sm/explain (explain params))))))
f))
(defn- wrap-output-validation
[_ f mdata]
(if (contains? cf/flags :rpc-output-validation)
(or (when-let [schema (::sm/result mdata)]
(let [schema (sm/schema schema)
valid? (sm/validator schema)
explain (sm/explainer schema)]
(fn [cfg params]
(let [response (f cfg params)]
(when (map? response)
(when-not (valid? response)
(ex/raise :type :validation
:code :data-validation
::sm/explain (explain response))))
response))))
f)
f))
(defn- wrap-all
[cfg f mdata]
(as-> f $
(wrap-metrics cfg $ mdata)
(cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(climit/wrap cfg $ mdata)
(rlimit/wrap cfg $ mdata)
(wrap-audit cfg $ mdata)
(wrap-spec-conform cfg $ mdata)
(wrap-output-validation cfg $ mdata)
(wrap-params-validation cfg $ mdata)
(wrap-authentication cfg $ mdata)))
(defn- wrap
[cfg f mdata]
(let [f (as-> f $
(wrap-dispatch cfg $ mdata)
(cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata)
(wrap-metrics cfg $ mdata)
(climit/wrap cfg $ mdata)
(rlimit/wrap cfg $ mdata)
(wrap-audit cfg $ mdata))
spec (or (::sv/spec mdata) (s/spec any?))
auth? (::auth mdata true)]
(l/debug :hint "register method" :name (::sv/name mdata))
(with-meta
(fn [params]
;; Raise authentication error when rpc method requires auth but
;; no profile-id is found in the request.
(let [profile-id (if (= "command" (::type cfg))
(::profile-id params)
(:profile-id params))]
(p/do!
(if (and auth? (not (uuid? profile-id)))
(ex/raise :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint")
(let [params (us/conform spec params)]
(f cfg params))))))
mdata)))
(l/debug :hint "register method" :name (::sv/name mdata))
(let [f (wrap-all cfg f mdata)]
(partial f cfg)))
(defn- process-method
[cfg vfn]
(let [mdata (meta vfn)]
[(keyword (::sv/name mdata))
(wrap cfg vfn mdata)]))
(defn- resolve-query-methods
[cfg]
(let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)]
(->> (sv/scan-ns 'app.rpc.queries.projects
'app.rpc.queries.files
'app.rpc.queries.teams
'app.rpc.queries.profile
'app.rpc.queries.viewer
'app.rpc.queries.fonts)
(map (partial process-method cfg))
(into {}))))
(defn- resolve-mutation-methods
[cfg]
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
(->> (sv/scan-ns 'app.rpc.mutations.media
'app.rpc.mutations.profile
'app.rpc.mutations.files
'app.rpc.mutations.projects
'app.rpc.mutations.teams
'app.rpc.mutations.fonts
'app.rpc.mutations.share-link)
(map (partial process-method cfg))
(into {}))))
[cfg [vfn mdata]]
[(keyword (::sv/name mdata)) [mdata (wrap cfg vfn mdata)]])
(defn- resolve-command-methods
[cfg]
(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.search
'app.rpc.commands.media
'app.rpc.commands.teams
'app.rpc.commands.auth
'app.rpc.commands.ldap
'app.rpc.commands.demo
'app.rpc.commands.webhooks
'app.rpc.commands.audit
'app.rpc.commands.files
'app.rpc.commands.files.update
'app.rpc.commands.files.create
'app.rpc.commands.files.temp)
(->> (sv/scan-ns
'app.rpc.commands.access-token
'app.rpc.commands.audit
'app.rpc.commands.auth
'app.rpc.commands.feedback
'app.rpc.commands.fonts
'app.rpc.commands.binfile
'app.rpc.commands.comments
'app.rpc.commands.demo
'app.rpc.commands.files
'app.rpc.commands.files-create
'app.rpc.commands.files-share
'app.rpc.commands.files-temp
'app.rpc.commands.files-update
'app.rpc.commands.files-thumbnails
'app.rpc.commands.ldap
'app.rpc.commands.management
'app.rpc.commands.media
'app.rpc.commands.profile
'app.rpc.commands.projects
'app.rpc.commands.search
'app.rpc.commands.teams
'app.rpc.commands.verify-token
'app.rpc.commands.viewer
'app.rpc.commands.webhooks)
(map (partial process-method cfg))
(into {}))))
(s/def ::ldap (s/nilable map?))
(s/def ::msgbus ::mbus/msgbus)
(s/def ::climit (s/nilable ::climit/climit))
(s/def ::rlimit (s/nilable ::rlimit/rlimit))
(s/def ::public-uri ::us/not-empty-string)
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::methods [_]
(s/keys :req [::http.client/client
(s/keys :req [::session/manager
::http.client/client
::db/pool
::mbus/msgbus
::ldap/provider
::sto/storage
::mtx/metrics
::main/props
::wrk/executor]
:req-un [::sto/storage
::http.session/session
::sprops
::public-uri
::msgbus
::rlimit
::climit
::wrk/executor
::mtx/metrics
::db/pool]))
:opt [::climit
::rlimit]
:req-un [::db/pool]))
(defmethod ig/init-key ::methods
[_ cfg]
{:mutations (resolve-mutation-methods cfg)
:queries (resolve-query-methods cfg)
:commands (resolve-command-methods cfg)})
(s/def ::mutations
(s/map-of keyword? fn?))
(s/def ::queries
(s/map-of keyword? fn?))
(s/def ::commands
(s/map-of keyword? fn?))
(let [cfg (d/without-nils cfg)]
(resolve-command-methods cfg)))
(s/def ::methods
(s/keys :req-un [::mutations
::queries
::commands]))
(s/map-of keyword? (s/tuple map? fn?)))
(s/def ::routes vector?)
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req-un [::methods]))
(s/keys :req [::methods
::db/pool
::main/props
::wrk/executor
::session/manager]))
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
[["/rpc"
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
:allowed-methods #{:post}}]]])
[_ {:keys [::methods] :as cfg}]
(let [methods (update-vals methods peek)]
[["/rpc" {:middleware [[session/authz cfg]
[actoken/authz cfg]]}
["/command/:type" {:handler (partial rpc-handler methods)}]]]))

View File

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

View File

@@ -0,0 +1,94 @@
;; 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.access-token
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias doc]
[app.rpc.quotes :as quotes]
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
(defn- decode-row
[row]
(dissoc row :perms))
(defn create-access-token
[{:keys [::db/conn ::main/props]} profile-id name expiration]
(let [created-at (dt/now)
token-id (uuid/next)
token (tokens/generate props {:iss "access-token"
:tid token-id
:iat created-at})
expires-at (some-> expiration dt/in-future)]
(db/insert! conn :access-token
{:id token-id
:name name
:token token
:profile-id profile-id
:created-at created-at
:updated-at created-at
:expires-at expires-at
:perms (db/create-array conn "text" [])})))
(defn repl-create-access-token
[{:keys [::db/pool] :as system} profile-id name expiration]
(db/with-atomic [conn pool]
(let [props (:app.setup/props system)]
(create-access-token {::db/conn conn ::main/props props}
profile-id
name
expiration))))
(s/def ::name ::us/not-empty-string)
(s/def ::expiration ::dt/duration)
(s/def ::create-access-token
(s/keys :req [::rpc/profile-id]
:req-un [::name]
:opt-un [::expiration]))
(sv/defmethod ::create-access-token
{::doc/added "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg ::db/conn conn)]
(quotes/check-quote! conn
{::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(-> (create-access-token cfg profile-id name expiration)
(decode-row)))))
(s/def ::delete-access-token
(s/keys :req [::rpc/profile-id]
:req-un [::us/id]))
(sv/defmethod ::delete-access-token
{::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
(db/delete! pool :access-token {:id id :profile-id profile-id})
nil)
(s/def ::get-access-tokens
(s/keys :req [::rpc/profile-id]))
(sv/defmethod ::get-access-tokens
{::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
(->> (db/query pool :access-token
{:profile-id profile-id}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:id :name :perms :created-at :updated-at :expires-at]})
(mapv decode-row)))

View File

@@ -21,10 +21,7 @@
[app.rpc.helpers :as rph]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]))
[clojure.spec.alpha :as s]))
(defn- event->row [event]
[(uuid/next)
@@ -42,8 +39,9 @@
:profile-id :ip-addr :props :context])
(defn- handle-events
[{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request] :as params}]
(let [ip-addr (audit/parse-client-ip request)
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request)
ip-addr (audit/parse-client-ip request)
xform (comp
(map #(assoc % :profile-id profile-id))
(map #(assoc % :ip-addr ip-addr))
@@ -71,17 +69,22 @@
:req-un [::events]))
(sv/defmethod ::push-audit-events
{::climit/queue :push-audit-events
{::climit/id :submit-audit-events-by-profile
::climit/key-fn ::rpc/profile-id
::audit/skip true
::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} params]
[{:keys [::db/pool] :as cfg} params]
(if (or (db/read-only? pool)
(not (contains? cf/flags :audit-log)))
(do
(l/warn :hint "audit: http handler disabled or db is read-only")
(rph/wrap nil))
(->> (px/submit! executor #(handle-events cfg params))
(p/fmap (constantly nil)))))
(do
(try
(handle-events cfg params)
(catch Throwable cause
(l/error :hint "unexpected error on persisting audit events from frontend"
:cause cause)))
(rph/wrap nil))))

View File

@@ -6,23 +6,22 @@
(ns app.rpc.commands.auth
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.email :as eml]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -52,24 +51,10 @@
(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 [::db/pool session] :as cfg} {:keys [email password] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [email password] :as params}]
(when-not (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password))
@@ -77,14 +62,18 @@
:code :login-disabled
:hint "login is disabled in this instance"))
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
(letfn [(check-password [conn profile password]
(if (= (:password profile) "!")
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password"))
(:valid (auth/verify-password password (:password profile))))
:hint "the current account does not have password")
(let [result (profile/verify-password cfg password (:password profile))]
(when (:update result)
(l/trace :hint "updating profile password" :id (:id profile) :email (:email profile))
(profile/update-profile-password! conn (assoc profile :password password)))
(:valid result))))
(validate-profile [profile]
(validate-profile [conn profile]
(when-not profile
(ex/raise :type :validation
:code :wrong-credentials))
@@ -94,7 +83,7 @@
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(when-not (check-password profile password)
(when-not (check-password conn profile password)
(ex/raise :type :validation
:code :wrong-credentials))
(when-let [deleted-at (:deleted-at profile)]
@@ -105,11 +94,9 @@
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))
(let [profile (->> (profile/get-profile-by-email conn email)
(validate-profile conn)
(profile/strip-private-attrs))
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))
@@ -122,19 +109,17 @@
(assoc profile :is-admin (let [admins (cf/get :admins)]
(contains? admins (:email profile)))))]
(-> response
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(s/def ::scope ::us/string)
(s/def ::login-with-password
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token ::scope]))
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-password
"Performs authentication using penpot password."
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
[cfg params]
(login-with-password cfg params))
@@ -148,8 +133,8 @@
"Clears the authentication cookie and logout the current session."
{::rpc/auth false
::doc/added "1.15"}
[{:keys [session] :as cfg} _]
(rph/with-transform {} (session/delete-fn session)))
[cfg _]
(rph/with-transform {} (session/delete-fn cfg)))
;; ---- COMMAND: Recover Profile
@@ -160,7 +145,7 @@
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (auth/derive-password password)]
(let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool]
@@ -174,7 +159,6 @@
(sv/defmethod ::recover-profile
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
[cfg params]
(recover-profile cfg params))
@@ -185,14 +169,16 @@
[{:keys [::db/pool] :as cfg} params]
(when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token)
(when-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)
(let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
:hint "email should match the invitation")))))
:code :registration-disabled)))
(when (contains? params :invitation-token)
(let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
:hint "email should match the invitation"))))
(when-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params))
@@ -226,7 +212,7 @@
(validate-register-attempt! cfg params)
(let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))]
(let [profile (when-let [profile (profile/get-profile-by-email pool (:email params))]
(cond
(:is-blocked profile)
(ex/raise :type :restriction
@@ -267,10 +253,11 @@
;; ---- COMMAND: Register Profile
(defn create-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]
[conn {:keys [email] :as params}]
(us/assert! ::us/email email)
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
@@ -279,9 +266,7 @@
:nudge {:big 10 :small 1}})
(db/tjson))
password (if-let [password (:password params)]
(auth/derive-password password)
"!")
password (or (:password params) "!")
locale (:locale params)
locale (when (and (string? locale) (not (str/blank? locale)))
@@ -291,7 +276,7 @@
is-demo (:is-demo params false)
is-muted (:is-muted params false)
is-active (:is-active params false)
email (str/lower (:email params))
email (str/lower email)
params {:id id
:fullname (:fullname params)
@@ -306,7 +291,7 @@
:is-demo is-demo}]
(try
(-> (db/insert! conn :profile params)
(profile/decode-profile-row))
(profile/decode-row))
(catch org.postgresql.util.PSQLException e
(let [state (.getSQLState e)]
(if (not= state "23505")
@@ -316,15 +301,17 @@
:hint "email already exists"
:cause e)))))))
(defn create-profile-relations
[conn profile]
(let [team (teams/create-team conn {:profile-id (:id profile)
(defn create-profile-rels!
[conn {:keys [id] :as profile}]
(let [team (teams/create-team conn {:profile-id id
:name "Default"
:is-default true})]
(-> profile
(profile/strip-private-attrs)
(assoc :default-team-id (:id team))
(assoc :default-project-id (:default-project-id team)))))
(-> (db/update! conn :profile
{:default-team-id (:id team)
:default-project-id (:default-project-id team)}
{:id id})
(profile/decode-row))))
(defn send-email-verification!
[conn props profile]
@@ -348,22 +335,21 @@
:extra-data ptoken})))
(defn register-profile
[{:keys [::db/conn session] :as cfg} {:keys [token] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify (::main/props cfg) {: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))
(not (contains? cf/flags :email-verification)))
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)))
(profile/get-profile conn profile-id)
(let [params (-> params
(assoc :is-active is-active)
(update :password #(profile/derive-password cfg %)))]
(->> (create-profile! conn params)
(create-profile-rels! conn))))
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
@@ -373,9 +359,9 @@
(when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
(audit/submit! cfg
{:type "fact"
:name "register-profile-retry"
:profile-id id}))
{::audit/type "fact"
::audit/name "register-profile-retry"
::audit/profile-id id}))
(cond
;; If invitation token comes in params, this is because the
@@ -389,7 +375,7 @@
token (tokens/generate (::main/props cfg) claims)
resp {:invitation-token token}]
(-> resp
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))
@@ -398,7 +384,7 @@
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
@@ -406,7 +392,7 @@
;; to sign in the user directly, without email verification.
(true? is-active)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
@@ -423,7 +409,6 @@
(sv/defmethod ::register-profile
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
@@ -448,7 +433,7 @@
:exp (dt/in-future {:days 30})})]
(eml/send! {::eml/conn conn
::eml/factory eml/password-recovery
:public-uri (:public-uri cfg)
:public-uri (cf/get :public-uri)
:to (:email profile)
:token (:token profile)
:name (:fullname profile)
@@ -456,7 +441,7 @@
nil))]
(db/with-atomic [conn pool]
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
(when-let [profile (profile/get-profile-by-email conn email)]
(when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted

View File

@@ -8,8 +8,10 @@
(:refer-clojure :exclude [assert])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.fressian :as fres]
[app.common.logging :as l]
[app.common.pages.migrations :as pmg]
[app.common.spec :as us]
@@ -21,14 +23,13 @@
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[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.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
@@ -45,8 +46,7 @@
java.io.DataInputStream
java.io.DataOutputStream
java.io.InputStream
java.io.OutputStream
java.lang.AutoCloseable))
java.io.OutputStream))
(set! *warn-on-reflection* true)
@@ -109,20 +109,20 @@
(defn write-byte!
[^DataOutputStream output data]
(l/trace :fn "write-byte!" :data data :position @*position* ::l/async false)
(l/trace :fn "write-byte!" :data data :position @*position* ::l/sync? true)
(.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)
(l/trace :fn "read-byte!" :val v :position @*position* ::l/sync? true)
(swap! *position* inc)
v))
(defn write-long!
[^DataOutputStream output data]
(l/trace :fn "write-long!" :data data :position @*position* ::l/async false)
(l/trace :fn "write-long!" :data data :position @*position* ::l/sync? true)
(.writeLong output (long data))
(swap! *position* + 8))
@@ -130,14 +130,14 @@
(defn read-long!
[^DataInputStream input]
(let [v (.readLong input)]
(l/trace :fn "read-long!" :val v :position @*position* ::l/async false)
(l/trace :fn "read-long!" :val v :position @*position* ::l/sync? true)
(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)
(l/trace :fn "write-bytes!" :size size :position @*position* ::l/sync? true)
(.write output data 0 size)
(swap! *position* + size)))
@@ -145,7 +145,7 @@
[^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)
(l/trace :fn "read-bytes!" :expected (alength buff) :readed readed :position @*position* ::l/sync? true)
(swap! *position* + readed)
readed))
@@ -153,7 +153,7 @@
(defn write-uuid!
[^DataOutputStream output id]
(l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/async false)
(l/trace :fn "write-uuid!" :position @*position* :WRITTEN? (.size output) ::l/sync? true)
(doto output
(write-byte! (get-mark :uuid))
@@ -162,7 +162,7 @@
(defn read-uuid!
[^DataInputStream input]
(l/trace :fn "read-uuid!" :position @*position* ::l/async false)
(l/trace :fn "read-uuid!" :position @*position* ::l/sync? true)
(let [m (read-byte! input)]
(assert-mark m :uuid)
(let [a (read-long! input)
@@ -171,7 +171,7 @@
(defn write-obj!
[^DataOutputStream output data]
(l/trace :fn "write-obj!" :position @*position* ::l/async false)
(l/trace :fn "write-obj!" :position @*position* ::l/sync? true)
(let [^bytes data (fres/encode data)]
(doto output
(write-byte! (get-mark :obj))
@@ -180,7 +180,7 @@
(defn read-obj!
[^DataInputStream input]
(l/trace :fn "read-obj!" :position @*position* ::l/async false)
(l/trace :fn "read-obj!" :position @*position* ::l/sync? true)
(let [m (read-byte! input)]
(assert-mark m :obj)
(let [size (read-long! input)]
@@ -191,14 +191,14 @@
(defn write-label!
[^DataOutputStream output label]
(l/trace :fn "write-label!" :label label :position @*position* ::l/async false)
(l/trace :fn "write-label!" :label label :position @*position* ::l/sync? true)
(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)
(l/trace :fn "read-label!" :position @*position* ::l/sync? true)
(let [m (read-byte! input)]
(assert-mark m :label)
(read-obj! input)))
@@ -208,7 +208,7 @@
(l/trace :fn "write-header!"
:version version
:position @*position*
::l/async false)
::l/sync? true)
(let [vers (-> version name (subs 1) parse-long)
output (io/data-output-stream output)]
(doto output
@@ -218,7 +218,7 @@
(defn read-header!
[^InputStream input]
(l/trace :fn "read-header!" :position @*position* ::l/async false)
(l/trace :fn "read-header!" :position @*position* ::l/sync? true)
(let [input (io/data-input-stream input)
mark (read-byte! input)
mnum (read-long! input)
@@ -235,13 +235,13 @@
(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)
(l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/sync? true)
(swap! *position* + written)
written))
(defn write-stream!
[^DataOutputStream output stream size]
(l/trace :fn "write-stream!" :position @*position* ::l/async false :size size)
(l/trace :fn "write-stream!" :position @*position* ::l/sync? true :size size)
(doto output
(write-byte! (get-mark :stream))
(write-long! size))
@@ -250,7 +250,7 @@
(defn read-stream!
[^DataInputStream input]
(l/trace :fn "read-stream!" :position @*position* ::l/async false)
(l/trace :fn "read-stream!" :position @*position* ::l/sync? true)
(let [m (read-byte! input)
s (read-long! input)
p (tmp/tempfile :prefix "penpot.binfile.")]
@@ -264,7 +264,7 @@
(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)
(l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/sync? true)
(swap! *position* + readed)
[s p]))
[s (io/read-as-bytes input :size s)])))
@@ -296,18 +296,18 @@
(defn- retrieve-file
[pool file-id]
(with-open [^AutoCloseable conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
(some-> (db/get* conn :file {:id file-id})
(files/decode-row)
(update :data files/process-pointers deref)))))
(files/process-pointers deref)))))
(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)]
(dm/with-open [conn (db/open pool)]
(let [ids (app.tasks.file-gc/collect-used-media data)
ids (db/create-array conn "uuid" ids)]
@@ -341,7 +341,7 @@
(defn- retrieve-libraries
[pool ids]
(with-open [^AutoCloseable conn (db/open pool)]
(dm/with-open [conn (db/open pool)]
(let [ids (db/create-array conn "uuid" ids)]
(map :id (db/exec! pool [sql:file-libraries ids])))))
@@ -351,10 +351,9 @@
(defn- retrieve-library-relations
[pool ids]
(with-open [^AutoCloseable conn (db/open pool)]
(dm/with-open [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) "
@@ -438,9 +437,8 @@
(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?]))
(s/keys :req [::db/pool ::sto/storage ::output ::file-ids]
:opt [::include-libraries? ::embed-assets?]))
(defn write-export!
"Do the exportation of a specified file in custom penpot binary
@@ -453,6 +451,7 @@
`::embed-assets?`: instead of including the libraries, embed 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?))
@@ -466,7 +465,7 @@
(with-open [output (io/data-output-stream output)]
(binding [*state* (volatile! {})]
(run! (fn [section]
(l/debug :hint "write section" :section section ::l/async false)
(l/debug :hint "write section" :section section ::l/sync? true)
(write-label! output section)
(let [options (-> options
(assoc ::output output)
@@ -477,7 +476,7 @@
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
(defmethod write-section :v1/metadata
[{:keys [pool ::output ::file-ids ::include-libraries?]}]
[{:keys [::db/pool ::output ::file-ids ::include-libraries?]}]
(let [libs (when include-libraries?
(retrieve-libraries pool file-ids))
files (into file-ids libs)]
@@ -485,7 +484,7 @@
(vswap! *state* assoc :files files)))
(defmethod write-section :v1/files
[{:keys [pool ::output ::embed-assets?]}]
[{:keys [::db/pool ::output ::embed-assets?]}]
;; Initialize SIDS with empty vector
(vswap! *state* assoc :sids [])
@@ -500,7 +499,7 @@
(l/debug :hint "write penpot file"
:id file-id
:media (count media)
::l/async false)
::l/sync? true)
(doto output
(write-obj! file)
@@ -509,31 +508,31 @@
(vswap! *state* update :sids into storage-object-id-xf media))))
(defmethod write-section :v1/rels
[{:keys [pool ::output ::include-libraries?]}]
[{:keys [::db/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)
(l/debug :hint "found rels" :total (count rels) ::l/sync? true)
(write-obj! output rels)))
(defmethod write-section :v1/sobjects
[{:keys [storage ::output]}]
[{:keys [::sto/storage ::output]}]
(let [sids (-> *state* deref :sids)
storage (media/configure-assets-storage storage)]
(l/debug :hint "found sobjects"
:items (count sids)
::l/async false)
::l/sync? true)
;; 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)
(let [{:keys [size] :as obj} (sto/get-object storage id)]
(l/debug :hint "write sobject" :id id ::l/sync? true)
(doto output
(write-uuid! id)
(write-obj! (meta obj)))
(with-open [^InputStream stream @(sto/get-object-data storage obj)]
(with-open [^InputStream stream (sto/get-object-data storage obj)]
(let [written (write-stream! output stream size)]
(when (not= written size)
(ex/raise :type :validation
@@ -557,9 +556,8 @@
(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?]))
(s/keys :req [::db/pool ::sto/storage ::project-id ::input]
:opt [::overwrite? ::migrate? ::ignore-index-errors?]))
(defn read-import!
"Do the importation of the specified resource in penpot custom binary
@@ -582,14 +580,14 @@
(read-import (assoc options ::version version ::timestamp timestamp))))
(defmethod read-import :v1
[{:keys [pool ::input] :as options}]
[{:keys [::db/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)
(l/debug :hint "reading section" :section section ::l/sync? true)
(assert-read-label! input section)
(let [options (-> options
(assoc ::section section)
@@ -607,7 +605,7 @@
(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)
(l/debug :hint "metadata readed" :version (:full version) :files files ::l/sync? true)
(vswap! *state* update :index update-index files)
(vswap! *state* assoc :version version :files files)))
@@ -618,7 +616,7 @@
(-> data
(update :pages-index update-vals #(update % :objects omap-wrap))
(update :pages-index update-vals pmap-wrap)
(update :components update-vals #(update % :objects omap-wrap))
(update :components update-vals #(d/update-when % :objects omap-wrap))
(update :components pmap-wrap))))
(defmethod read-section :v1/files
@@ -627,7 +625,7 @@
(let [file (read-obj! input)
media' (read-obj! input)
file-id (:id file)
features files/default-features]
features (files/get-default-features)]
(when (not= file-id expected-file-id)
(ex/raise :type :validation
@@ -635,14 +633,14 @@
: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)
(l/debug :hint "update index with media" ::l/sync? true)
(vswap! *state* update :index update-index (map :id media'))
;; Store file media for later insertion
(l/debug :hint "update media references" ::l/async false)
(l/debug :hint "update media references" ::l/sync? true)
(vswap! *state* update :media into (map #(update % :id lookup-index)) media')
(l/debug :hint "processing file" :file-id file-id ::features features ::l/async false)
(l/debug :hint "processing file" :file-id file-id ::features features ::l/sync? true)
(binding [ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity)
@@ -668,7 +666,7 @@
:created-at timestamp
:modified-at timestamp}]
(l/debug :hint "create file" :id file-id' ::l/async false)
(l/debug :hint "create file" :id file-id' ::l/sync? true)
(if overwrite?
(create-or-update-file conn params)
@@ -691,11 +689,11 @@
(l/debug :hint "create file library link"
:file-id (:file-id rel)
:lib-id (:library-file-id rel)
::l/async false)
::l/sync? true)
(db/insert! conn :file-library-rel rel)))))
(defmethod read-section :v1/sobjects
[{:keys [storage conn ::input ::overwrite?]}]
[{:keys [::sto/storage conn ::input ::overwrite?]}]
(let [storage (media/configure-assets-storage storage)
ids (read-obj! input)]
@@ -708,7 +706,7 @@
: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)
(l/debug :hint "readed storage object" :id id ::l/sync? true)
(let [[size resource] (read-stream! input)
hash (sto/calculate-hash resource)
@@ -720,20 +718,20 @@
(assoc ::sto/touched-at (dt/now))
(assoc :bucket "file-media-object"))
sobject @(sto/put-object! storage params)]
sobject (sto/put-object! storage params)]
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/async false)
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
(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)
::l/sync? true)
(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)
(l/warn :hint "ignoring file media object" :file-id (:file-id item) ::l/sync? true)
(db/insert! conn :file-media-object
(-> item
(assoc :file-id file-id)
@@ -744,7 +742,7 @@
(defn- lookup-index
[id]
(let [val (get-in @*state* [:index id])]
(l/trace :fn "lookup-index" :id id :val val ::l/async false)
(l/trace :fn "lookup-index" :id id :val val ::l/sync? true)
(when (and (not (::ignore-index-errors? *options*)) (not val))
(ex/raise :type :validation
:code :incomplete-index
@@ -757,7 +755,7 @@
index index]
(if-let [id (first items)]
(let [new-id (if (::overwrite? *options*) id (uuid/next))]
(l/trace :fn "update-index" :id id :new-id new-id ::l/async false)
(l/trace :fn "update-index" :id id :new-id new-id ::l/sync? true)
(recur (rest items)
(assoc index id new-id)))
index)))
@@ -805,7 +803,7 @@
(try
(process-map-form form)
(catch Throwable cause
(l/warn :hint "failed form" :form (pr-str form) ::l/async false)
(l/warn :hint "failed form" :form (pr-str form) ::l/sync? true)
(throw cause)))
form))
data)))
@@ -836,7 +834,7 @@
cs (volatile! nil)]
(try
(l/info :hint "start exportation" :export-id id)
(with-open [^AutoCloseable output (io/output-stream output)]
(dm/with-open [output (io/output-stream output)]
(binding [*position* (atom 0)]
(write-export! (assoc cfg ::output output))))
@@ -859,7 +857,7 @@
(defn export-to-tmpfile!
[cfg]
(let [path (tmp/tempfile :prefix "penpot.export.")]
(with-open [^AutoCloseable output (io/output-stream path)]
(dm/with-open [output (io/output-stream path)]
(export! cfg output)
path)))
@@ -871,7 +869,7 @@
(l/info :hint "import: started" :import-id id)
(try
(binding [*position* (atom 0)]
(with-open [^AutoCloseable input (io/input-stream input)]
(dm/with-open [input (io/input-stream input)]
(read-import! (assoc cfg ::input input))))
(catch Throwable cause
@@ -893,13 +891,14 @@
(s/def ::embed-assets? ::us/boolean)
(s/def ::export-binfile
(s/keys :req [::rpc/profile-id] :req-un [::file-id ::include-libraries? ::embed-assets?]))
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::include-libraries? ::embed-assets?]))
(sv/defmethod ::export-binfile
"Export a penpot file in a binary format."
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}]
(files/check-read-permissions! pool profile-id file-id)
(let [body (reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream]
@@ -910,17 +909,20 @@
(export! output-stream))))]
(fn [_]
(yrs/response 200 body {"content-type" "application/octet-stream"}))))
{::yrs/status 200
::yrs/body body
::yrs/headers {"content-type" "application/octet-stream"}})))
(s/def ::file ::media/upload)
(s/def ::import-binfile
(s/keys :req [::rpc/profile-id] :req-un [::project-id ::file]))
(s/keys :req [::rpc/profile-id]
:req-un [::project-id ::file]))
(sv/defmethod ::import-binfile
"Import a penpot file in a binary format."
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}]
(db/with-atomic [conn pool]
(projects/check-read-permissions! conn profile-id project-id)
(let [ids (import! (assoc cfg

View File

@@ -19,8 +19,8 @@
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.quotes :as quotes]
[app.rpc.retry :as rtry]
[app.util.pointer-map :as pmap]
[app.util.retry :as rtry]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -54,8 +54,8 @@
:hint "file not found"))))
(defn- get-comment-thread
[conn thread-id & {:keys [for-update?]}]
(-> (db/get-by-id conn :comment-thread thread-id {:for-update for-update?})
[conn thread-id & {:as opts}]
(-> (db/get-by-id conn :comment-thread thread-id opts)
(decode-row)))
(defn- get-comment
@@ -100,8 +100,8 @@
(sv/defmethod ::get-comment-threads
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
(dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comment-threads conn profile-id file-id)))
@@ -143,8 +143,8 @@
(sv/defmethod ::get-unread-comment-threads
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(get-unread-comment-threads conn profile-id team-id)))
@@ -190,8 +190,8 @@
(sv/defmethod ::get-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
(dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")]
@@ -210,8 +210,8 @@
(sv/defmethod ::get-comments
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
(dm/with-open [conn (db/open pool)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-comments conn thread-id))))
@@ -262,8 +262,8 @@
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 [::rpc/profile-id file-id share-id]}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
(dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-file-comments-users conn file-id profile-id)))
@@ -309,7 +309,8 @@
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 3
::rtry/label "create-comment-thread"}
::rtry/label "create-comment-thread"
::db/conn conn}
(create-comment-thread conn
{:created-at request-at
:profile-id profile-id
@@ -372,9 +373,9 @@
(sv/defmethod ::update-comment-thread-status
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(upsert-comment-thread-status! conn profile-id id))))
@@ -389,9 +390,9 @@
(sv/defmethod ::update-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:is-resolved is-resolved}
@@ -412,9 +413,9 @@
(sv/defmethod ::create-comment
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id :for-update? true)
(let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)
{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)]
(files/check-comment-permissions! conn profile-id (:id file) share-id)
@@ -465,10 +466,10 @@
(sv/defmethod ::update-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [thread-id] :as comment} (get-comment conn id :for-update? true)
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id :for-update? true)]
(let [{:keys [thread-id] :as comment} (get-comment conn id ::db/for-update? true)
{:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
@@ -498,9 +499,9 @@
(sv/defmethod ::delete-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(when-not (= owner-id profile-id)
(ex/raise :type :validation
@@ -518,9 +519,9 @@
(sv/defmethod ::delete-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id :for-update? true)
(let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::db/for-update? true)
{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(when-not (= owner-id profile-id)
@@ -538,9 +539,9 @@
(sv/defmethod ::update-comment-thread-position
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:modified-at (::rpc/request-at params)
@@ -558,9 +559,9 @@
(sv/defmethod ::update-comment-thread-frame
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(db/update! conn :comment-thread
{:modified-at (::rpc/request-at params)

View File

@@ -8,12 +8,12 @@
"A demo specific mutations."
(:require
[app.common.exceptions :as ex]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -30,32 +30,31 @@
{::rpc/auth false
::doc/added "1.15"
::doc/changes ["1.15" "This method is migrated from mutations to commands."]}
[{:keys [pool] :as cfg} _]
(let [id (uuid/next)
sem (System/currentTimeMillis)
[{:keys [::db/pool] :as cfg} _]
(when-not (contains? cf/flags :demo-users)
(ex/raise :type :validation
:code :demo-users-not-allowed
:hint "Demo users are disabled by config."))
(let [sem (System/currentTimeMillis)
email (str "demo-" sem ".demo@example.com")
fullname (str "Demo User " sem)
password (-> (bn/random-bytes 16)
(bc/bytes->b64u)
(bc/bytes->str))
params {:id id
:email email
params {:email email
:fullname fullname
:is-active true
:deleted-at (dt/in-future cf/deletion-delay)
:password password
:props {}
}]
(when-not (contains? cf/flags :demo-users)
(ex/raise :type :validation
:code :demo-users-not-allowed
:hint "Demo users are disabled by config."))
:password (profile/derive-password cfg password)
:props {}}]
(db/with-atomic [conn pool]
(->> (cmd.auth/create-profile conn params)
(cmd.auth/create-profile-relations conn))
(with-meta {:email email
:password password}
{::audit/profile-id id}))))
(let [profile (->> (auth/create-profile! conn params)
(auth/create-profile-rels! conn))]
(with-meta {:email email
:password password}
{::audit/profile-id (:id profile)})))))

View File

@@ -0,0 +1,56 @@
;; 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.feedback
"A general purpose feedback module."
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cf]
[app.db :as db]
[app.email :as eml]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(declare ^:private send-feedback!)
(s/def ::content ::us/string)
(s/def ::from ::us/email)
(s/def ::subject ::us/string)
(s/def ::send-user-feedback
(s/keys :req [::rpc/profile-id]
:req-un [::subject
::content]))
(sv/defmethod ::send-user-feedback
{::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
(when-not (contains? cf/flags :user-feedback)
(ex/raise :type :restriction
:code :feedback-disabled
:hint "feedback not enabled"))
(let [profile (profile/get-profile pool profile-id)]
(send-feedback! pool profile params)
nil))
(defn- send-feedback!
[pool profile params]
(let [dest (cf/get :feedback-destination)]
(eml/send! {::eml/conn pool
::eml/factory eml/feedback
:from dest
:to dest
:profile profile
:reply-to (:email profile)
:email (:email profile)
:subject (:subject params)
:content (:content params)})
nil))

View File

@@ -9,26 +9,25 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.common.pages.migrations :as pmg]
[app.common.schema :as sm]
[app.common.schema.desc-js-like :as-alias smdj]
[app.common.schema.generators :as sg]
[app.common.spec :as us]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.common.types.shape-tree :as ctt]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files.thumbnails :as-alias thumbs]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.permissions :as perms]
[app.rpc.queries.projects :as projects]
[app.rpc.queries.share-link :refer [retrieve-share-link]]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
@@ -44,7 +43,8 @@
"storage/pointer-map"
"components/v2"})
(def default-features
(defn get-default-features
[]
(cond-> #{}
(contains? cf/flags :fdata-storage-pointer-map)
(conj "storage/pointer-map")
@@ -128,7 +128,9 @@
([conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id)
ldata (retrieve-share-link conn file-id share-id)]
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
@@ -188,7 +190,7 @@
(ex/raise :type :restriction
:code :features-not-supported
:feature (first not-supported)
:hint (format "features %s not supported" (str/join "," not-supported))))
:hint (format "features %s not supported" (str/join "," (map name not-supported)))))
features))
(defn load-pointer
@@ -196,9 +198,19 @@
(let [row (db/get conn :file-data-fragment
{:id id :file-id file-id}
{:columns [:content]
:check-deleted? false})]
::db/check-deleted? false})]
(blob/decode (:content row))))
(defn- load-all-pointers!
[{:keys [data] :as file}]
(doseq [[_id page] (:pages-index data)]
(when (pmap/pointer-map? page)
(pmap/load! page)))
(doseq [[_id component] (:components data)]
(when (pmap/pointer-map? component)
(pmap/load! component)))
file)
(defn persist-pointers!
[conn file-id]
(doseq [[id item] @pmap/*tracked*]
@@ -222,12 +234,22 @@
(update-fn val)
val)))))))
(defn get-all-pointer-ids
"Given a file, return all pointer ids used in the data."
[fdata]
(->> (concat (vals fdata)
(vals (:pages-index fdata)))
(into #{} (comp (filter pmap/pointer-map?)
(map pmap/get-id)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUERY COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn handle-file-features
[{:keys [features] :as file} client-features]
(defn handle-file-features!
[conn {:keys [id features data] :as file} client-features]
(when (and (contains? features "components/v2")
(not (contains? client-features "components/v2")))
(ex/raise :type :restriction
@@ -235,116 +257,153 @@
:feature "components/v2"
:hint "file has 'components/v2' feature enabled but frontend didn't specifies it"))
(cond-> file
(and (contains? client-features "components/v2")
(not (contains? features "components/v2")))
(update :data ctf/migrate-to-components-v2)
(and (contains? features "storage/pointer-map")
(not (contains? client-features "storage/pointer-map")))
(process-pointers deref)))
;; NOTE: this operation is needed because the components migration
;; generates a new page with random id which is returned to the
;; client; without persisting the migration this can cause that two
;; simultaneous clients can have a different view of the file data
;; and end persisting two pages with main components and breaking
;; the whole file
(let [file (if (and (contains? client-features "components/v2")
(not (contains? features "components/v2")))
(binding [pmap/*tracked* (atom {})]
(let [data (ctf/migrate-to-components-v2 data)
features (conj features "components/v2")
modified-at (dt/now)
features' (db/create-array conn "text" features)]
(db/update! conn :file
{:data (blob/encode data)
:modified-at modified-at
:features features'}
{:id id})
(persist-pointers! conn id)
(-> file
(assoc :modified-at modified-at)
(assoc :features features)
(assoc :data data))))
file)]
(cond-> file
(and (contains? features "storage/pointer-map")
(not (contains? client-features "storage/pointer-map")))
(process-pointers deref))))
;; --- COMMAND QUERY: get-file (by id)
(sm/def! ::features
[:schema
{:title "FileFeatures"
::smdj/inline true
:gen/gen (sg/subseq supported-features)}
::sm/set-of-strings])
(sm/def! ::file
[:map {:title "File"}
[:id ::sm/uuid]
[:features ::features]
[:has-media-trimmed :boolean]
[:comment-thread-seqn {:min 0} :int]
[:name :string]
[:revn {:min 0} :int]
[:modified-at ::dt/instant]
[:is-shared :boolean]
[:project-id ::sm/uuid]
[:created-at ::dt/instant]
[:data {:optional true} :any]])
(sm/def! ::permissions-mixin
[:map {:title "PermissionsMixin"}
[:permissions ::perms/permissions]])
(sm/def! ::file-with-permissions
[:merge {:title "FileWithPermissions"}
::file
::permissions-mixin])
(sm/def! ::get-file
[:map {:title "get-file"}
[:features {:optional true} ::features]
[:id ::sm/uuid]
[:project-id {:optional true} ::sm/uuid]])
(defn get-file
[conn id client-features]
([conn id client-features]
(get-file conn id client-features nil))
([conn id client-features project-id]
;; here we check if client requested features are supported
(check-features-compatibility! client-features)
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(-> (db/get-by-id conn :file id)
(decode-row)
(pmg/migrate-file)
(handle-file-features client-features))))
(check-features-compatibility! client-features)
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(let [params (merge {:id id}
(when (some? project-id)
{:project-id project-id}))
file (-> (db/get conn :file params)
(decode-row)
(pmg/migrate-file))
file (handle-file-features! conn file client-features)]
;; NOTE: if migrations are applied, probably new pointers generated so
;; instead of persiting them on each get-file, we just resolve them until
;; user updates the file and permanently persists the new pointers
(cond-> file
(pmg/migrated? file)
(process-pointers deref))))))
(defn get-minimal-file
[{:keys [pool] :as cfg} id]
[{:keys [::db/pool] :as cfg} id]
(db/get pool :file {:id id} {:columns [:id :modified-at :revn]}))
(defn get-file-etag
[{:keys [modified-at revn]}]
(str (dt/format-instant modified-at :iso) "-" revn))
(s/def ::get-file
(s/keys :req [::rpc/profile-id]
:req-un [::id]
:opt-un [::features]))
(sv/defmethod ::get-file
"Retrieve a file by its ID. Only authenticated users."
{::doc/added "1.17"
::cond/get-object #(get-minimal-file %1 (:id %2))
::cond/key-fn get-file-etag}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features]}]
(with-open [conn (db/open pool)]
::cond/key-fn get-file-etag
::sm/params ::get-file
::sm/result ::file-with-permissions}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features project-id] :as params}]
(dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms)
(let [file (-> (get-file conn id features)
(let [file (-> (get-file conn id features project-id)
(assoc :permissions perms))]
(vary-meta file assoc ::cond/key (get-file-etag file))))))
;; --- COMMAND QUERY: get-file-fragment (by id)
(sm/def! ::file-fragment
[:map {:title "FileFragment"}
[:id ::sm/uuid]
[:file-id ::sm/uuid]
[:created-at ::dt/instant]
[:content any?]])
(sm/def! ::get-file-fragment
[:map {:title "get-file-fragment"}
[:file-id ::sm/uuid]
[:fragment-id ::sm/uuid]
[:share-id {:optional true} ::sm/uuid]])
(defn- get-file-fragment
[conn file-id fragment-id]
(some-> (db/get conn :file-data-fragment {:file-id file-id :id fragment-id})
(update :content blob/decode)))
(s/def ::share-id ::us/uuid)
(s/def ::fragment-id ::us/uuid)
(s/def ::get-file-fragment
(s/keys :req-un [::file-id ::fragment-id]
:opt [::rpc/profile-id]
:opt-un [::share-id]))
(sv/defmethod ::get-file-fragment
"Retrieve a file by its ID. Only authenticated users."
{::doc/added "1.17"
::rpc/:auth false}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
(with-open [conn (db/open pool)]
::sm/params ::get-file-fragment
::sm/result ::file-fragment}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
(dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id file-id share-id)]
(check-read-permissions! perms)
(-> (get-file-fragment conn file-id fragment-id)
(rph/with-http-cache long-cache-duration)))))
;; --- COMMAND QUERY: get-file-object-thumbnails
(defn get-object-thumbnails
([conn file-id]
(let [sql (str/concat
"select object_id, data "
" from file_object_thumbnail"
" where file_id=?")]
(->> (db/exec! conn [sql file-id])
(d/index-by :object-id :data))))
([conn file-id object-ids]
(let [sql (str/concat
"select object_id, data "
" from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)")
ids (db/create-array conn "text" (seq object-ids))]
(->> (db/exec! conn [sql file-id ids])
(d/index-by :object-id :data)))))
(s/def ::get-file-object-thumbnails
(s/keys :req [::rpc/profile-id] :req-un [::file-id]))
(sv/defmethod ::get-file-object-thumbnails
"Retrieve a file object thumbnails."
{::doc/added "1.17"
::cond/get-object #(get-minimal-file %1 (:file-id %2))
::cond/reuse-key? true
::cond/key-fn get-file-etag}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(get-object-thumbnails conn file-id)))
;; --- COMMAND QUERY: get-project-files
(def ^:private sql:project-files
@@ -360,18 +419,18 @@
and f.deleted_at is null
order by f.modified_at desc")
(s/def ::get-project-files
(s/keys :req [::rpc/profile-id] :req-un [::project-id]))
(defn get-project-files
[conn project-id]
(db/exec! conn [sql:project-files project-id]))
(sv/defmethod ::get-project-files
"Get all files for the specified project."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
(with-open [conn (db/open pool)]
{::doc/added "1.17"
::sm/params [:map {:title "get-project-files"}
[:project-id ::sm/uuid]]
::sm/result [:vector ::file]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
(dm/with-open [conn (db/open pool)]
(projects/check-read-permissions! conn profile-id project-id)
(get-project-files conn project-id)))
@@ -380,17 +439,14 @@
(declare get-has-file-libraries)
(s/def ::file-id ::us/uuid)
(s/def ::has-file-libraries
(s/keys :req [::rpc/profile-id]
:req-un [::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 [::rpc/profile-id file-id]}]
(with-open [conn (db/open pool)]
{::doc/added "1.15.1"
::sm/params [:map {:title "has-file-libraries"}
[:file-id ::sm/uuid]]
::sm/result :boolean}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! pool profile-id file-id)
(get-has-file-libraries conn file-id)))
@@ -416,7 +472,7 @@
structure."
[{:keys [objects] :as page} object-id]
(let [objects (cph/get-children-with-self objects object-id)]
(assoc page :objects (d/index-by :id objects))))
(assoc page :objects (d/index-by :id objects))))
(defn- prune-thumbnails
"Given the page data, removes the `:thumbnail` prop from all
@@ -426,24 +482,28 @@
(defn get-page
[conn {:keys [file-id page-id object-id features]}]
(when (and (uuid? object-id)
(not (uuid? page-id)))
(ex/raise :type :validation
:code :params-validation
:hint "page-id is required when object-id is provided"))
(let [file (get-file conn file-id features)
page-id (or page-id (-> file :data :pages first))
page (dm/get-in file [:data :pages-index page-id])]
page (dm/get-in file [:data :pages-index page-id])
page (if (pmap/pointer-map? page)
(deref page)
page)]
(cond-> (prune-thumbnails page)
(uuid? object-id)
(prune-objects object-id))))
(s/def ::page-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::get-page
(s/and
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::page-id ::object-id ::features])
(fn [obj]
(if (contains? obj :object-id)
(contains? obj :page-id)
true))))
(sm/def! ::get-page
[:map {:title "GetPage"}
[:file-id ::sm/uuid]
[:page-id {:optional true} ::sm/uuid]
[:object-id {:optional true} ::sm/uuid]
[:features {:optional true} ::features]])
(sv/defmethod ::get-page
"Retrieves the page data from file and returns it. If no page-id is
@@ -455,11 +515,15 @@
mandatory.
Mainly used for rendering purposes."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(with-open [conn (db/open pool)]
{::doc/added "1.17"
::sm/params ::get-page}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(get-page conn params)))
(binding [pmap/*load-fn* (partial load-pointer conn file-id)]
(get-page conn params))))
;; --- COMMAND QUERY: get-team-shared-files
@@ -484,17 +548,23 @@
(defn get-team-shared-files
[conn team-id]
(letfn [(assets-sample [assets limit]
(let [sorted-assets (->> (vals assets)
(sort-by #(str/lower (:name %))))]
{:count (count sorted-assets)
:sample (into [] (take limit sorted-assets))}))
(let [sorted-assets (->> (vals assets)
(sort-by #(str/lower (:name %))))]
{:count (count sorted-assets)
:sample (into [] (take limit sorted-assets))}))
(library-summary [{:keys [id data] :as file}]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
{:components (assets-sample (:components data) 4)
:media (assets-sample (:media data) 3)
:colors (assets-sample (:colors data) 3)
:typographies (assets-sample (:typographies data) 3)}))]
(let [load-objects (fn [component]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(ctf/load-component-objects data component)))
components-sample (-> (assets-sample (ctkl/components data) 4)
(update :sample
#(map load-objects %)))]
{:components components-sample
:media (assets-sample (:media data) 3)
:colors (assets-sample (:colors data) 3)
:typographies (assets-sample (:typographies data) 3)})))]
(->> (db/exec! conn [sql:team-shared-files team-id])
(into #{} (comp
@@ -509,15 +579,15 @@
(sv/defmethod ::get-team-shared-files
"Get all file (libraries) for the specified team."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(get-team-shared-files conn team-id)))
;; --- COMMAND QUERY: get-file-libraries
(def ^:private sql:file-libraries
(def ^:private sql:get-file-libraries
"WITH RECURSIVE libs AS (
SELECT fl.*, flr.synced_at
FROM file AS fl
@@ -530,7 +600,6 @@
JOIN libs AS l ON (flr.file_id = l.id)
)
SELECT l.id,
l.data,
l.features,
l.project_id,
l.created_at,
@@ -543,30 +612,24 @@
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
(defn get-file-libraries
[conn file-id client-features]
(check-features-compatibility! client-features)
(->> (db/exec! conn [sql:file-libraries file-id])
(map decode-row)
(map #(assoc % :is-indirect false))
(map (fn [{:keys [id] :as row}]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(-> row
(update :data dissoc :pages-index)
(handle-file-features client-features)))))
(vec)))
[conn file-id]
(into []
(comp
(map #(assoc % :is-indirect false))
(map decode-row))
(db/exec! conn [sql:get-file-libraries file-id])))
(s/def ::get-file-libraries
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::features]))
:req-un [::file-id]))
(sv/defmethod ::get-file-libraries
"Get libraries used by the specified file."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(get-file-libraries conn file-id features)))
(get-file-libraries conn file-id)))
;; --- COMMAND QUERY: Files that use this File library
@@ -589,8 +652,8 @@
(sv/defmethod ::get-library-file-references
"Returns all the file references that use specified file (library) id."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(get-library-file-references conn file-id)))
@@ -626,148 +689,11 @@
(sv/defmethod ::get-team-recent-files
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(get-team-recent-files conn team-id)))
;; --- COMMAND QUERY: get-file-thumbnail
(defn get-file-thumbnail
[conn file-id revn]
(let [sql (sql/select :file-thumbnail
(cond-> {:file-id file-id}
revn (assoc :revn revn))
{:limit 1
:order-by [[:revn :desc]]})
row (db/exec-one! conn sql)]
(when-not row
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
{:data (:data row)
:props (some-> (:props row) db/decode-transit-pgobject)
:revn (:revn row)
:file-id (:file-id row)}))
(s/def ::revn ::us/integer)
(s/def ::get-file-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::revn]))
(sv/defmethod ::get-file-thumbnail
{::doc/added "1.17"}
[{:keys [pool]} {:keys [::rpc/profile-id file-id revn]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(-> (get-file-thumbnail conn file-id revn)
(rph/with-http-cache long-cache-duration))))
;; --- COMMAND QUERY: get-file-data-for-thumbnail
(defn get-file-data-for-thumbnail
[conn {:keys [data id] :as file}]
(letfn [;; function responsible on finding the frame marked to be
;; used as thumbnail; the returned frame always have
;; the :page-id set to the page that it belongs.
(get-thumbnail-frame [data]
(d/seek :use-for-thumbnail?
(for [page (-> data :pages-index vals)
frame (-> page :objects ctt/get-frames)]
(assoc frame :page-id (:id page)))))
;; function responsible to filter objects data structure of
;; all unneeded shapes if a concrete frame is provided. If no
;; frame, the objects is returned untouched.
(filter-objects [objects frame-id]
(d/index-by :id (cph/get-children-with-self objects frame-id)))
;; function responsible of assoc available thumbnails
;; to frames and remove all children shapes from objects if
;; thumbnails is available
(assoc-thumbnails [objects page-id thumbnails]
(loop [objects objects
frames (filter cph/frame-shape? (vals objects))]
(if-let [frame (-> frames first)]
(let [frame-id (:id frame)
object-id (str page-id frame-id)
frame (if-let [thumb (get thumbnails object-id)]
(assoc frame :thumbnail thumb :shapes [])
(dissoc frame :thumbnail))
children-ids
(cph/get-children-ids objects frame-id)
bounds
(when (:show-content frame)
(gsh/selection-rect (concat [frame] (->> children-ids (map (d/getf objects))))))
frame
(cond-> frame
(some? bounds)
(assoc :children-bounds bounds))]
(if (:thumbnail frame)
(recur (-> objects
(assoc frame-id frame)
(d/without-keys children-ids))
(rest frames))
(recur (assoc objects frame-id frame)
(rest frames))))
objects)))]
(binding [pmap/*load-fn* (partial load-pointer conn id)]
(let [frame (get-thumbnail-frame data)
frame-id (:id frame)
page-id (or (:page-id frame)
(-> data :pages first))
page (dm/get-in data [:pages-index page-id])
page (cond-> page (pmap/pointer-map? page) deref)
frame-ids (if (some? frame) (list frame-id) (map :id (ctt/get-frames (:objects page))))
obj-ids (map #(str page-id %) frame-ids)
thumbs (get-object-thumbnails conn id obj-ids)]
(cond-> page
;; If we have frame, we need to specify it on the page level
;; and remove the all other unrelated objects.
(some? frame-id)
(-> (assoc :thumbnail-frame-id frame-id)
(update :objects filter-objects frame-id))
;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecessary data.
:always
(update :objects assoc-thumbnails page-id thumbs))))))
(s/def ::get-file-data-for-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
:opt-un [::features]))
(sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used
mainly for render thumbnails on dashboard."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
;; NOTE: we force here the "storage/pointer-map" feature, because
;; it used internally only and is independent if user supports it
;; or not.
(let [feat (into #{"storage/pointer-map"} features)
file (get-file conn file-id feat)]
{:file-id file-id
:revn (:revn file)
:page (get-file-data-for-thumbnail conn file)})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -781,14 +707,31 @@
:modified-at (dt/now)}
{:id id}))
(s/def ::rename-file
(s/keys :req [::rpc/profile-id]
:req-un [::name ::id]))
(sv/defmethod ::rename-file
{::doc/added "1.17"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
::webhooks/event? true
::sm/webhook
[:map {:title "RenameFileEvent"}
[:id ::sm/uuid]
[:project-id ::sm/uuid]
[:name :string]
[:created-at ::dt/instant]
[:modified-at ::dt/instant]]
::sm/params
[:map {:title "RenameFileParams"}
[:name {:min 1} :string]
[:id ::sm/uuid]]
::sm/result
[:map {:title "SimplifiedFile"}
[:id ::sm/uuid]
[:name :string]
[:created-at ::dt/instant]
[:modified-at ::dt/instant]]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id)
(let [file (rename-file conn params)]
@@ -810,25 +753,38 @@
{:is-shared is-shared}
{:id id}))
(def sql:get-referenced-files
"SELECT f.id
FROM file_library_rel AS flr
INNER JOIN file AS f ON (f.id = flr.file_id)
WHERE flr.library_file_id = ?
ORDER BY f.created_at ASC;")
(defn absorb-library
"Find all files using a shared library, and absorb all library assets
into the file local libraries"
[conn {:keys [id] :as params}]
(let [library (db/get-by-id conn :file id)]
(when (:is-shared library)
(let [ldata (-> library decode-row pmg/migrate-file :data)]
(->> (db/query conn :file-library-rel {:library-file-id id})
(map :file-id)
(keep #(db/get-by-id conn :file % {:check-deleted? false}))
(map decode-row)
(map pmg/migrate-file)
(run! (fn [{:keys [id data revn] :as file}]
(let [data (ctf/absorb-assets data ldata)]
(db/update! conn :file
{:revn (inc revn)
:data (blob/encode data)
:modified-at (dt/now)}
{:id id})))))))))
(let [ldata (binding [pmap/*load-fn* (partial load-pointer conn id)]
(-> library decode-row load-all-pointers! pmg/migrate-file :data))
rows (db/exec! conn [sql:get-referenced-files id])]
(doseq [file-id (map :id rows)]
(binding [pmap/*load-fn* (partial load-pointer conn file-id)
pmap/*tracked* (atom {})]
(let [file (-> (db/get-by-id conn :file file-id
::db/check-deleted? false
::db/remove-deleted? false)
(decode-row)
(load-all-pointers!)
(pmg/migrate-file))
data (ctf/absorb-assets (:data file) ldata)]
(db/update! conn :file
{:revn (inc (:revn file))
:data (blob/encode data)
:modified-at (dt/now)}
{:id file-id})
(persist-pointers! conn file-id))))))))
(s/def ::set-file-shared
(s/keys :req [::rpc/profile-id]
@@ -837,7 +793,7 @@
(sv/defmethod ::set-file-shared
{::doc/added "1.17"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id)
(when-not is-shared
@@ -866,7 +822,7 @@
(sv/defmethod ::delete-file
{::doc/added "1.17"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id)
(absorb-library conn params)
@@ -896,7 +852,7 @@
(sv/defmethod ::link-file-to-library
{::doc/added "1.17"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
(when (= file-id library-id)
(ex/raise :type :validation
:code :invalid-library
@@ -921,7 +877,7 @@
(sv/defmethod ::unlink-file-from-library
{::doc/added "1.17"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(unlink-file-from-library conn params)))
@@ -943,9 +899,9 @@
;; TODO: improve naming
(sv/defmethod ::update-file-library-sync-status
"Update the synchronization statos of a file->library link"
"Update the synchronization status of a file->library link"
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(update-sync conn params)))
@@ -967,70 +923,8 @@
(sv/defmethod ::ignore-file-library-sync-status
"Ignore updates in linked files"
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(-> (ignore-sync conn params)
(update :features db/decode-pgarray #{}))))
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
(def sql:upsert-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, data)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set data = ?;")
(defn upsert-file-object-thumbnail!
[conn {:keys [file-id object-id data]}]
(if data
(db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id})))
(s/def ::data (s/nilable ::us/string))
(s/def ::thumbs/object-id ::us/string)
(s/def ::upsert-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::thumbs/object-id]
:opt-un [::data]))
(sv/defmethod ::upsert-file-object-thumbnail
{::doc/added "1.17"
::audit/skip true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(upsert-file-object-thumbnail! conn params)
nil))
;; --- MUTATION COMMAND: upsert-file-thumbnail
(def sql:upsert-file-thumbnail
"insert into file_thumbnail (file_id, revn, data, props)
values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
update set data = ?, props=?, updated_at=now();")
(defn upsert-file-thumbnail
[conn {:keys [file-id revn data props]}]
(let [props (db/tjson (or props {}))]
(db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data props data props])))
(s/def ::revn ::us/integer)
(s/def ::props map?)
(s/def ::upsert-file-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::revn ::data ::props]))
(sv/defmethod ::upsert-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.17"
::audit/skip true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id)
(upsert-file-thumbnail conn params)
nil))

View File

@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.commands.files.create
(ns app.rpc.commands.files-create
(:require
[app.common.data :as d]
[app.common.files.features :as ffeat]
@@ -15,14 +15,15 @@
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms]
[app.rpc.queries.projects :as proj]
[app.rpc.quotes :as quotes]
[app.util.blob :as blob]
[app.util.objects-map :as omap]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
(defn create-file-role!
@@ -33,22 +34,25 @@
(db/insert! conn :file-profile-rel))))
(defn create-file
[conn {:keys [id name project-id is-shared data revn
[conn {:keys [id name project-id is-shared revn
modified-at deleted-at create-page
ignore-sync-until features]
:or {is-shared false revn 0 create-page true}
:as params}]
(let [id (or id (:id data) (uuid/next))
features (-> (into files/default-features features)
(files/check-features-compatibility!))
data (or data
(binding [ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)]
(if create-page
(ctf/make-file-data id)
(ctf/make-file-data id nil))))
(let [id (or id (uuid/next))
features (->> features
(into (files/get-default-features))
(files/check-features-compatibility!))
pointers (atom {})
data (binding [pmap/*tracked* pointers
ffeat/*current* features
ffeat/*wrap-with-objects-map-fn* (if (features "storate/objects-map") omap/wrap identity)
ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity)]
(if create-page
(ctf/make-file-data id)
(ctf/make-file-data id nil)))
features (db/create-array conn "text" features)
file (db/insert! conn :file
@@ -64,9 +68,16 @@
:modified-at modified-at
:deleted-at deleted-at}))]
(binding [pmap/*tracked* pointers]
(files/persist-pointers! conn id))
(->> (assoc params :file-id id :role :owner)
(create-file-role! conn))
(db/update! conn :project
{:modified-at (dt/now)}
{:id project-id})
(files/decode-row file)))
(s/def ::create-file
@@ -79,10 +90,11 @@
(sv/defmethod ::create-file
{::doc/added "1.17"
::doc/module :files
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id project-id)
(projects/check-edition-permissions! conn profile-id project-id)
(let [team-id (files/get-team-id conn project-id)
params (assoc params :profile-id profile-id)]

View File

@@ -4,42 +4,44 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.mutations.share-link
(ns app.rpc.commands.files-share
"Share link related rpc mutation methods."
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::who-comment ::us/string)
(s/def ::who-inspect ::us/string)
(s/def ::pages (s/every ::us/uuid :kind set?))
;; --- Mutation: Create Share Link
;; --- MUTATION: Create Share Link
(declare create-share-link)
(s/def ::create-share-link
(s/keys :req-un [::profile-id ::file-id ::who-comment ::who-inspect ::pages]))
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::who-comment ::who-inspect ::pages]))
(sv/defmethod ::create-share-link
"Creates a share-link object.
Share links are resources that allows external users access to specific
pages of a file with specific permissions (who-comment and who-inspect)."
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
{::doc/added "1.18"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(create-share-link conn params)))
(create-share-link conn (assoc params :profile-id profile-id))))
(defn create-share-link
[conn {:keys [profile-id file-id pages who-comment who-inspect]}]
@@ -51,18 +53,19 @@
:who-inspect who-inspect
:pages pages
:owner-id profile-id})]
(-> slink
(update :pages db/decode-pgarray #{}))))
;; --- Mutation: Delete Share Link
(update slink :pages db/decode-pgarray #{})))
(declare delete-share-link)
;; --- MUTATION: Delete Share Link
(s/def ::delete-share-link
(s/keys :req-un [::profile-id ::id]))
(s/keys :req [::rpc/profile-id]
:req-un [::us/id]))
(sv/defmethod ::delete-share-link
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
{::doc/added "1.18"
::doc/module ::files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [slink (db/get-by-id conn :share-link id)]
(files/check-edition-permissions! conn profile-id (:file-id slink))

View File

@@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.commands.files.temp
(ns app.rpc.commands.files-temp
(:require
[app.common.exceptions :as ex]
[app.common.pages :as cp]
@@ -13,10 +13,10 @@
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.files.create :as files.create]
[app.rpc.commands.files.update :as files.update]
[app.rpc.commands.files-create :refer [create-file]]
[app.rpc.commands.files-update :as-alias files.update]
[app.rpc.commands.projects :as projects]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.projects :as proj]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -36,16 +36,17 @@
::create-page]))
(sv/defmethod ::create-temp-file
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
{::doc/added "1.17"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id project-id)
(files.create/create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1})))))
(projects/check-edition-permissions! conn profile-id project-id)
(create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1})))))
;; --- MUTATION COMMAND: update-temp-file
(defn update-temp-file
[conn {:keys [::rpc/profile-id session-id id revn changes] :as params}]
[conn {:keys [profile-id session-id id revn changes] :as params}]
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
@@ -57,16 +58,18 @@
:changes (blob/encode changes)}))
(s/def ::update-temp-file
(s/keys :req-un [::files.update/changes
(s/keys :req [::rpc/profile-id]
:req-un [::files.update/changes
::files.update/revn
::files.update/session-id
::files/id]))
(sv/defmethod ::update-temp-file
{::doc/added "1.17"}
[{:keys [pool] :as cfg} params]
{::doc/added "1.17"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(update-temp-file conn params)
(update-temp-file conn (assoc params :profile-id profile-id))
nil))
;; --- MUTATION COMMAND: persist-temp-file
@@ -100,8 +103,9 @@
:req-un [::files/id]))
(sv/defmethod ::persist-temp-file
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
{::doc/added "1.17"
::doc/module :files}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(persist-temp-file conn params)))

View File

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

View File

@@ -4,13 +4,16 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.commands.files.update
(ns app.rpc.commands.files-update
(:require
[app.common.exceptions :as ex]
[app.common.files.features :as ffeat]
[app.common.logging :as l]
[app.common.pages :as cp]
[app.common.pages.changes :as cpc]
[app.common.pages.migrations :as pmg]
[app.common.schema :as sm]
[app.common.schema.generators :as smg]
[app.common.spec :as us]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
@@ -21,7 +24,7 @@
[app.metrics :as mtx]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.climit :as climit]
[app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
@@ -60,6 +63,40 @@
(or (contains? o :changes)
(contains? o :changes-with-metadata)))))
;; --- SCHEMA
(sm/def! ::changes
[:vector ::cpc/change])
(sm/def! ::change-with-metadata
[:map {:title "ChangeWithMetadata"}
[:changes ::changes]
[:hint-origin {:optional true} :keyword]
[:hint-events {:optional true} [:vector :string]]])
(sm/def! ::update-file-params
[:map {:title "UpdateFileParams"}
[:id ::sm/uuid]
[:session-id ::sm/uuid]
[:revn {:min 0} :int]
[:features {:optional true
:gen/max 3
:gen/gen (smg/subseq files/supported-features)}
::sm/set-of-strings]
[:changes {:optional true} ::changes]
[:changes-with-metadata {:optional true}
[:vector ::change-with-metadata]]])
(sm/def! ::update-file-result
[:vector {:title "UpdateFileResults"}
[:map {:title "UpdateFileResult"}
[:changes ::changes]
[:file-id ::sm/uuid]
[:id ::sm/uuid]
[:revn {:min 0} :int]
[:session-id ::sm/uuid]]])
;; --- HELPERS
;; File changes that affect to the library, and must be notified
@@ -78,8 +115,7 @@
(defn- library-change?
[{:keys [type] :as change}]
(or (contains? library-change-types type)
(and (contains? file-change-types type)
(some? (:component-id change)))))
(contains? file-change-types type)))
(def ^:private sql:get-file
"SELECT f.*, p.team_id
@@ -101,7 +137,7 @@
(defn- wrap-with-pointer-map-context
[f]
(fn [{:keys [conn] :as cfg} {:keys [id] :as file}]
(fn [{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(binding [pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn id)
ffeat/*wrap-with-pointer-map-fn* pmap/wrap]
@@ -126,18 +162,22 @@
;; database.
(sv/defmethod ::update-file
{::climit/queue :update-file
{::climit/id :update-file-by-id
::climit/key-fn :id
::webhooks/event? true
::webhooks/batch-timeout (dt/duration "2m")
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
::sm/params ::update-file-params
::sm/result ::update-file-result
::doc/module :files
::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id)
(let [cfg (assoc cfg :conn conn)
(let [cfg (assoc cfg ::db/conn conn)
params (assoc params :profile-id profile-id)
tpoint (dt/tpoint)]
(-> (update-file cfg params)
@@ -145,17 +185,18 @@
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
(defn update-file
[{:keys [conn metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
[{:keys [::db/conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
(let [file (get-file conn id)
features (->> (concat (:features file)
(:features params))
(into files/default-features)
(into (files/get-default-features))
(files/check-features-compatibility!))]
(files/check-edition-permissions! conn profile-id (:id file))
(binding [ffeat/*current* features
ffeat/*previous* (:features file)]
(let [update-fn (cond-> update-file*
(contains? features "storage/pointer-map")
(wrap-with-pointer-map-context)
@@ -197,24 +238,34 @@
:project-id (:project-id file)
:team-id (:team-id file)}))))))
(defn- update-file-data
[file changes]
(-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
:always
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
(and (contains? ffeat/*current* "components/v2")
(not (contains? ffeat/*previous* "components/v2")))
(ctf/migrate-to-components-v2)
:always
(-> (cp/process-changes changes)
(blob/encode)))))))
(defn- update-file*
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
(let [file (-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
:always
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
[{:keys [::db/conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
(let [;; Process the file data in the CLIMIT context; scheduling it
;; to be executed on a separated executor for avoid to do the
;; CPU intensive operation on vthread.
file (-> (climit/configure cfg :update-file)
(climit/submit! (partial update-file-data file changes)))]
(and (contains? ffeat/*current* "components/v2")
(not (contains? ffeat/*previous* "components/v2")))
(ctf/migrate-to-components-v2)
:always
(-> (cp/process-changes changes)
(blob/encode))))))]
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
@@ -273,11 +324,10 @@
(vec)))
(defn- send-notifications!
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [file changes session-id] :as params}]
(let [lchanges (filter library-change? changes)
msgbus (:msgbus cfg)]
msgbus (::mbus/msgbus cfg)]
;; Asynchronously publish message to the msgbus
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-change
@@ -290,7 +340,6 @@
(when (and (:is-shared file) (seq lchanges))
(let [team-id (or (:team-id file)
(files/get-team-id conn (:project-id file)))]
;; Asynchronously publish message to the msgbus
(mbus/pub! msgbus
:topic team-id
:message {:type :library-change

View File

@@ -0,0 +1,226 @@
;; 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.fonts
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
(s/def ::data (s/map-of ::us/string any?))
(s/def ::file-id ::us/uuid)
(s/def ::font-id ::us/uuid)
(s/def ::id ::us/uuid)
(s/def ::name ::us/not-empty-string)
(s/def ::project-id ::us/uuid)
(s/def ::style valid-style)
(s/def ::team-id ::us/uuid)
(s/def ::weight valid-weight)
;; --- QUERY: Get font variants
(s/def ::get-font-variants
(s/and
(s/keys :req [::rpc/profile-id]
:opt-un [::team-id
::file-id
::project-id])
(fn [o]
(or (contains? o :team-id)
(contains? o :file-id)
(contains? o :project-id)))))
(sv/defmethod ::get-font-variants
{::doc/added "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}]
(dm/with-open [conn (db/open pool)]
(cond
(uuid? team-id)
(do
(teams/check-read-permissions! conn profile-id team-id)
(db/query conn :team-font-variant
{:team-id team-id
:deleted-at nil}))
(uuid? project-id)
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
(projects/check-read-permissions! conn profile-id project-id)
(db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil}))
(uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})]
(files/check-read-permissions! conn profile-id file-id)
(db/query conn :team-font-variant
{:team-id (:team-id project)
:deleted-at nil})))))
(declare create-font-variant)
(s/def ::create-font-variant
(s/keys :req [::rpc/profile-id]
:req-un [::team-id
::data
::font-id
::font-family
::font-weight
::font-style]))
(sv/defmethod ::create-font-variant
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(teams/check-edition-permissions! pool profile-id team-id)
(quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(create-font-variant cfg (assoc params :profile-id profile-id))))
(defn create-font-variant
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [data] :as params}]
(letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf"))
(not (contains? data "font/ttf"))
(not (contains? data "font/woff"))
(not (contains? data "font/woff2")))
(ex/raise :type :validation
:code :invalid-font-upload
:hint "invalid font upload, unable to generate missing font assets"))
data))
(prepare-font [data mtype]
(when-let [resource (get data mtype)]
(let [hash (sto/calculate-hash resource)
content (-> (sto/content resource)
(sto/wrap-with-hash hash))]
{::sto/content content
::sto/touched-at (dt/now)
::sto/deduplicate? true
:content-type mtype
:bucket "team-font-variant"})))
(persist-fonts-files! [data]
(let [otf-params (prepare-font data "font/otf")
ttf-params (prepare-font data "font/ttf")
wf1-params (prepare-font data "font/woff")
wf2-params (prepare-font data "font/woff2")]
(cond-> {}
(some? otf-params)
(assoc :otf (sto/put-object! storage otf-params))
(some? ttf-params)
(assoc :ttf (sto/put-object! storage ttf-params))
(some? wf1-params)
(assoc :woff1 (sto/put-object! storage wf1-params))
(some? wf2-params)
(assoc :woff2 (sto/put-object! storage wf2-params)))))
(insert-font-variant! [{:keys [woff1 woff2 otf ttf]}]
(db/insert! pool :team-font-variant
{:id (uuid/next)
:team-id (:team-id params)
:font-id (:font-id params)
:font-family (:font-family params)
:font-weight (:font-weight params)
:font-style (:font-style params)
:woff1-file-id (:id woff1)
:woff2-file-id (:id woff2)
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)}))
]
(let [data (-> (climit/configure cfg :process-font)
(climit/submit! (partial generate-missing! data)))
assets (persist-fonts-files! data)
result (insert-font-variant! assets)]
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
;; --- UPDATE FONT FAMILY
(s/def ::update-font
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::id ::name]))
(sv/defmethod ::update-font
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id id name]}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(rph/with-meta
(db/update! conn :team-font-variant
{:font-family name}
{:font-id id
:team-id team-id})
{::audit/replace-props {:id id
:name name
:team-id team-id
:profile-id profile-id}})))
;; --- DELETE FONT
(s/def ::delete-font
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::id]))
(sv/defmethod ::delete-font
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [font (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:font-id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:id id
:team-id team-id
:name (:font-family font)
:profile-id profile-id}}))))
;; --- DELETE FONT VARIANT
(s/def ::delete-font-variant
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::id]))
(sv/defmethod ::delete-font-variant
{::doc/added "1.18"
::webhooks/event? true}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [variant (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}}))))

View File

@@ -14,10 +14,10 @@
[app.loggers.audit :as-alias audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
@@ -39,7 +39,7 @@
is properly configured and enabled with `login-with-ldap` flag."
{::rpc/auth false
::doc/added "1.15"}
[{:keys [::main/props ::ldap/provider session] :as cfg} params]
[{:keys [::main/props ::ldap/provider] :as cfg} params]
(when-not provider
(ex/raise :type :restriction
:code :ldap-not-initialized
@@ -67,24 +67,23 @@
:member-email (:email profile))
token (tokens/generate props claims)]
(-> {:invitation-token token}
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/props (:props profile)
::audit/profile-id (:id profile)})))
(-> profile
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/props (:props profile)
::audit/profile-id (:id profile)}))))))
(defn- login-or-register
[{:keys [pool] :as cfg} info]
[{:keys [::db/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))
(profile/get-profile-by-email conn)
(profile/decode-row))
(->> (assoc info :is-active true :is-demo false)
(cmd.auth/create-profile conn)
(cmd.auth/create-profile-relations conn)
(auth/create-profile! conn)
(auth/create-profile-rels! conn)
(profile/strip-private-attrs)))))

View File

@@ -17,9 +17,9 @@
[app.rpc :as-alias rpc]
[app.rpc.commands.binfile :as binfile]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as proj]
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.projects :as proj]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
@@ -46,7 +46,7 @@
"Duplicate a single file in the same team."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(duplicate-file conn (assoc params :profile-id profile-id))))
@@ -221,7 +221,7 @@
"Duplicate an entire project with all the files"
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} params]
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(duplicate-project conn (assoc params :profile-id (::rpc/profile-id params)))))
@@ -231,12 +231,13 @@
;; Defer all constraints
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(let [project (db/get-by-id conn :project project-id)
(let [project (-> (db/get-by-id conn :project project-id)
(assoc :is-pinned false))
files (db/query conn :file
{:project-id (:id project)
:deleted-at nil}
{:columns [:id]})
{:project-id (:id project)
:deleted-at nil}
{:columns [:id]})
project (cond-> project
(string? name)
@@ -329,7 +330,7 @@
"Move a set of files from one project to other."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(move-files conn (assoc params :profile-id profile-id))))
@@ -369,7 +370,7 @@
"Move projects between teams."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(move-project conn (assoc params :profile-id profile-id))))
@@ -386,7 +387,7 @@
"Clone into the specified project the template by its id."
{::doc/added "1.16"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(clone-template (assoc params :profile-id profile-id)))))

View File

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

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