Compare commits

...

110 Commits

Author SHA1 Message Date
Andrés Moya
41a46fe56a 🔧 Refactor debug traces in libraries module 2024-04-18 12:30:02 +02:00
Andrey Antukh
fd92437f7d Show error notificaton on oidc error redirect 2024-04-18 09:52:24 +02:00
Andrey Antukh
81b52d7170 Disable oidc registration when registration flag is disabled 2024-04-18 09:52:24 +02:00
Andrey Antukh
a969550aa4 Trust oidc providers which provides email_verified claim 2024-04-18 09:52:24 +02:00
Andrey Antukh
7e20cf10c5 ⬆️ Update dependencies 2024-04-18 09:52:24 +02:00
Andrey Antukh
90022041e6 Improve error handling on ldap frontend code 2024-04-18 09:52:24 +02:00
Andrey Antukh
ce790d83fd Improve internal registration flow 2024-04-18 09:52:24 +02:00
Andrey Antukh
606aeeb38f Allow attach context to audit entry from metadata 2024-04-18 09:52:24 +02:00
Andrey Antukh
ef632bcae7 Trigger email verification on OIDC register process
This will happen only if:
- the oidc provider properly reports that user has a non-verified email
- the oidc does not provides any way to know the email verification
  status
2024-04-18 09:52:24 +02:00
Andrey Antukh
b9ed0e1146 Merge remote-tracking branch 'origin/staging' into develop 2024-04-17 19:32:39 +02:00
Alejandro
8bd9c0d031 Merge pull request #4444 from penpot/palba-bugfixing-008
🐛 Bugfixing
2024-04-17 06:46:57 +02:00
Alejandro
88f46f2ab2 Merge pull request #4458 from penpot/niwinz-staging-bugfix-2
 Minor enhancements
2024-04-17 06:40:42 +02:00
Andrey Antukh
edd91f00af Add minor improvement to worker module logging 2024-04-16 23:27:00 +02:00
Andrey Antukh
6477a48a50 Merge remote-tracking branch 'origin/staging' into develop 2024-04-16 19:35:24 +02:00
Andrey Antukh
16fa0b0330 Improve email clean mechanism 2024-04-16 17:24:50 +02:00
Alejandro
c975e0bcee Merge pull request #4455 from penpot/niwinz-staging-bugfix-2
 Make some storage operations asynchronous
2024-04-16 17:01:52 +02:00
Andrey Antukh
25001e5b80 📎 Add minor logging improvements on worker module 2024-04-16 16:42:44 +02:00
Andrey Antukh
c3a0db2431 Add the ability to schedule storage object touching as a task 2024-04-16 16:42:44 +02:00
Andrey Antukh
e27c0b2086 Add a task for asynchronous object update operation 2024-04-16 16:42:44 +02:00
Pablo Alba
ec8c847440 🐛 Fix wrong permissions on shared prototype for owners 2024-04-16 13:12:21 +02:00
Ikko Eltociear Ashimine
64ff6d05c0 🐛 Fix alt typo
Communnity -> Community
2024-04-16 13:01:11 +02:00
Alejandro Alonso
ea42669ff6 Merge remote-tracking branch 'origin/staging' into develop 2024-04-16 12:42:20 +02:00
Andrey Antukh
caaf695352 📚 Update changelog 2024-04-16 12:39:13 +02:00
Alejandro
56f4348586 Merge pull request #4452 from penpot/niwinz-staging-bugfix-1
 Reduce lock contention on uploading file object thumbnail
2024-04-16 11:52:24 +02:00
Andrey Antukh
56ba32b66d Reduce lock contention on uploading file object thumbnail 2024-04-16 11:37:35 +02:00
Alejandro
4dacba6836 Merge pull request #4450 from penpot/niwinz-staging-bugfix-1
 Make cron task schedule sync more lock resilent
2024-04-16 09:28:03 +02:00
Jordi Sala Morales
ddfe5fbcb8 Avoid non existent function warning 2024-04-16 08:47:35 +02:00
Andrey Antukh
7948f565e3 Make cron task schedule sync more lock resilent 2024-04-16 08:39:04 +02:00
Alejandro Alonso
b14b8f794a Merge remote-tracking branch 'origin/staging' into develop 2024-04-15 20:58:22 +02:00
Alejandro Alonso
87927a3476 Merge remote-tracking branch 'origin/staging' into develop 2024-04-15 16:47:06 +02:00
Pablo Alba
acc1fac8de 🐛 Fix color picker names are not displayed correctly on their tooltips 2024-04-15 16:07:05 +02:00
Andrey Antukh
83f8218bf3 🐛 Fix incorrect queryparams handling on default nginx entrypoint 2024-04-12 10:50:13 +02:00
Andrey Antukh
c9fbb21924 Don't stop start-tmux if playwright is not found on frontend 2024-04-12 10:08:15 +02:00
Andrey Antukh
bc279977d5 📎 Comment playwright installation on frontend directory 2024-04-12 09:58:40 +02:00
Andrey Antukh
ca640964d1 ⬆️ Update exporter dependencies 2024-04-12 09:58:07 +02:00
Belén Albeza
4d29bff9b5 Add playwright to the frontend 2024-04-11 14:05:16 +02:00
Alejandro Alonso
ff04877d24 Merge remote-tracking branch 'origin/staging' into develop 2024-04-11 13:33:37 +02:00
Alejandro Alonso
34d75957f0 Merge remote-tracking branch 'origin/staging' into develop 2024-04-11 12:36:45 +02:00
Alejandro Alonso
5fccc59ad5 Merge remote-tracking branch 'origin/staging' into develop 2024-04-11 10:21:04 +02:00
Alejandro Alonso
c1c0ec6f2f Merge remote-tracking branch 'origin/staging' into develop 2024-04-10 16:32:19 +02:00
Alejandro Alonso
f47991fa9c Merge remote-tracking branch 'origin/staging' into develop 2024-04-10 09:31:44 +02:00
Kelp
656e910896 📎 Remove unnecessary html tags 2024-04-09 17:55:51 +02:00
Kelp
39c35d51d3 📎 Add missing <p> 2024-04-09 17:55:51 +02:00
Kelp
5b4fcd2c80 📎 Fix small content error 2024-04-09 17:55:51 +02:00
Kelp
91ade7fe58 🐛 Fix social media links 2024-04-09 17:55:51 +02:00
Kelp
19878484a6 🐛 Fix wrong website link 2024-04-09 17:55:51 +02:00
Kelp
c27f884755 Add light and dark version of the header 2024-04-09 17:55:51 +02:00
Alejandro Alonso
19fc5fa820 Merge remote-tracking branch 'origin/staging' into develop 2024-04-09 12:56:31 +02:00
Andrey Antukh
171320d1c0 Merge remote-tracking branch 'origin/staging' into develop 2024-04-09 10:05:51 +02:00
Alejandro Alonso
3fa1d3b5fd Merge remote-tracking branch 'origin/staging' into develop 2024-04-08 16:53:28 +02:00
Andrey Antukh
fb24918fd9 Merge remote-tracking branch 'origin/staging' into develop 2024-04-08 14:07:43 +02:00
Kelp
984dacba2b Update README.md small fix 2024-04-08 11:50:55 +02:00
Kelp
93debeb272 Update README.md 2.0 2024-04-08 11:49:56 +02:00
Andrey Antukh
e813fcb9b7 Merge remote-tracking branch 'origin/staging' into develop 2024-04-04 14:19:45 +02:00
Andrey Antukh
ad0aae375b Merge remote-tracking branch 'origin/staging' into develop 2024-04-04 12:45:38 +02:00
Andrey Antukh
d20a92ce50 Merge remote-tracking branch 'origin/staging' into develop 2024-04-02 11:35:30 +02:00
Andrey Antukh
4351c221ac Merge remote-tracking branch 'origin/staging' into develop 2024-03-27 11:10:45 +01:00
Alejandro Alonso
1f5658ad1b Merge remote-tracking branch 'origin/staging' into develop 2024-03-26 09:44:10 +01:00
Alejandro
0df97d4d7d Merge pull request #4324 from penpot/alotor-bugfix-34
Bugfixes
2024-03-26 07:40:07 +01:00
alonso.torres
e10c96fa8b 🐛 Fix problem with grid edition 2024-03-25 16:10:25 +01:00
alonso.torres
4b846b17f0 🐛 Fix problem when exporting html texts 2024-03-25 15:35:04 +01:00
alonso.torres
6f2f2291c2 🐛 Fix problem when importing SVG 2024-03-25 13:12:47 +01:00
alonso.torres
33c12117cc 🐛 Fix problem with gradients and borders 2024-03-25 09:49:38 +01:00
alonso.torres
94f9551b92 🐛 Fix problem with gradient fill text 2024-03-25 09:20:12 +01:00
Andrey Antukh
259eae70c6 Merge remote-tracking branch 'origin/staging' into develop 2024-03-25 08:48:18 +01:00
Andrey Antukh
c0fa766b64 📎 Update version.txt file 2024-03-22 08:40:31 +01:00
Andrey Antukh
a79e4d7af3 Merge remote-tracking branch 'origin/staging' into develop 2024-03-22 08:40:09 +01:00
Alejandro
589fb144db Merge pull request #4272 from peterstnsz/patch-1
Update README.md
2024-03-18 10:18:41 +01:00
alonso.torres
fd4470afde Merge remote-tracking branch 'origin/staging' into develop 2024-03-18 09:59:06 +01:00
Peter
e1e05e6de8 Update README.md
Link in the footer https://kaleidos.net/products returns 404
2024-03-14 14:52:20 +00:00
Andrey Antukh
d5dbe0b594 Merge remote-tracking branch 'origin/staging' into develop 2024-03-06 12:06:52 +01:00
Andrey Antukh
0606ef1c84 Merge remote-tracking branch 'origin/staging' into develop 2024-03-05 18:53:36 +01:00
Andrey Antukh
481058b8d4 Merge remote-tracking branch 'origin/staging' into develop 2024-03-04 10:29:11 +01:00
Andrey Antukh
a109673654 Merge remote-tracking branch 'origin/staging' into develop 2024-02-29 12:53:08 +01:00
Pablo Alba
b17371d440 🐛 Fix problems on sync with components chain with reset override 2024-02-28 11:35:19 +01:00
Andrey Antukh
617edbebec Merge remote-tracking branch 'origin/staging' into develop 2024-02-28 11:25:36 +01:00
Andrey Antukh
e3f9bafb33 Merge remote-tracking branch 'origin/staging' into develop 2024-02-27 12:53:20 +01:00
Andrey Antukh
2a6589ab01 Merge remote-tracking branch 'origin/staging' into develop 2024-02-27 12:45:59 +01:00
Andrey Antukh
9e2e9f5b64 Merge remote-tracking branch 'origin/staging' into develop 2024-02-22 16:24:26 +01:00
Andrey Antukh
82dca0439a Merge remote-tracking branch 'origin/staging' into develop 2024-02-21 14:56:28 +01:00
Andrey Antukh
66f8ffb408 Merge remote-tracking branch 'origin/staging' into develop 2024-02-20 16:07:51 +01:00
Andrey Antukh
fd641e87c9 Merge remote-tracking branch 'origin/staging' into develop 2024-02-19 16:02:59 +01:00
Andrey Antukh
bec59ab3c2 Merge remote-tracking branch 'origin/staging' into develop 2024-02-16 14:30:20 +01:00
Andrey Antukh
b426db133d Merge remote-tracking branch 'origin/staging' into develop 2024-02-14 13:01:58 +01:00
Andrey Antukh
e232beeb59 Merge remote-tracking branch 'origin/staging' into develop 2024-02-12 16:13:55 +01:00
Andrey Antukh
dae277adb2 Merge remote-tracking branch 'origin/staging' into develop 2024-02-12 10:07:32 +01:00
Andrey Antukh
cdf8c5836a Merge remote-tracking branch 'origin/staging' into develop 2024-02-09 15:01:42 +01:00
Andrey Antukh
1743da7ebf Merge remote-tracking branch 'origin/staging' into develop 2024-02-09 14:57:48 +01:00
Andrey Antukh
cff44e34c3 Merge remote-tracking branch 'origin/staging' into develop 2024-02-09 09:53:13 +01:00
Andrey Antukh
740db82b6c Merge remote-tracking branch 'origin/staging' into develop 2024-02-08 09:10:06 +01:00
Andrey Antukh
4d7a572daa Merge remote-tracking branch 'origin/staging' into develop 2024-02-06 20:10:55 +01:00
Andrey Antukh
cb6db21e63 Merge remote-tracking branch 'origin/staging' into develop 2024-02-06 09:38:25 +01:00
Andrey Antukh
06033ea955 Merge remote-tracking branch 'origin/staging' into develop 2024-02-02 10:15:40 +01:00
Andrey Antukh
f15caf54dd Merge remote-tracking branch 'origin/staging' into develop 2024-01-31 11:05:20 +01:00
Andrey Antukh
94ee83a120 Merge remote-tracking branch 'origin/staging' into develop 2024-01-30 19:32:44 +01:00
Andrey Antukh
cb6e3a2e3c Merge remote-tracking branch 'origin/staging' into develop 2024-01-30 15:34:50 +01:00
Andrey Antukh
8abab982e7 Merge remote-tracking branch 'origin/staging' into develop 2024-01-25 23:26:55 +01:00
Andrey Antukh
6a412c75ce Merge remote-tracking branch 'origin/staging' into develop 2024-01-24 09:00:42 +01:00
Andrey Antukh
098e33bd98 Merge remote-tracking branch 'origin/staging' into develop 2024-01-19 14:44:14 +01:00
Andrey Antukh
a5d056f254 Merge remote-tracking branch 'origin/staging' into develop 2024-01-17 09:51:47 +01:00
Andrey Antukh
aa33bb1ebf Merge remote-tracking branch 'origin/staging' into develop 2024-01-16 00:12:55 +01:00
Andrey Antukh
8a81bc11e0 Merge remote-tracking branch 'origin/staging' into develop 2024-01-15 10:10:35 +01:00
Andrey Antukh
f0ea613d47 Merge remote-tracking branch 'origin/staging' into develop 2024-01-11 16:18:17 +01:00
Andrey Antukh
725501faf9 Merge remote-tracking branch 'origin/staging' into develop 2024-01-11 10:17:52 +01:00
Andrey Antukh
470cf08134 Merge remote-tracking branch 'origin/staging' into develop 2024-01-10 11:43:01 +01:00
Andrey Antukh
e48bfb5d94 Merge remote-tracking branch 'origin/staging' into develop 2024-01-09 09:56:55 +01:00
Andrey Antukh
326f18cb66 Merge remote-tracking branch 'origin/staging' into develop 2024-01-09 08:53:46 +01:00
Andrey Antukh
5983b9cd54 Merge remote-tracking branch 'origin/staging' into develop 2024-01-08 15:27:27 +01:00
Andrey Antukh
864881721f 🐛 Fix validation of validation error on file validate ns 2024-01-05 17:59:17 +01:00
Andrey Antukh
9aff12f3c6 Merge remote-tracking branch 'origin/staging' into develop 2024-01-05 09:46:53 +01:00
48 changed files with 1459 additions and 737 deletions

View File

@@ -1,5 +1,13 @@
# CHANGELOG
## 2.0.2
### :sparkles: Enhancements
- Fix locking contention on cron subsystem (causes backend start blocking)
- Fix locking contention on file object thumbails backend RPC calls
## 2.0.1
### :bug: Bugs fixed

124
README.md
View File

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

View File

@@ -3,8 +3,8 @@
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.12.0-alpha5"}
org.clojure/tools.namespace {:mvn/version "1.4.4"}
org.clojure/clojure {:mvn/version "1.12.0-alpha9"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.5-11"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "6.3.0.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "6.3.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti
@@ -26,13 +26,13 @@
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.909"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.925"}
metosin/reitit-core {:mvn/version "0.6.0"}
nrepl/nrepl {:mvn/version "1.1.0"}
cider/cider-nrepl {:mvn/version "0.44.0"}
nrepl/nrepl {:mvn/version "1.1.1"}
cider/cider-nrepl {:mvn/version "0.47.1"}
org.postgresql/postgresql {:mvn/version "42.7.1"}
org.xerial/sqlite-jdbc {:mvn/version "3.44.1.0"}
org.postgresql/postgresql {:mvn/version "42.7.3"}
org.xerial/sqlite-jdbc {:mvn/version "3.45.2.0"}
com.zaxxer/HikariCP {:mvn/version "5.1.0"}
@@ -54,7 +54,7 @@
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.1.5"}
markdown-clj/markdown-clj {:mvn/version "1.11.7"}
markdown-clj/markdown-clj {:mvn/version "1.12.1"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
@@ -74,7 +74,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
{io.github.clojure/tools.build {:git/tag "v0.10.0" :git/sha "3a2c484"}}
:ns-default build}
:test

View File

@@ -20,7 +20,6 @@
[app.http.client :as http]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc.commands.profile :as profile]
[app.setup :as-alias setup]
[app.tokens :as tokens]
@@ -283,12 +282,12 @@
(into [(keyword (:name provider) fitem)] (map keyword) items)))
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
[{:keys [::provider] :as cfg}]
(let [public (u/uri (cf/get :public-uri))]
(str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback")))))
(defn- build-auth-uri
[{:keys [provider] :as cfg} state]
[{:keys [::provider] :as cfg} state]
(let [params {:client_id (:client-id provider)
:redirect_uri (build-redirect-uri cfg)
:response_type "code"
@@ -299,15 +298,19 @@
(assoc :query query)
(str))))
(defn- qualify-prop-key
[provider k]
(keyword (:name provider) (name k)))
(defn- qualify-props
[provider props]
(reduce-kv (fn [result k v]
(assoc result (keyword (:name provider) (name k)) v))
(assoc result (qualify-prop-key provider k) v))
{}
props))
(defn fetch-access-token
[{:keys [provider] :as cfg} code]
(defn- fetch-access-token
[{:keys [::provider] :as cfg} code]
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code code
@@ -364,7 +367,7 @@
:props props})))
(defn- fetch-user-info
[{:keys [provider] :as cfg} tdata]
[{:keys [::provider] :as cfg} tdata]
(l/trace :hint "fetch user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata)))
@@ -389,7 +392,7 @@
(-> response :body json/decode)))
(defn- get-user-info
[{:keys [provider]} tdata]
[{:keys [::provider]} tdata]
(try
(when (:token/id tdata)
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
@@ -413,8 +416,8 @@
::fullname
::props]))
(defn get-info
[{:keys [provider ::setup/props] :as cfg} {:keys [params] :as request}]
(defn- get-info
[{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}]
(when-let [error (get params :error)]
(ex/raise :type :internal
:code :error-on-retrieving-code
@@ -472,82 +475,108 @@
(update :props merge (:props state)))))
(defn- get-profile
[{:keys [::db/pool] :as cfg} info]
(dm/with-open [conn (db/open pool)]
(some->> (:email info)
(profile/clean-email)
(profile/get-profile-by-email conn))))
[cfg info]
(db/run! cfg (fn [{:keys [::db/conn]}]
(some->> (:email info)
(profile/clean-email)
(profile/get-profile-by-email conn)))))
(defn- redirect-response
[uri]
{::rres/status 302
::rres/headers {"location" (str uri)}})
(defn- generate-error-redirect
[_ cause]
(let [data (if (ex/error? cause) (ex-data cause) nil)
code (or (:code data) :unexpected)
type (or (:type data) :internal)
hint (or (:hint data)
(if (ex/exception? cause)
(ex-message cause)
(str cause)))
(defn- redirect-with-error
([error] (redirect-with-error error nil))
([error hint]
(let [params {:error error :hint hint}
params (d/without-nils params)
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string params)))]
(redirect-response uri))))
params {:error "unable-to-auth"
:hint hint
:type type
:code code}
(defn- redirect-to-register
[cfg info]
(let [info (assoc info
:iss :prepared-register
:exp (dt/in-future {:hours 48}))
params {:token (tokens/generate (::setup/props cfg) info)
:fullname (:fullname info)}
params (d/without-nils params)]
(redirect-response
(-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params))))))
(defn- redirect-to-verify-token
[token]
(let [params {:token token}
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/login")
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string params)))]
(redirect-response uri)))
(defn- generate-redirect
(defn- provider-has-email-verified?
[{:keys [::provider] :as cfg} {:keys [props] :as info}]
(let [prop (qualify-prop-key provider :email_verified)]
(true? (get props prop))))
(defn- profile-has-provider-props?
[{:keys [::provider] :as cfg} profile]
(let [prop (qualify-prop-key provider :email)]
(contains? (:props profile) prop)))
(defn- provider-matches-profile?
[{:keys [::provider] :as cfg} profile info]
(or (= (:auth-backend profile) (:name provider))
(profile-has-provider-props? cfg profile)
(provider-has-email-verified? cfg info)))
(defn- process-callback
[cfg request info profile]
(if profile
(let [sxf (session/create-fn cfg (:id profile))
token (or (:invitation-token info)
(tokens/generate (::setup/props cfg)
{:iss :auth
:exp (dt/in-future "15m")
:profile-id (:id profile)}))
params {:token token}
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string params)))]
(cond
(some? profile)
(cond
(:is-blocked profile)
(redirect-with-error "profile-blocked")
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(not (provider-matches-profile? cfg profile info))
(redirect-with-error "auth-provider-not-allowed")
(audit/submit! cfg {::audit/type "command"
::audit/name "login-with-oidc"
::audit/profile-id (:id profile)
::audit/ip-addr (audit/parse-client-ip request)
::audit/props (audit/profile->props profile)})
(not (:is-active profile))
(let [info (assoc info :profile-id (:id profile))]
(redirect-to-register cfg info))
(->> (redirect-response uri)
(sxf request)))
:else
(let [sxf (session/create-fn cfg (:id profile))
token (or (:invitation-token info)
(tokens/generate (::setup/props cfg)
{:iss :auth
:exp (dt/in-future "15m")
:props (:props info)
:profile-id (:id profile)}))]
(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)})
(if (auth/email-domain-in-whitelist? (:email info))
(let [info (assoc info
:iss :prepared-register
:is-active true
:exp (dt/in-future {:hours 48}))
token (tokens/generate (::setup/props cfg) info)
params (d/without-nils
{:token token
:fullname (:fullname info)})
uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))]
(->> (redirect-to-verify-token token)
(sxf request))))
(redirect-response uri))
(generate-error-redirect cfg "email-domain-not-allowed"))))
(not (auth/email-domain-in-whitelist? (:email info)))
(redirect-with-error "email-domain-not-allowed")
:else
(let [info (assoc info :is-active (provider-has-email-verified? cfg info))]
(if (contains? cf/flags :registration)
(redirect-to-register cfg info)
(redirect-with-error "registration-disabled")))))
(defn- auth-handler
[cfg {:keys [params] :as request}]
@@ -566,10 +595,10 @@
(try
(let [info (get-info cfg request)
profile (get-profile cfg info)]
(generate-redirect cfg request info profile))
(process-callback cfg request info profile))
(catch Throwable cause
(l/warn :hint "error on oauth process" :cause cause)
(generate-error-redirect cfg cause))))
(l/err :hint "error on oauth process" :cause cause)
(redirect-with-error "unable-to-auth" (ex-message cause)))))
(def provider-lookup
{:compile
@@ -578,13 +607,12 @@
(fn [request]
(let [provider (some-> request :path-params :provider keyword)]
(if-let [provider (get providers provider)]
(handler (assoc cfg :provider provider) request)
(handler (assoc cfg ::provider provider) request)
(ex/raise :type :restriction
:code :provider-not-configured
:provider provider
:hint "provider not configured"))))))})
(s/def ::client-id ::cf/oidc-client-id)
(s/def ::client-secret ::cf/oidc-client-secret)
(s/def ::base-uri ::cf/oidc-base-uri)
@@ -597,7 +625,6 @@
(s/def ::email-attr ::cf/oidc-email-attr)
(s/def ::name-attr ::cf/oidc-name-attr)
;; FIXME: migrate to qualified-keywords
(s/def ::provider
(s/keys :req-un [::client-id
::client-secret]

View File

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

View File

@@ -150,8 +150,9 @@
(clean-props))
token-id (::actoken/id request)
context (d/without-nils
{:access-token-id (some-> token-id str)})]
context (-> (::context resultm)
(assoc :access-token-id (some-> token-id str))
(d/without-nils))]
{::type (or (::type resultm)
(::rpc/type cfg))

View File

@@ -349,6 +349,8 @@
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
:object-update
(ig/ref :app.tasks.object-update/handler)
:process-webhook-event
(ig/ref ::webhooks/process-event-handler)
:run-webhook
@@ -376,7 +378,10 @@
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.orphan-teams-gc/handler
{::db/pool (ig/ref ::db/pool)}
{::db/pool (ig/ref ::db/pool)}
:app.tasks.object-update/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.file-gc/handler
{::db/pool (ig/ref ::db/pool)

View File

@@ -19,7 +19,6 @@
[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-alias climit]
[app.rpc.commands.profile :as profile]
@@ -38,6 +37,14 @@
(def schema:token
[::sm/word-string {:max 6000}])
(def ^:private default-verify-threshold
(dt/duration "15m"))
(defn- elapsed-verify-threshold?
[profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
(pos? (compare elapsed default-verify-threshold))))
;; ---- COMMAND: login with password
(defn login-with-password
@@ -139,7 +146,7 @@
(update-password [conn profile-id]
(let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd} {:id profile-id})
(db/update! conn :profile {:password pwd :is-active true} {:id profile-id})
nil))]
(db/with-atomic [conn pool]
@@ -162,8 +169,8 @@
;; ---- COMMAND: Prepare Register
(defn validate-register-attempt!
[{:keys [::db/pool] :as cfg} params]
(defn- validate-register-attempt!
[cfg params]
(when-not (contains? cf/flags :registration)
(when-not (contains? params :invitation-token)
@@ -171,7 +178,9 @@
:code :registration-disabled)))
(when (contains? params :invitation-token)
(let [invitation (tokens/verify (::setup/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
(let [invitation (tokens/verify (::setup/props cfg)
{:token (:invitation-token params)
:iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
@@ -181,13 +190,6 @@
(ex/raise :type :validation
:code :email-domain-is-not-allowed))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spammer.
(when (eml/has-bounce-reports? pool (:email params))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email has one or many bounces reported"))
;; Perform a basic validation of email & password
(when (= (str/lower (:email params))
(str/lower (:password params)))
@@ -195,35 +197,13 @@
:code :email-as-password
:hint "you can't use your email as password")))
(def register-retry-threshold
(dt/duration "15m"))
(defn- elapsed-register-retry-threshold?
[profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
(pos? (compare elapsed register-retry-threshold))))
(defn prepare-register
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
(validate-register-attempt! cfg params)
(let [email (profile/clean-email email)
profile (when-let [profile (profile/get-profile-by-email pool email)]
(cond
(:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked)
(and (not (:is-active profile))
(elapsed-register-retry-threshold? profile))
profile
:else
(ex/raise :type :validation
:code :email-already-exists
:hint "profile already exists")))
profile (profile/get-profile-by-email pool email)
params {:email email
:password (:password params)
:invitation-token (:invitation-token params)
@@ -233,7 +213,6 @@
:exp (dt/in-future {:days 7})}
params (d/without-nils params)
token (tokens/generate (::setup/props cfg) params)]
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
@@ -317,17 +296,16 @@
{::db/return-keys true})
(profile/decode-row))))
(defn send-email-verification!
[conn props profile]
(let [vtoken (tokens/generate props
[{:keys [::db/conn] :as cfg} profile]
(let [vtoken (tokens/generate (::setup/props cfg)
{:iss :verify-email
:exp (dt/in-future "72h")
:profile-id (:id profile)
:email (:email profile)})
;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook
ptoken (tokens/generate props
ptoken (tokens/generate (::setup/props cfg)
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
@@ -346,69 +324,94 @@
(into params)
(assoc :fullname fullname))
is-active (or (:is-active params)
(not (contains? cf/flags :email-verification)))
profile (if-let [profile-id (:profile-id claims)]
(profile/get-profile conn profile-id)
(let [params (-> params
(assoc :is-active is-active)
(update :password #(profile/derive-password cfg %)))]
(let [is-active (or (boolean (:is-active params))
(not (contains? cf/flags :email-verification)))
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 (::setup/props cfg) {:token token :iss :team-invitation}))]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
props (audit/profile->props profile)]
;; If profile is filled in claims, means it tries to register
;; again, so we proceed to update the modified-at attr
;; accordingly.
(when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
(audit/submit! cfg
{::audit/type "fact"
::audit/name "register-profile-retry"
::audit/profile-id id}))
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged). This happens
;; only if the invitation email matches with the register
;; email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
;; When profile is blocked, we just ignore it and return plain data
(:is-blocked profile)
(do
(l/wrn :hint "register attempt for already blocked profile"
:profile-id (str (:id profile))
:profile-email (:email profile))
(rph/with-meta {:email (:email profile)}
{::audit/replace-props props
::audit/context {:action "ignore-because-blocked"}
::audit/profile-id (:id profile)
::audit/name "register-profile-retry"}))
;; If invitation token comes in params, this is because the user
;; comes from team-invitation process; in this case, regenerate
;; token and send back to the user a new invitation token (and
;; mark current session as logged). This happens only if the
;; invitation email matches with the register email.
(and (some? invitation)
(= (:email profile)
(:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens/generate (::setup/props cfg) claims)
resp {:invitation-token token}]
(-> resp
token (tokens/generate (::setup/props cfg) claims)]
(-> {:invitation-token token}
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
(rph/with-meta {::audit/replace-props props
::audit/context {:action "accept-invitation"}
::audit/profile-id (:id profile)})))
;; If auth backend is different from "penpot" means user is
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
;; When a new user is created and it is already activated by
;; configuration or specified by OIDC, we just mark the profile
;; as logged-in
(not (:profile-id claims))
(if (:is-active claims)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta
{::audit/replace-props props
::audit/context {:action "login"}
::audit/profile-id (:id profile)}))
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
(do
(send-email-verification! cfg profile)
(rph/with-meta {:email (:email profile)}
{::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)})))
;; In all other cases, send a verification email.
:else
(do
(send-email-verification! conn (::setup/props cfg) profile)
(rph/with-meta profile
(let [elapsed? (elapsed-verify-threshold? profile)
bounce? (eml/has-bounce-reports? conn (:email profile))
action (if bounce?
"ignore-because-bounce"
(if elapsed?
"resend-email-verification"
"ignore"))]
(l/wrn :hint "repeated registry detected"
:profile-id (str (:id profile))
:profile-email (:email profile)
:context-action action)
(when (= action "resend-email-verification")
(db/update! conn :profile
{:modified-at (dt/now)}
{:id (:id profile)})
(send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)}
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
::audit/context {:action action}
::audit/profile-id (:id profile)
::audit/name "register-profile-retry"})))))
(def schema:register-profile
[:map {:title "register-profile"}
@@ -427,7 +430,7 @@
;; ---- COMMAND: Request Profile Recovery
(defn request-profile-recovery
(defn- request-profile-recovery
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate (::setup/props cfg)
@@ -451,26 +454,38 @@
nil))]
(db/with-atomic [conn pool]
(when-let [profile (->> (profile/clean-email email)
(profile/get-profile-by-email conn))]
(when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(let [profile (->> (profile/clean-email email)
(profile/get-profile-by-email conn))]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :profile-not-verified
:hint "the user need to validate profile before recover password"))
(cond
(not profile)
(l/wrn :hint "attempt of profile recovery: no profile found"
:profile-email email)
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(not (eml/allow-send-emails? conn profile))
(l/wrn :hint "attempt of profile recovery: profile is muted"
:profile-id (str (:id profile))
:profile-email (:email profile))
(->> profile
(create-recovery-token)
(send-email-notification conn))))))
(eml/has-bounce-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: email has bounces"
:profile-id (str (:id profile))
:profile-email (:email profile))
(not (elapsed-verify-threshold? profile))
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
:profile-id (str (:id profile))
:profile-email (:email profile))
:else
(do
(db/update! conn :profile
{:modified-at (dt/now)}
{:id (:id profile)})
(->> profile
(create-recovery-token)
(send-email-notification conn))))))))
(def schema:request-profile-recovery

View File

@@ -228,51 +228,52 @@
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMAND: create-file-object-thumbnail
(def sql:get-file-object-thumbnail
"SELECT * FROM file_tagged_object_thumbnail
WHERE file_id = ? AND object_id = ? AND tag = ?
FOR UPDATE")
(defn- create-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id media tag]
(def sql:create-file-object-thumbnail
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (file_id, object_id, tag)
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
RETURNING *")
(let [thumb (db/get* conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:tag tag}
{::db/remove-deleted false
::sql/for-update true})
path (:path media)
(defn- persist-thumbnail!
[storage media created-at]
(let [path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
tnow (dt/now)
(sto/wrap-with-hash hash))]
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? true
::sto/touched-at tnow
:content-type mtype
:bucket "file-object-thumbnail"})]
(sto/put-object! storage
{::sto/content data
::sto/deduplicate? true
::sto/touched-at created-at
:content-type mtype
:bucket "file-object-thumbnail"})))
(if (some? thumb)
(do
;; We mark the old media id as touched if it does not matches
(when (not= (:id media) (:media-id thumb))
(sto/touch-object! storage (:media-id thumb)))
(db/update! conn :file-tagged-object-thumbnail
{:media-id (:id media)
:deleted-at nil
:updated-at tnow}
{:file-id file-id
:object-id object-id
:tag tag}))
(db/insert! conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:created-at tnow
:updated-at tnow
:tag tag
:media-id (:id media)}))))
(defn- create-file-object-thumbnail!
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
(let [tsnow (dt/now)
media (persist-thumbnail! storage media tsnow)
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
file-id object-id tag (:id media)
tsnow (:id media)])]
[th1 th2])))]
(when (and (some? th1)
(not= (:media-id th1)
(:media-id th2)))
(sto/touch-object! storage (:media-id th1) :async true))
th2))
(def ^:private
schema:create-file-object-thumbnail
@@ -296,16 +297,10 @@
(media/validate-media-type! media)
(media/validate-media-size! media)
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [cfg (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 5)
(assoc ::rtry/label "create-file-object-thumbnail"))]
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))))))
(db/run! cfg files/check-edition-permissions! profile-id file-id)
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))
;; --- MUTATION COMMAND: delete-file-object-thumbnail

View File

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

View File

@@ -46,6 +46,10 @@
(let [email (str/lower email)
email (if (str/starts-with? email "mailto:")
(subs email 7)
email)
email (if (or (str/starts-with? email "<")
(str/ends-with? email ">"))
(str/trim email "<>")
email)]
email))
@@ -87,8 +91,8 @@
(defn get-profile
"Get profile by id. Throws not-found exception if no profile found."
[conn id & {:as attrs}]
(-> (db/get-by-id conn :profile id attrs)
[conn id & {:as opts}]
(-> (db/get-by-id conn :profile id opts)
(decode-row)))
;; --- MUTATION: Update Profile (own)

View File

@@ -9,6 +9,7 @@
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
@@ -82,8 +83,16 @@
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)]
(assoc claims :profile profile)))
(let [profile (profile/get-profile conn profile-id {::sql/for-update true})
props (merge (:props profile)
(:props claims))]
(when (not= props (:props profile))
(db/update! conn :profile
{:props (db/tjson props)}
{:id profile-id}))
(let [profile (assoc profile :props props)]
(assoc claims :profile profile))))
;; --- Team Invitation

View File

@@ -38,6 +38,11 @@
team (-> (db/get conn :team {:id (:team-id project)})
(teams/decode-row))
members (into #{} (->> (teams/get-team-members conn (:team-id project))
(map :id)))
perms (assoc perms :in-team (contains? members profile-id))
_ (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file)))

View File

@@ -86,13 +86,11 @@
(defn resend-email-verification-email!
[email]
(let [sprops (:app.setup/props main/system)
pool (:app.db/pool main/system)
email (profile/clean-email email)
profile (profile/get-profile-by-email pool email)]
(auth/send-email-verification! pool sprops profile)
:email-sent))
(db/tx-run! main/system
(fn [{:keys [::db/conn] :as cfg}]
(let [email (profile/clean-email email)
profile (profile/get-profile-by-email conn email)]
(#'auth/send-email-verification! cfg profile)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROFILES MANAGEMENT

View File

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

View File

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

View File

@@ -119,11 +119,13 @@
:next.jdbc/update-count))]
(l/trc :hint "submit task"
:name task
:task-id (str id)
:queue queue
:label label
:dedupe (boolean dedupe)
:deleted (or deleted 0)
:in (dt/format-duration duration))
:delay (dt/format-duration duration)
:replace (or deleted 0))
(db/exec-one! conn [sql:insert-new-task id task props queue
label priority max-retries interval])

View File

@@ -27,14 +27,15 @@
"insert into scheduled_task (id, cron_expr)
values (?, ?)
on conflict (id)
do update set cron_expr=?")
do nothing")
(defn- synchronize-cron-entries!
[{:keys [::db/pool ::entries]}]
(db/with-atomic [conn pool]
(doseq [{:keys [id cron]} entries]
(l/trc :hint "register cron task" :id id :cron (str cron))
(db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)]))))
[{:keys [::db/conn ::entries]}]
(doseq [{:keys [id cron]} entries]
(let [result (db/exec-one! conn [sql:upsert-cron-task id (str cron)])
updated? (pos? (db/get-update-count result))]
(l/dbg :hint "register task" :id id :cron (str cron)
:status (if updated? "created" "exists")))))
(defn- lock-scheduled-task!
[conn id]
@@ -45,7 +46,7 @@
(declare ^:private schedule-cron-task)
(defn- execute-cron-task
[cfg {:keys [id] :as task}]
[cfg {:keys [id cron] :as task}]
(px/thread
{:name (str "penpot/cron-task/" id)}
(let [tpoint (dt/tpoint)]
@@ -54,20 +55,25 @@
(db/exec-one! conn ["SET LOCAL statement_timeout=0;"])
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout=0;"])
(when (lock-scheduled-task! conn id)
(l/dbg :hint "start task" :task-id id)
(db/update! conn :scheduled-task
{:cron-expr (str cron)
:modified-at (dt/now)}
{:id id}
{::db/return-keys false})
(l/dbg :hint "start" :id id)
((:fn task) task)
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "end task" :task-id id :elapsed elapsed)))))
(l/dbg :hint "end" :id id :elapsed elapsed)))))
(catch InterruptedException _
(let [elapsed (dt/format-duration (tpoint))]
(l/debug :hint "task interrupted" :task-id id :elapsed elapsed)))
(l/debug :hint "task interrupted" :id id :elapsed elapsed)))
(catch Throwable cause
(let [elapsed (dt/format-duration (tpoint))]
(binding [l/*context* (get-error-context cause task)]
(l/err :hint "unhandled exception on running task"
:task-id id
:id id
:elapsed elapsed
:cause cause))))
(finally
@@ -86,7 +92,7 @@
(let [ts (ms-until-valid cron)
ft (px/schedule! ts (partial execute-cron-task cfg task))]
(l/dbg :hint "schedule task" :task-id id
(l/dbg :hint "schedule" :id id
:ts (dt/format-duration ts)
:at (dt/format-instant (dt/in-future ts)))
@@ -135,7 +141,8 @@
cfg (assoc cfg ::entries entries ::running running)]
(l/inf :hint "started" :tasks (count entries))
(synchronize-cron-entries! cfg)
(db/tx-run! cfg synchronize-cron-entries!)
(->> (filter some? entries)
(run! (partial schedule-cron-task cfg)))

View File

@@ -139,7 +139,7 @@
:else
(try
(l/trc :hint "start task"
(l/dbg :hint "start"
:name (:name task)
:task-id (str task-id)
:queue queue
@@ -149,7 +149,7 @@
result (handle-task task)
elapsed (dt/format-duration (tpoint))]
(l/trc :hint "end task"
(l/dbg :hint "end"
:name (:name task)
:task-id (str task-id)
:queue queue
@@ -228,9 +228,9 @@
(recur))))
(catch InterruptedException _
(l/debug :hint "interrupted"
:id id
:queue queue))
(l/dbg :hint "interrupted"
:id id
:queue queue))
(catch Throwable cause
(l/err :hint "unexpected exception"
:id id

View File

@@ -1158,7 +1158,7 @@
;; check that the unknown frame thumbnail is deleted
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
(t/is (= 2 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
(t/is (= 1 (count (remove :deleted-at rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 3 (:processed res))))

View File

@@ -277,8 +277,6 @@
(t/is (thrown? org.postgresql.util.PSQLException
(th/db-delete! :storage-object {:id (:media-id row1)}))))))
(t/deftest get-file-object-thumbnail
(let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1)
@@ -317,3 +315,44 @@
(let [result (:result out)]
(t/is (contains? result "test-key-2"))))))
(t/deftest create-file-object-thumbnail
(th/db-delete! :task {:name "object-update"})
(let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
data {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id "test-key-2"
:media {:filename "sample.jpg"
:mtype "image/jpeg"}}]
(let [data (update data :media
(fn [media]
(-> media
(assoc :path (th/tempfile "backend_tests/test_files/sample2.jpg"))
(assoc :size 7923))))
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (map? (:result out))))
(let [data (update data :media
(fn [media]
(-> media
(assoc :path (th/tempfile "backend_tests/test_files/sample.jpg"))
(assoc :size 312043))))
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (map? (:result out))))
(let [[row1 :as rows]
(->> (th/db-query :task {:name "object-update"})
(map #(update % :props db/decode-transit-pgobject)))]
;; (app.common.pprint/pprint rows)
(t/is (= 1 (count rows)))
(t/is (> (inst-ms (dt/diff (:created-at row1) (:scheduled-at row1)))
(inst-ms (dt/duration "4m")))))))

View File

@@ -27,6 +27,14 @@
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest clean-email
(t/is "foo@example.com" (profile/clean-email "mailto:foo@example.com"))
(t/is "foo@example.com" (profile/clean-email "mailto:<foo@example.com>"))
(t/is "foo@example.com" (profile/clean-email "<foo@example.com>"))
(t/is "foo@example.com" (profile/clean-email "foo@example.com>"))
(t/is "foo@example.com" (profile/clean-email "<foo@example.com")))
;; Test with wrong credentials
(t/deftest profile-login-failed-1
(let [profile (th/create-profile* 1)
@@ -221,20 +229,51 @@
(t/is (= "mtma" (:penpot/mtm-campaign props)))))))
(t/deftest prepare-register-and-register-profile-2
(with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)]
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [current-token (atom nil)]
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [current-token (atom nil)]
;; PREPARE REGISTER
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
;; PREPARE REGISTER
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (string? token))
(reset! current-token token))
;; DO REGISTRATION
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 1 (:call-count @mock))))
;; DO REGISTRATION: try correct register attempt 1
(th/reset-mock! mock)
;; PREPARE REGISTER: second attempt
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
;; DO REGISTRATION: second attempt
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 0 (:call-count @mock))))
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
;; DO REGISTRATION: third attempt
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
@@ -242,44 +281,56 @@
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 1 (:call-count @mock))))
(t/is (= 1 (:call-count @mock))))))))
(th/reset-mock! mock)
(t/deftest prepare-register-and-register-profile-3
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [current-token (atom nil)]
;; PREPARE REGISTER
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
;; PREPARE REGISTER without waiting for threshold
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (= :validation (-> out :error th/ex-type)))
(t/is (= :email-already-exists (-> out :error th/ex-code))))
;; DO REGISTRATION
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (nil? (:error out)))
(t/is (= 1 (:call-count @mock))))
(th/sleep {:millis 500})
(th/reset-mock! mock)
(th/reset-mock! mock)
;; PREPARE REGISTER waiting the threshold
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)]
(th/db-update! :profile
{:is-blocked true}
{:email "hello@example.com"})
(t/is (th/success? out))
(t/is (= 0 (:call-count @mock)))
;; PREPARE REGISTER: second attempt
(let [data {::th/type :prepare-register-profile
:email "hello@example.com"
:password "foobar"}
out (th/command! data)
token (get-in out [:result :token])]
(t/is (th/success? out))
(reset! current-token token))
(let [result (:result out)]
(t/is (contains? result :token))
(reset! current-token (:token result))))
;; DO REGISTRATION: try correct register attempt 1
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
;; DO REGISTRATION: second attempt
(let [data {::th/type :register-profile
:token @current-token
:fullname "foobar"
:accept-terms-and-privacy true
:accept-newsletter-subscription true}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= 1 (:call-count @mock))))))))
(t/is (nil? (:error out)))
(t/is (= 0 (:call-count @mock))))))))
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
@@ -351,13 +402,13 @@
:email (:email profile)
:password "foobar"}
out (th/command! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token)))))
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :email-already-exists (:code edata))))))
(t/deftest prepare-register-profile-with-bounced-email
(t/deftest register-profile-with-bounced-email
(let [pool (:app.db/pool th/*system*)
data {::th/type :prepare-register-profile
:email "user@example.com"
@@ -366,10 +417,9 @@
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
(let [out (th/command! data)]
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))))
(t/is (th/success? out))
(let [result (:result out)]
(t/is (contains? result :token))))))
(t/deftest register-profile-with-complained-email
(let [pool (:app.db/pool th/*system*)
@@ -447,7 +497,7 @@
(t/deftest request-profile-recovery
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1)
(let [profile1 (th/create-profile* 1 {:is-active false})
profile2 (th/create-profile* 2 {:is-active true})
pool (:app.db/pool th/*system*)
data {::th/type :request-profile-recovery}]
@@ -460,38 +510,47 @@
;; with valid email inactive user
(let [data (assoc data :email (:email profile1))
out (th/command! data)
error (:error out)]
out (th/command! data)]
(t/is (= 0 (:call-count @mock)))
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :profile-not-verified)))
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
;; with valid email inactive user
(let [data (assoc data :email (:email profile1))
out (th/command! data)]
(t/is (= 1 (:call-count @mock)))
(t/is (nil? (:result out)))
(t/is (nil? (:error out)))))
(th/reset-mock! mock)
;; with valid email and active user
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 1 (:call-count @mock))))
(with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold?
:return true}]
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 1 (:call-count @mock))))
;; with valid email and active user with global complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock))))
;; with valid email and active user with global complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock))))
;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)
error (:error out)]
;; (th/print-result! out)
(t/is (= 2 (:call-count @mock)))
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-has-permanent-bounces))))))
;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
(let [data (assoc data :email (:email profile2))
out (th/command! data)]
(t/is (nil? (:result out)))
(t/is (nil? (:error out)))
;; (th/print-result! out)
(t/is (= 2 (:call-count @mock))))))))
(t/deftest update-profile-password

View File

@@ -1,5 +1,5 @@
{:deps
{org.clojure/clojure {:mvn/version "1.11.1"}
{org.clojure/clojure {:mvn/version "1.11.2"}
org.clojure/data.json {:mvn/version "2.5.0"}
org.clojure/tools.cli {:mvn/version "1.0.219"}
org.clojure/clojurescript {:mvn/version "1.11.132"}
@@ -7,12 +7,12 @@
org.clojure/data.fressian {:mvn/version "1.0.0"}
;; Logging
org.apache.logging.log4j/log4j-api {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.22.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.22.1"}
org.slf4j/slf4j-api {:mvn/version "2.0.10"}
org.apache.logging.log4j/log4j-api {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-web {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.23.1"}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.23.1"}
org.slf4j/slf4j-api {:mvn/version "2.0.12"}
pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"}
selmer/selmer {:mvn/version "1.12.59"}
@@ -28,7 +28,7 @@
integrant/integrant {:mvn/version "0.8.1"}
org.apache.commons/commons-pool2 {:mvn/version "2.12.0"}
org.graalvm.js/js {:mvn/version "23.0.2"}
org.graalvm.js/js {:mvn/version "23.0.3"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2023.11.09-407"}
@@ -41,7 +41,7 @@
:git/tag "3.0.0"
:git/url "https://github.com/funcool/datoteka"}
lambdaisland/uri {:mvn/version "1.16.134"
lambdaisland/uri {:mvn/version "1.19.155"
:exclusions [org.clojure/data.json]}
frankiesardo/linked {:mvn/version "1.3.0"}
@@ -72,7 +72,7 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}}
{io.github.clojure/tools.build {:git/tag "v0.10.0" :git/sha "3a2c484"}}
:ns-default build}
:test

View File

@@ -5,8 +5,8 @@ ARG DEBIAN_FRONTEND=noninteractive
ENV NODE_VERSION=v20.11.1 \
CLOJURE_VERSION=1.11.1.1435 \
CLJKONDO_VERSION=2024.02.12 \
BABASHKA_VERSION=1.3.188 \
CLJKONDO_VERSION=2024.03.13 \
BABASHKA_VERSION=1.3.189 \
CLJFMT_VERSION=0.12.0 \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8

View File

@@ -224,7 +224,7 @@ http {
add_header Last-Modified $date_gmt;
add_header Cache-Control "no-store, no-cache, max-age=0" always;
if_modified_since off;
try_files $uri /index.html$is_args$args =404;
try_files $uri /index.html$is_args$args /index.html =404;
}
}
}

View File

@@ -6,15 +6,14 @@ cd ~;
source ~/.bashrc
set -e;
echo "[start-tmux.sh] Installing node dependencies"
pushd ~/penpot/frontend/
yarn install
yarn run playwright install --with-deps chromium
popd
pushd ~/penpot/exporter/
yarn install
npx playwright install chromium
yarn run playwright install --with-deps chromium
popd
tmux -2 new-session -d -s penpot

View File

@@ -202,7 +202,7 @@ http {
add_header Last-Modified $date_gmt;
add_header Cache-Control "no-store, no-cache, max-age=0" always;
if_modified_since off;
try_files $uri /index.html$is_args$args =404;
try_files $uri /index.html$is_args$args /index.html =404;
}
}
}

View File

@@ -15,7 +15,7 @@
:dev
{:extra-deps
{thheller/shadow-cljs {:mvn/version "2.27.4"}}}
{thheller/shadow-cljs {:mvn/version "2.28.3"}}}
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}

View File

@@ -10,19 +10,19 @@
"url": "https://github.com/penpot/penpot"
},
"dependencies": {
"archiver": "^6.0.1",
"archiver": "^6.0.2",
"cookies": "^0.9.1",
"generic-pool": "^3.9.0",
"inflation": "^2.1.0",
"ioredis": "^5.3.2",
"luxon": "^3.4.4",
"playwright": "^1.40.1",
"playwright": "^1.43.0",
"raw-body": "^2.5.2",
"xml-js": "^1.6.11",
"xregexp": "^5.1.1"
},
"devDependencies": {
"shadow-cljs": "2.27.4",
"shadow-cljs": "2.28.3",
"source-map-support": "^0.5.21"
},
"scripts": {

View File

@@ -135,9 +135,9 @@ __metadata:
languageName: node
linkType: hard
"archiver@npm:^6.0.1":
version: 6.0.1
resolution: "archiver@npm:6.0.1"
"archiver@npm:^6.0.2":
version: 6.0.2
resolution: "archiver@npm:6.0.2"
dependencies:
archiver-utils: "npm:^4.0.1"
async: "npm:^3.2.4"
@@ -146,7 +146,7 @@ __metadata:
readdir-glob: "npm:^1.1.2"
tar-stream: "npm:^3.0.0"
zip-stream: "npm:^5.0.1"
checksum: 54c5a634b39691114e727d4b4f360439fa7cd40b414c9d909606fbfd7048037f7dccefa49337f9ed19b1f5c209e021ce5e1ff9c6b547907257bc71f1af6f8cf3
checksum: 23a470d468c01cd40fc13b6bd3dbc6d04c4f7b770785dcc7e1e4af256c3d79c4ffd7f7e0e84ae320437e5b8d0a2117aecfca0586b8c0fbd6edc3e04977c438cc
languageName: node
linkType: hard
@@ -727,15 +727,15 @@ __metadata:
version: 0.0.0-use.local
resolution: "exporter@workspace:."
dependencies:
archiver: "npm:^6.0.1"
archiver: "npm:^6.0.2"
cookies: "npm:^0.9.1"
generic-pool: "npm:^3.9.0"
inflation: "npm:^2.1.0"
ioredis: "npm:^5.3.2"
luxon: "npm:^3.4.4"
playwright: "npm:^1.40.1"
playwright: "npm:^1.43.0"
raw-body: "npm:^2.5.2"
shadow-cljs: "npm:2.27.4"
shadow-cljs: "npm:2.28.3"
source-map-support: "npm:^0.5.21"
xml-js: "npm:^1.6.11"
xregexp: "npm:^5.1.1"
@@ -1546,27 +1546,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.40.1":
version: 1.40.1
resolution: "playwright-core@npm:1.40.1"
"playwright-core@npm:1.43.0":
version: 1.43.0
resolution: "playwright-core@npm:1.43.0"
bin:
playwright-core: cli.js
checksum: 56c283012974982313a6ae583b975ee4af76d52059fb9a25d9cc616a11224685ec64682b391910c795d2b12d2ab5c7eec31124722c75c0b1703a76ac9b6fd1c2
checksum: 3afdcc79176e0c872b3bb66994ef6288b10819dfb800234b81134a35daffc8100596d65ccff10faa8941a450a259a78f446db570898e53031abba7f542602c6a
languageName: node
linkType: hard
"playwright@npm:^1.40.1":
version: 1.40.1
resolution: "playwright@npm:1.40.1"
"playwright@npm:^1.43.0":
version: 1.43.0
resolution: "playwright@npm:1.43.0"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.40.1"
playwright-core: "npm:1.43.0"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 5dae164d1f69162da8d7eee52da651296fb885c76a8b36049f216975c751a0a826ff05795a1c0902dc0bd193fe606ae17d5def655f4cbcccb8d8b71afb74b950
checksum: 82506d3937416439d42ed7940e03b62225651a049792b7c122cd1fb4318674e5789dbcd38e31c8e2f41d99e3b7dc9aa0898bfdd26dff5456eb3f89a166252bd8
languageName: node
linkType: hard
@@ -1842,9 +1842,9 @@ __metadata:
languageName: node
linkType: hard
"shadow-cljs@npm:2.27.4":
version: 2.27.4
resolution: "shadow-cljs@npm:2.27.4"
"shadow-cljs@npm:2.28.3":
version: 2.28.3
resolution: "shadow-cljs@npm:2.28.3"
dependencies:
node-libs-browser: "npm:^2.2.1"
readline-sync: "npm:^1.4.7"
@@ -1854,7 +1854,7 @@ __metadata:
ws: "npm:^7.4.6"
bin:
shadow-cljs: cli/runner.js
checksum: bae23e71df9c2b2979259a0cde8747c923ee295f58ab4637c9d6b103d82542b40ef39172d4be2dbb94af2e6458a177d1ec96c1eb1e73b1d8f3a4ddb5eaaba7d4
checksum: 623b536a0d95d7696dd465c09ab3cb5d921c867a577a33463ad58dbc40f51f5d0424ba2791a8803f33a94f5c877198de91c3c7f7616618a6b4ae90e80d5d213e
languageName: node
linkType: hard

5
frontend/.gitignore vendored
View File

@@ -5,3 +5,8 @@
!.yarn/releases
!.yarn/sdks
!.yarn/versions
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -30,11 +30,14 @@
"translations:find-unused": "node ./scripts/find-unused-translations.js",
"compile": "node ./scripts/compile.js",
"watch": "node ./scripts/watch.js",
"e2e:server": "NODE_NO_WARNINGS=1 http-server ./resources/public -p 3500 -a 0.0.0.0",
"e2e:test": "playwright test",
"storybook:compile": "gulp template:storybook && clojure -M:dev:shadow-cljs compile storybook",
"storybook:watch": "npm run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\"",
"storybook:build": "npm run storybook:compile && storybook build"
},
"devDependencies": {
"@playwright/test": "^1.42.1",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-interactions": "^7.6.17",
"@storybook/addon-links": "^7.6.17",
@@ -59,6 +62,7 @@
"gulp-sass": "^5.1.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^2.0.3",
"http-server": "^14.1.1",
"jsdom": "^24.0.0",
"map-stream": "0.0.7",
"marked": "^12.0.0",

View File

@@ -0,0 +1,47 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
testDir: "./playwright",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://0.0.0.0:3500",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer: {
command: "yarn e2e:server",
url: "http://0.0.0.0:3500",
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -0,0 +1,14 @@
import { test, expect } from "@playwright/test";
test("has title", async ({ page }) => {
await page.route("**/api/rpc/command/get-profile", (route) => {
route.fulfill({
status: 200,
contentType: "application/transit+json",
path: "playwright/fixtures/get-profile-anonymous.json",
});
});
await page.goto("/");
await expect(page).toHaveTitle(/Penpot/);
});

View File

@@ -0,0 +1,4 @@
{
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:fullname": "Anonymous User"
}

View File

@@ -253,7 +253,7 @@
(export [_]
(->> (export-file file)
(rx/subs
(rx/subs!
(fn [value]
(when (not (contains? value :type))
(let [[file export-blob] value]

View File

@@ -15,10 +15,11 @@
[app.config :as cf]
[app.main.data.events :as ev]
[app.main.data.media :as di]
[app.main.data.messages :as msg]
[app.main.data.websocket :as ws]
[app.main.features :as features]
[app.main.repo :as rp]
[app.util.i18n :as i18n]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :refer [storage]]
[beicon.v2.core :as rx]
@@ -231,8 +232,35 @@
(rx/observe-on :async)))))
(rx/catch on-error))))))
(def ^:private schema:login-with-ldap
(sm/define
[:map
[:email ::sm/email]
[:password :string]]))
(defn login-with-ldap
[params]
(dm/assert!
"expected valid params"
(sm/check! schema:login-with-ldap params))
(ptk/reify ::login-with-ldap
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-error on-success]
:or {on-error rx/throw
on-success identity}} (meta params)]
(->> (rp/cmd! :login-with-ldap params)
(rx/tap on-success)
(rx/map (fn [profile]
(-> profile
(with-meta {::ev/source "login-with-ldap"})
(logged-in))))
(rx/catch on-error))))))
(defn login-from-token
"Used mainly as flow continuation after token validation."
[{:keys [profile] :as tdata}]
(ptk/reify ::login-from-token
ptk/WatchEvent
@@ -656,3 +684,24 @@
(->> (rp/cmd! :delete-access-token params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn show-redirect-error
"A helper event that interprets the OIDC redirect errors on the URI
and shows an appropriate error message using the notification
banners."
[error]
(ptk/reify ::show-redirect-error
ptk/WatchEvent
(watch [_ _ _]
(let [hint (case error
"registration-disabled"
(tr "errors.registration-disabled")
"profile-blocked"
(tr "errors.profile-blocked")
"auth-provider-not-allowed"
(tr "errors.auth-provider-not-allowed")
"email-domain-not-allowed"
(tr "errors.email-domain-not-allowed")
:else
(tr "errors.generic"))]
(rx/of (msg/warn hint))))))

View File

@@ -14,7 +14,7 @@
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
;; [app.common.logging :as log]
[app.common.types.color :as ctc]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
@@ -51,26 +51,28 @@
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
;; ;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
;; (log/set-level! :debug)
(defn- log-changes
[changes file]
(let [extract-change
(fn [change]
(let [shape (when (:id change)
(cond
(:page-id change)
(get-in file [:pages-index
(:page-id change)
:objects
(:id change)])
(:component-id change)
(get-in file [:components
(:component-id change)
:objects
(:id change)])
:else nil))
(let [shape (if-let [obj (:obj change)]
obj
(when (:id change)
(cond
(:page-id change)
(get-in file [:pages-index
(:page-id change)
:objects
(:id change)])
(:component-id change)
(get-in file [:components
(:component-id change)
:objects
(:id change)])
:else nil)))
prefix (if (:component-id change) "[C] " "[P] ")
@@ -711,7 +713,7 @@
(ptk/reify ::sync-head
ptk/WatchEvent
(watch [it state _]
(log/info :msg "SYNC-head of shape" :id (str id))
(dwlh/dbg-warn "SYNC-HEAD of shape" :id id)
(let [file (wsh/get-local-file state)
file-full (wsh/get-local-file-full state)
libraries (wsh/get-libraries state)
@@ -733,9 +735,7 @@
(pcb/with-objects (:objects container))
(dwlh/generate-sync-shape-direct file-full libraries container (:id head) false components-v2))]
(log/debug :msg "SYNC-head finished" :js/rchanges (log-changes
(:redo-changes changes)
file))
(dwlh/dbg-info "SYNC-HEAD finished" :rchanges (dwlh/dbg-obj (log-changes (:redo-changes changes) file)))
(rx/of (dch/commit-changes changes))))))
(defn reset-component
@@ -747,7 +747,7 @@
(ptk/reify ::reset-component
ptk/WatchEvent
(watch [it state _]
(log/info :msg "RESET-COMPONENT of shape" :id (str id))
(dwlh/dbg-warn "RESET-COMPONENT of shape" :id id)
(let [file (wsh/get-local-file state)
file-full (wsh/get-local-file-full state)
libraries (wsh/get-libraries state)
@@ -769,9 +769,7 @@
(pcb/with-objects (:objects container))
(dwlh/generate-sync-shape-direct file-full libraries container id true components-v2))]
(log/debug :msg "RESET-COMPONENT finished" :js/rchanges (log-changes
(:redo-changes changes)
file))
(dwlh/dbg-info "RESET-COMPONENT finished" :rchanges (dwlh/dbg-obj (log-changes (:redo-changes changes) file)))
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
@@ -808,7 +806,7 @@
(ptk/reify ::update-component
ptk/WatchEvent
(watch [it state _]
(log/info :msg "UPDATE-COMPONENT of shape" :id (str id) :undo-group undo-group)
(dwlh/dbg-warn "UPDATE-COMPONENT of shape" :id id :undo-group undo-group)
(let [page-id (get state :current-page-id)
local-file (wsh/get-local-file state)
full-file (wsh/get-local-file-full state)
@@ -844,13 +842,9 @@
(update :redo-changes #(into [] xf-remove %))
(update :undo-changes #(into [] xf-remove %)))]
(log/debug :msg "UPDATE-COMPONENT finished"
:js/local-changes (log-changes
(:redo-changes local-changes)
file)
:js/nonlocal-changes (log-changes
(:redo-changes nonlocal-changes)
file))
(dwlh/dbg-info "UPDATE-COMPONENT finished"
:local-changes (dwlh/dbg-obj (log-changes (:redo-changes local-changes) file))
:nonlocal-changes (dwlh/dbg-obj (log-changes (:redo-changes nonlocal-changes) file)))
(rx/of
(when (seq (:redo-changes local-changes))
@@ -1038,10 +1032,10 @@
ptk/WatchEvent
(watch [_ state _]
(let [undo-id (js/Symbol)]
(log/info :msg "COMPONENT-SWAP"
:file (dwlh/pretty-file file-id state)
:id-new-component id-new-component
:undo-id undo-id)
(dwlh/dbg-warn "COMPONENT-SWAP"
:file (dwlh/dbg-file file-id state)
:id-new-component id-new-component
:undo-id undo-id)
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(rx/map #(component-swap % file-id id-new-component) (rx/from shapes))
@@ -1091,12 +1085,12 @@
ptk/WatchEvent
(watch [it state _]
(when (and (some? file-id) (some? library-id)) ; Prevent race conditions while navigating out of the file
(log/info :msg "SYNC-FILE"
:file (dwlh/pretty-file file-id state)
:library (dwlh/pretty-file library-id state)
:asset-type asset-type
:asset-id asset-id
:undo-group undo-group)
(dwlh/dbg-warn "SYNC-FILE"
:file (dwlh/dbg-file file-id state)
:library (dwlh/dbg-file library-id state)
:asset-type asset-type
:asset-id asset-id
:undo-group undo-group)
(let [file (wsh/get-file state file-id)
sync-components? (or (nil? asset-type) (= asset-type :components))
@@ -1136,9 +1130,8 @@
(mapcat find-frames)
distinct)]
(log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes
(:redo-changes changes)
file))
(dwlh/dbg-info "SYNC-FILE finished"
:rchanges (dwlh/dbg-obj (log-changes (:redo-changes changes) file)))
(rx/concat
(rx/of (set-updating-library false)
(msg/hide-tag :sync-dialog))
@@ -1306,9 +1299,9 @@
(if (d/not-empty? changed-components)
(if save-undo?
(do (log/info :msg "DETECTED COMPONENTS CHANGED"
:ids (map str changed-components)
:undo-group undo-group)
(do (dwlh/dbg-warn "DETECTED COMPONENTS CHANGED"
:ids (map dwlh/dbg-id changed-components)
:undo-group undo-group)
(->> (rx/from changed-components)
(rx/map #(component-changed % (:id old-data) undo-group))))
@@ -1327,7 +1320,7 @@
notifier-s
(->> changes-s
(rx/debounce 5000)
(rx/tap #(log/trc :hint "buffer initialized")))]
(rx/tap #(dwlh/dbg-log "buffer initialized")))]
(when components-v2?
(->> (rx/merge

View File

@@ -13,7 +13,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.grid-layout :as gslg]
[app.common.logging :as log]
;; [app.common.logging :as log]
[app.common.spec :as us]
[app.common.text :as txt]
[app.common.types.color :as ctc]
@@ -27,10 +27,97 @@
[app.common.types.typography :as cty]
[app.main.data.workspace.state-helpers :as wsh]
[cljs.spec.alpha :as s]
[clojure.set :as set]))
[clojure.set :as set]
[cuerdas.core :as str]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
;; ---- Logging utilities ----
(def dbg-level-log 1)
(def dbg-level-info 2)
(def dbg-level-warn 3)
;; Change this to one of the levels to display traces of this level or higher
(def dbg-level dbg-level-info)
;; Add some uuids to this to filter log messages affecting only to those shapes
(def dbg-filters #{})
(defn dbg-id
[uuid]
(let [uuid-str (str uuid)]
(str "#" (subs uuid-str (- (count uuid-str) 6)))))
(defn dbg-file
[file-id state]
(if (= file-id (:current-file-id state))
"<local>"
(str "<" (get-in state [:workspace-libraries file-id :name]) "> " (dbg-id file-id))))
(defn dbg-page
[page]
(with-meta page {:dbg-type :page}))
(defn dbg-component
[component]
(with-meta component {:dbg-type :component}))
(defn dbg-container
[container]
(if (cfh/page? container)
(with-meta container {:dbg-type :page})
(with-meta container {:dbg-type :component})))
(defn dbg-shape
[shape]
(with-meta shape {:dbg-type :shape}))
(defn dbg-obj
[obj]
(clj->js obj))
(defn js-value?
[[_key value]]
;; (prn value)
;; (prn (type value))
(or (object? value) (array? value)))
(defn dbg-format-str
[[key value]]
(let [value-str (case (:dbg-type (meta value))
:page (str "[P \"" (subs (:name value) 0 50) "\" " (dbg-id (:id value)) "]")
:component (str "[C \"" (subs (:name value) 0 50) "\" " (dbg-id (:id value)) "]")
:shape (str "\"" (subs (:name value) 0 50) "\" " (dbg-id (:id value)))
(cond
(uuid? value) (dbg-id value)
(js-symbol? value) (.toString value)
:else (d/nilv value "nil")))]
(str (name key) "=" value-str)))
(defn dbg
[level msg & args]
(when (>= level dbg-level)
(let [kvargs (apply sorted-map args)
ids (into #{} (filter uuid? (vals kvargs)))]
(when (or (empty? dbg-filters)
(empty? ids)
(seq (set/intersection dbg-filters ids)))
(let [dbg-fn (condp = level
dbg-level-log js/console.log
dbg-level-info js/console.info
dbg-level-warn js/console.warn)
kv-objects (d/filterm js-value? kvargs)
kv-rest (d/removem js-value? kvargs)]
(dbg-fn (->> (cons msg (map dbg-format-str kv-rest))
(str/join " ")))
(when (seq kv-objects)
(dorun (for [[key value] kv-objects]
(dbg-fn (str key) value)))))))))
(defn dbg-log [& args] (apply dbg (cons dbg-level-log args)))
(defn dbg-info [& args] (apply dbg (cons dbg-level-info args)))
(defn dbg-warn [& args] (apply dbg (cons dbg-level-warn args)))
;; ---- Advanced declaration of helpers ----
(declare generate-sync-container)
(declare generate-sync-shape)
@@ -58,17 +145,6 @@
(declare reposition-shape)
(declare make-change)
(defn pretty-file
[file-id state]
(if (= file-id (:current-file-id state))
"<local>"
(str "<" (get-in state [:workspace-libraries file-id :name]) ">")))
(defn pretty-uuid
[uuid]
(let [uuid-str (str uuid)]
(subs uuid-str (- (count uuid-str) 6))))
;; ---- Components and instances creation ----
(defn duplicate-component
@@ -220,7 +296,7 @@
with a component."
[changes container libraries shape-id]
(let [shape (ctn/get-shape container shape-id)]
(log/debug :msg "Detach instance" :shape-id shape-id :container (:id container))
(dbg-info "Detach instance" :shape-id shape-id :container (:id container))
(generate-detach-recursive changes container libraries shape-id true (true? (:component-root shape)))))
(defn- generate-detach-recursive
@@ -310,11 +386,11 @@
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(log/info :msg "Sync file with library"
(dbg-info "Sync file with library"
:asset-type asset-type
:asset-id asset-id
:file (pretty-file file-id state)
:library (pretty-file library-id state))
:file (dbg-file file-id state)
:library (dbg-file library-id state))
(let [file (wsh/get-file state file-id)
components-v2 (get-in file [:options :components-v2])]
@@ -347,11 +423,11 @@
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(log/info :msg "Sync local components with library"
(dbg-info "Sync local components with library"
:asset-type asset-type
:asset-id asset-id
:file (pretty-file file-id state)
:library (pretty-file library-id state))
:file (dbg-file file-id state)
:library (dbg-file library-id state))
(let [file (wsh/get-file state file-id)
components-v2 (get-in file [:options :components-v2])]
@@ -376,8 +452,8 @@
[it asset-type asset-id library-id state container components-v2]
(if (cfh/page? container)
(log/debug :msg "Sync page in local file" :page-id (:id container))
(log/debug :msg "Sync component in local library" :component-id (:id container)))
(dbg-info "Sync page in local file" :page (dbg-page container))
(dbg-info "Sync component in local library" :component (dbg-component container)))
(let [linked-shapes (->> (vals (:objects container))
(filter #(uses-assets? asset-type asset-id % library-id)))]
@@ -432,7 +508,7 @@
(defmethod generate-sync-shape :colors
[_ changes library-id state _ shape _]
(log/debug :msg "Sync colors of shape" :shape (:name shape))
(dbg-info "Sync colors of shape" :shape (dbg-shape shape))
;; Synchronize a shape that uses some colors of the library. The value of the
;; color in the library is copied to the shape.
@@ -443,7 +519,7 @@
(defmethod generate-sync-shape :typographies
[_ changes library-id state container shape _]
(log/debug :msg "Sync typographies of shape" :shape (:name shape))
(dbg-info "Sync typographies of shape" :shape (dbg-shape shape))
;; Synchronize a shape that uses some typographies of the library. The attributes
;; of the typography are copied to the shape."
@@ -611,7 +687,7 @@
"Generate changes to synchronize one shape that is the root of a component
instance, and all its children, from the given component."
[changes file libraries container shape-id reset? components-v2]
(log/debug :msg "Sync shape direct" :shape-inst (str shape-id) :reset? reset?)
(dbg-info "Sync shape direct" :shape-id shape-id :reset? reset?)
(let [shape-inst (ctn/get-shape container shape-id)
library (dm/get-in libraries [(:component-file shape-inst) :data])
component (ctkl/get-component library (:component-id shape-inst) true)]
@@ -648,6 +724,7 @@
reset?
initial-root?
redirect-shaperef
1
components-v2)
;; If the component is not found, because the master component has been
;; deleted or the library unlinked, do nothing in v2 or detach in v1.
@@ -674,10 +751,10 @@
nil))))))
(defn- generate-sync-shape-direct-recursive
[changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef components-v2]
(log/debug :msg "Sync shape direct recursive"
:shape-inst (str (:name shape-inst) " " (pretty-uuid (:id shape-inst)))
:component (:name component))
[changes container shape-inst component library file libraries shape-main root-inst root-main reset? initial-root? redirect-shaperef level components-v2]
(dbg-info (str (str/repeat "-" level) "> Sync shape direct recursive")
:shape-inst (dbg-shape shape-inst)
:component (dbg-component component))
(if (nil? shape-main)
;; This should not occur, but protect against it in any case
@@ -733,8 +810,7 @@
(map #(redirect-shaperef %) children-inst) children-inst)
only-inst (fn [changes child-inst]
(log/trace :msg "Only inst"
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))))
(dbg-log " *Only inst:" :child-inst (dbg-shape child-inst))
(if-not (and omit-touched?
(contains? (:touched shape-inst)
:shapes-group))
@@ -745,8 +821,7 @@
changes))
only-main (fn [changes child-main]
(log/trace :msg "Only main"
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
(dbg-log " *Only main:" :child-main (dbg-shape child-main))
(if-not (and omit-touched?
(contains? (:touched shape-inst)
:shapes-group))
@@ -764,9 +839,9 @@
changes))
both (fn [changes child-inst child-main]
(log/trace :msg "Both"
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
(dbg-log " *Both:"
:child-inst (dbg-shape child-inst)
:child-main (dbg-shape child-main))
(generate-sync-shape-direct-recursive changes
container
child-inst
@@ -780,19 +855,20 @@
reset?
initial-root?
redirect-shaperef
(inc level)
components-v2))
swapped (fn [changes child-inst child-main]
(log/trace :msg "Match slot"
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
(dbg-log " *Match slot:"
:child-inst (dbg-shape child-inst)
:child-main (dbg-shape child-main))
;; For now we don't make any sync here.
changes)
moved (fn [changes child-inst child-main]
(log/trace :msg "Move"
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
(dbg-log " *Move:"
:child-inst (dbg-shape child-inst)
:child-main (dbg-shape child-main))
(move-shape
changes
child-inst
@@ -840,7 +916,7 @@
"Generate changes to update the component a shape is linked to, from
the values in the shape and all its children."
[changes file libraries container shape-id components-v2]
(log/debug :msg "Sync shape inverse" :shape (str shape-id))
(dbg-info "Sync shape inverse" :shape-id shape-id)
(let [redirect-shaperef (partial redirect-shaperef container libraries)
shape-inst (ctn/get-shape container shape-id)
library (dm/get-in libraries [(:component-file shape-inst) :data])
@@ -877,14 +953,15 @@
root-main
initial-root?
redirect-shaperef
1
components-v2)
changes)))
(defn- generate-sync-shape-inverse-recursive
[changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef components-v2]
(log/trace :msg "Sync shape inverse recursive"
:shape (str (:name shape-inst))
:component (:name component))
[changes container shape-inst component library file libraries shape-main root-inst root-main initial-root? redirect-shaperef level components-v2]
(dbg-log (str (str/repeat "-" level) "> Sync shape inverse recursive")
:shape-inst (dbg-shape shape-inst)
:component (dbg-component component))
(if (nil? shape-main)
;; This should not occur, but protect against it in any case
@@ -942,6 +1019,7 @@
children-inst)
only-inst (fn [changes child-inst]
(dbg-log " *Only inst:" :child-inst (dbg-shape child-inst))
(add-shape-to-main changes
child-inst
(d/index-of children-inst
@@ -954,12 +1032,16 @@
components-v2))
only-main (fn [changes child-main]
(dbg-log " *Only main:" :child-main (dbg-shape child-main))
(remove-shape changes
child-main
component-container
false))
both (fn [changes child-inst child-main]
(dbg-log " *Both:"
:child-inst (dbg-shape child-inst)
:child-main (dbg-shape child-main))
(generate-sync-shape-inverse-recursive changes
container
child-inst
@@ -972,16 +1054,20 @@
root-main
initial-root?
redirect-shaperef
(inc level)
components-v2))
swapped (fn [changes child-inst child-main]
(log/trace :msg "Match slot"
:child-inst (str (:name child-inst) " " (pretty-uuid (:id child-inst)))
:child-main (str (:name child-main) " " (pretty-uuid (:id child-main))))
(dbg-log " *Match slot:"
:child-inst (dbg-shape child-inst)
:child-main (dbg-shape child-main))
;; For now we don't make any sync here.
changes)
moved (fn [changes child-inst child-main]
(dbg-log " *Move:"
:child-inst (dbg-shape child-inst)
:child-main (dbg-shape child-main))
(move-shape
changes
child-main
@@ -1024,14 +1110,13 @@
(defn- compare-children
[changes children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset? components-v2]
(log/trace :msg "Compare children")
(dbg-log "Compare children")
(loop [children-inst (seq (or children-inst []))
children-main (seq (or children-main []))
changes changes]
(let [child-inst (first children-inst)
child-main (first children-main)]
(log/trace :main (str (:name child-main) " " (pretty-uuid (:id child-main)))
:inst (str (:name child-inst) " " (pretty-uuid (:id child-inst))))
(dbg-log " " :main (dbg-shape child-main) :inst (dbg-shape child-inst))
(cond
(and (nil? child-inst) (nil? child-main))
changes
@@ -1093,10 +1178,7 @@
(defn- add-shape-to-instance
[changes component-shape index component-page container root-instance root-main omit-touched? set-remote-synced? components-v2]
(log/info :msg (str "ADD [P " (pretty-uuid (:id container)) "] "
(:name component-shape)
" "
(pretty-uuid (:id component-shape))))
(dbg-warn "ADD TO INST" :container (dbg-container container) :component-shape (dbg-shape component-shape))
(let [component-parent-shape (ctn/get-shape component-page (:parent-id component-shape))
parent-shape (d/seek #(ctk/is-main-of? component-parent-shape % components-v2)
(cfh/get-children-with-self (:objects container)
@@ -1125,7 +1207,7 @@
update-original-shape (fn [original-shape _new-shape]
original-shape)
[_ new-shapes _]
[new-shape new-shapes _]
(ctst/clone-shape component-shape
(:id parent-shape)
(get component-page :objects)
@@ -1162,16 +1244,14 @@
:shapes all-parents}))
changes' (reduce del-obj-change changes' new-shapes)]
(dbg-log " " :parent-shape (dbg-shape parent-shape) :new-shape (dbg-shape new-shape))
(if (and (cfh/touched-group? parent-shape :shapes-group) omit-touched?)
changes
changes')))
(defn- add-shape-to-main
[changes shape index component component-container page root-instance root-main components-v2]
(log/info :msg (str "ADD [C " (pretty-uuid (:id component-container)) "] "
(:name shape)
" "
(pretty-uuid (:id shape))))
(dbg-warn "ADD TO MAIN" :component-container (dbg-container component-container) :shape (dbg-shape shape))
(let [parent-shape (ctn/get-shape page (:parent-id shape))
component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape components-v2)
(cfh/get-children-with-self (:objects component-container)
@@ -1189,7 +1269,7 @@
(assoc original-shape
:shape-ref (:id new-shape)))
[_new-shape new-shapes updated-shapes]
[new-shape new-shapes updated-shapes]
(ctst/clone-shape shape
(:id component-parent-shape)
(get page :objects)
@@ -1267,16 +1347,12 @@
changes' (reduce mod-obj-change changes' updated-shapes)
changes' (reduce del-obj-change changes' new-shapes)]
(dbg-log " " :component-parent-shape (dbg-shape component-parent-shape) :new-shape (dbg-shape new-shape))
changes'))
(defn- remove-shape
[changes shape container omit-touched?]
(log/info :msg (str "REMOVE-SHAPE "
(if (cfh/page? container) "[P " "[C ")
(pretty-uuid (:id container)) "] "
(:name shape)
" "
(pretty-uuid (:id shape))))
(dbg-warn "REMOVE SHAPE" :container (dbg-container container) :shape (dbg-shape shape))
(let [objects (get container :objects)
parents (cfh/get-parent-ids objects (:id shape))
parent (first parents)
@@ -1323,16 +1399,10 @@
(defn- move-shape
[changes shape index-before index-after container omit-touched?]
(log/info :msg (str "MOVE "
(if (cfh/page? container) "[P " "[C ")
(pretty-uuid (:id container)) "] "
(:name shape)
" "
(pretty-uuid (:id shape))
" "
index-before
" -> "
index-after))
(dbg-warn "MOVE"
:container (dbg-container container)
:shape (dbg-shape shape)
:index (str index-before "->" index-after))
(let [parent (ctn/get-shape container (:parent-id shape))
changes' (-> changes
@@ -1363,13 +1433,11 @@
(if (nil? (:shape-ref dest-shape))
changes
(do
(log/info :msg (str "CHANGE-TOUCHED "
(if (cfh/page? container) "[P " "[C ")
(pretty-uuid (:id container)) "] "
(:name dest-shape)
" "
(pretty-uuid (:id dest-shape)))
:options options)
(dbg-warn "CHANGE TOUCHED"
:container (dbg-container container)
:dest-shape (dbg-shape dest-shape)
:reset-touched? reset-touched?
:copy-touched? copy-touched?)
(let [new-touched (cond
reset-touched?
nil
@@ -1405,13 +1473,10 @@
(if (nil? (:shape-ref shape))
changes
(do
(log/info :msg (str "CHANGE-REMOTE-SYNCED? "
(if (cfh/page? container) "[P " "[C ")
(pretty-uuid (:id container)) "] "
(:name shape)
" "
(pretty-uuid (:id shape)))
:remote-synced remote-synced?)
(dbg-warn "CHANGE REMOTE SYNCED"
:container (dbg-container container)
:shape (dbg-shape shape)
:remote-synced? remote-synced?)
(-> changes
(update :redo-changes conj (make-change
container
@@ -1436,16 +1501,10 @@
in the destination shape will not be copied."
[changes dest-shape origin-shape dest-root origin-root container omit-touched?]
(log/info :msg (str "SYNC "
(:name origin-shape)
" "
(pretty-uuid (:id origin-shape))
" -> "
(if (cfh/page? container) "[P " "[C ")
(pretty-uuid (:id container)) "] "
(:name dest-shape)
" "
(pretty-uuid (:id dest-shape))))
(dbg-warn "SYNC"
:origin-shape (dbg-shape origin-shape)
:container (dbg-container container)
:dest-shape (dbg-shape dest-shape))
(let [;; To synchronize geometry attributes we need to make a prior
;; operation, because coordinates are absolute, but we need to

View File

@@ -9,6 +9,8 @@
(:require
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.auth.login :refer [login-page]]
[app.main.ui.auth.recovery :refer [recovery-page]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
@@ -42,11 +44,16 @@
{::mf/props :obj}
[{:keys [route]}]
(let [section (dm/get-in route [:data :name])
params (:query-params route)]
params (:query-params route)
error (:error params)]
(mf/with-effect []
(dom/set-html-title (tr "title.default")))
(mf/with-effect [error]
(when error
(st/emit! (du/show-redirect-error error))))
[:main {:class (stl/css :auth-section)}
[:a {:href "#/" :class (stl/css :logo-btn)} i/logo]
[:div {:class (stl/css :login-illustration)}

View File

@@ -42,6 +42,10 @@
{:type :warning
:content (tr "auth.demo-warning")}])
(defn create-demo-profile
[]
(st/emit! (du/create-demo-profile)))
(defn- login-with-oidc
[event provider params]
(dom/prevent-default event)
@@ -60,28 +64,6 @@
:else
(st/emit! (msg/error (tr "errors.generic"))))))))
(defn- login-with-ldap
[event params]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [{:keys [on-error]} (meta params)]
(->> (rp/cmd! :login-with-ldap params)
(rx/subs! (fn [profile]
(if-let [token (:invitation-token profile)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
(st/emit! (du/login-from-token {:profile profile}))))
(fn [{:keys [type code] :as error}]
(cond
(and (= type :restriction)
(= code :ldap-not-initialized))
(st/emit! (msg/error (tr "errors.ldap-disabled")))
(fn? on-error)
(on-error error)
:else
(st/emit! (msg/error (tr "errors.generic")))))))))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::invitation-token ::us/not-empty-string)
@@ -107,13 +89,18 @@
:initial initial)
on-error
(fn [err]
(let [cause (ex-data err)]
(fn [cause]
(let [cause (ex-data cause)]
(cond
(and (= :restriction (:type cause))
(= :profile-blocked (:code cause)))
(reset! error (tr "errors.profile-blocked"))
(and (= :restriction (:type cause))
(= :ldap-not-initialized (:code cause)))
(st/emit! (msg/error (tr "errors.ldap-disabled")))
(and (= :restriction (:type cause))
(= :admin-only-profile (:code cause)))
(reset! error (tr "errors.profile-blocked"))
@@ -130,9 +117,10 @@
(reset! error (tr "errors.generic")))))
on-success-default
(fn [data]
(when-let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))))
(mf/use-fn
(fn [data]
(when-let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})))))
on-success
(fn [data]
@@ -153,11 +141,15 @@
(mf/use-callback
(mf/deps form)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(reset! error nil)
(let [params (:clean-data @form)]
(login-with-ldap event (with-meta params
{:on-error on-error
:on-success on-success})))))
(let [params (:clean-data @form)
params (with-meta params
{:on-error on-error
:on-success on-success})]
(st/emit! (du/login-with-ldap params)))))
on-recovery-request
(mf/use-fn
@@ -308,5 +300,11 @@
[:& lk/link {:action go-register
:class (stl/css :register-link)
:data-test "register-submit"}
(tr "auth.register-submit")]])]]))
(tr "auth.register-submit")]])]
(when (contains? cf/flags :demo-users)
[:div {:class (stl/css :link-entry :demo-account)}
[:span (tr "auth.create-demo-profile") " "]
[:& lk/link {:action create-demo-profile
:data-test "demo-account-link"}
(tr "auth.create-demo-account")]])]))

View File

@@ -26,18 +26,20 @@
;; --- PAGE: Register
(defn- validate
(defn- validate-password-length
[errors data]
(let [password (:password data)]
(cond-> errors
(> 8 (count password))
(assoc :password {:message "errors.password-too-short"})
:always
(d/update-when :email
(fn [{:keys [code] :as error}]
(cond-> error
(= code ::us/email)
(assoc :message (tr "errors.email-invalid"))))))))
(assoc :password {:message "errors.password-too-short"}))))
(defn- validate-email
[errors _]
(d/update-when errors :email
(fn [{:keys [code] :as error}]
(cond-> error
(= code ::us/email)
(assoc :message (tr "errors.email-invalid"))))))
(s/def ::fullname ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
@@ -49,31 +51,20 @@
(s/keys :req-un [::password ::email]
:opt-un [::invitation-token]))
(defn- handle-prepare-register-error
(defn- on-prepare-register-error
[form cause]
(let [{:keys [type code]} (ex-data cause)]
(condp = [type code]
[:restriction :registration-disabled]
(st/emit! (msg/error (tr "errors.registration-disabled")))
[:restriction :profile-blocked]
(st/emit! (msg/error (tr "errors.profile-blocked")))
[:validation :email-has-permanent-bounces]
(let [email (get @form [:data :email])]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email))))
[:validation :email-already-exists]
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})
[:validation :email-as-password]
(swap! form assoc-in [:errors :password]
{:message "errors.email-as-password"})
(st/emit! (msg/error (tr "errors.generic"))))))
(defn- handle-prepare-register-success
(defn- on-prepare-register-success
[params]
(st/emit! (rt/nav :auth-register-validate {} params)))
@@ -81,28 +72,30 @@
[{:keys [params on-success-callback]}]
(let [initial (mf/use-memo (mf/deps params) (constantly params))
form (fm/use-form :spec ::register-form
:validators [validate
:validators [validate-password-length
validate-email
(fm/validate-not-empty :password (tr "auth.password-not-empty"))]
:initial initial)
submitted? (mf/use-state false)
on-success (fn [p]
(if (nil? on-success-callback)
(handle-prepare-register-success p)
(on-success-callback p)))
submitted? (mf/use-state false)
on-submit
(mf/use-fn
(mf/deps on-success-callback)
(fn [form _event]
(reset! submitted? true)
(let [cdata (:clean-data @form)]
(let [cdata (:clean-data @form)
on-success (fn [data]
(if (nil? on-success-callback)
(on-prepare-register-success data)
(on-success-callback data)))
on-error (fn [data]
(on-prepare-register-error form data))]
(->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params))
(rx/finalize #(reset! submitted? false))
(rx/subs!
on-success
(partial handle-prepare-register-error form))))))]
(rx/subs! on-success on-error)))))]
[:& fm/form {:on-submit on-submit :form form}
[:div {:class (stl/css :fields-row)}
@@ -126,7 +119,6 @@
:data-test "register-form-submit"
:class (stl/css :register-btn)}]]))
(mf/defc register-methods
{::mf/props :obj}
[{:keys [params on-success-callback]}]
@@ -162,22 +154,15 @@
[:*
[:hr {:class (stl/css :separator)}]
[:div {:class (stl/css :demo-account)}
[:& lk/link {:action #(st/emit! (du/create-demo-profile))
[:& lk/link {:action login/create-demo-profile
:class (stl/css :demo-account-link)}
(tr "auth.create-demo-account")]]])]])
;; --- PAGE: register validation
(defn- handle-register-error
[form error]
(case (:code error)
:email-already-exists
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})
(do
(println (:explain error))
(st/emit! (msg/error (tr "errors.generic"))))))
[_form _data]
(st/emit! (msg/error (tr "errors.generic"))))
(defn- handle-register-success
[data]
@@ -186,8 +171,6 @@
(let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
;; The :is-active flag is true, when insecure-register is enabled
;; or the user used external auth provider.
(:is-active data)
(st/emit! (du/login-from-register))
@@ -282,5 +265,3 @@
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]
[:div {:class (stl/css :notification-text-email)} (:email params "")]
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])

View File

@@ -54,9 +54,10 @@
(let [props (.-props tab)
id (.-id props)
title (.-title props)
sid (d/name id)]
sid (d/name id)
tooltip (if (string? title) title nil)]
[:div {:key (str/concat "tab-" sid)
:title title
:title tooltip
:data-id sid
:on-click on-click
:class (stl/css-case

View File

@@ -180,7 +180,7 @@
:on-zoom-fit handle-zoom-fit
:on-fullscreen toggle-fullscreen}]
(when (:can-edit permissions)
(when (:in-team permissions)
[:span {:on-click go-to-workspace
:class (stl/css :edit-btn)}
i/curve])
@@ -191,7 +191,9 @@
:on-click toggle-fullscreen}
i/expand]
(when (:is-admin permissions)
(when (and
(:in-team permissions)
(:is-admin permissions))
[:button {:on-click open-share-dialog
:class (stl/css :share-btn)}
(tr "labels.share")])
@@ -301,8 +303,8 @@
;; If the user doesn't have permission we disable the link
[:a {:class (stl/css :home-link)
:on-click go-to-dashboard
:style {:cursor (when-not (:can-edit permissions) "auto")
:pointer-events (when-not (:can-edit permissions) "none")}}
:style {:cursor (when-not (:in-team permissions) "auto")
:pointer-events (when-not (:in-team permissions) "none")}}
[:span {:class (stl/css :logo-icon)}
i/logo-icon]]
@@ -321,7 +323,7 @@
:title (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))}
i/play]
(when (or (:can-edit permissions)
(when (or (:in-team permissions)
(= (:who-comment permissions) "all"))
[:button {:on-click navigate
:data-value "comments"

View File

@@ -968,6 +968,14 @@ msgstr "Your profile has emails muted (spam reports or high bounces)."
msgid "errors.registration-disabled"
msgstr "The registration is currently disabled."
#: src/app/main/data/users.cljs
msgid "errors.auth-provider-not-allowed"
msgstr "Auth provider not allowed for this profile"
#: src/app/main/data/users.cljs
msgid "errors.email-domain-not-allowed"
msgstr "Domain not allowed"
msgid "errors.team-leave.insufficient-members"
msgstr "Insufficient members to leave team, you probably want to delete it."

View File

@@ -991,6 +991,14 @@ msgstr ""
msgid "errors.registration-disabled"
msgstr "El registro está actualmente desactivado."
#: src/app/main/data/users.cljs
msgid "errors.auth-provider-not-allowed"
msgstr "El proveedor de autenticación no permitido para este perfil de usuario"
#: src/app/main/data/users.cljs
msgid "errors.email-domain-not-allowed"
msgstr "Dominio no permitido"
msgid "errors.team-leave.insufficient-members"
msgstr ""
"No hay miembros suficientes para salir del equipo, probablemente quieras "

View File

@@ -2141,6 +2141,17 @@ __metadata:
languageName: node
linkType: hard
"@playwright/test@npm:^1.42.1":
version: 1.42.1
resolution: "@playwright/test@npm:1.42.1"
dependencies:
playwright: "npm:1.42.1"
bin:
playwright: cli.js
checksum: e5d7c1ffedabb934643edb010038edcb70d51d224fb6444844a854d94365a6179d4407a83da176cae37ccd42b62c148843e0b6f9b4c6506048e06558c00d4267
languageName: node
linkType: hard
"@radix-ui/number@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/number@npm:1.0.1"
@@ -4902,6 +4913,15 @@ __metadata:
languageName: node
linkType: hard
"async@npm:^2.6.4":
version: 2.6.4
resolution: "async@npm:2.6.4"
dependencies:
lodash: "npm:^4.17.14"
checksum: 0ebb3273ef96513389520adc88e0d3c45e523d03653cc9b66f5c46f4239444294899bfd13d2b569e7dbfde7da2235c35cf5fd3ece9524f935d41bbe4efccdad0
languageName: node
linkType: hard
"async@npm:^3.2.3, async@npm:^3.2.4":
version: 3.2.5
resolution: "async@npm:3.2.5"
@@ -5054,6 +5074,15 @@ __metadata:
languageName: node
linkType: hard
"basic-auth@npm:^2.0.1":
version: 2.0.1
resolution: "basic-auth@npm:2.0.1"
dependencies:
safe-buffer: "npm:5.1.2"
checksum: 05f56db3a0fc31c89c86b605231e32ee143fb6ae38dc60616bc0970ae6a0f034172def99e69d3aed0e2c9e7cac84e2d63bc51a0b5ff6ab5fc8808cc8b29923c1
languageName: node
linkType: hard
"better-opn@npm:^3.0.2":
version: 3.0.2
resolution: "better-opn@npm:3.0.2"
@@ -5482,6 +5511,19 @@ __metadata:
languageName: node
linkType: hard
"call-bind@npm:^1.0.7":
version: 1.0.7
resolution: "call-bind@npm:1.0.7"
dependencies:
es-define-property: "npm:^1.0.0"
es-errors: "npm:^1.3.0"
function-bind: "npm:^1.1.2"
get-intrinsic: "npm:^1.2.4"
set-function-length: "npm:^1.2.1"
checksum: a3ded2e423b8e2a265983dba81c27e125b48eefb2655e7dfab6be597088da3d47c47976c24bc51b8fd9af1061f8f87b4ab78a314f3c77784b2ae2ba535ad8b8d
languageName: node
linkType: hard
"camelcase@npm:^3.0.0":
version: 3.0.0
resolution: "camelcase@npm:3.0.0"
@@ -6094,6 +6136,13 @@ __metadata:
languageName: node
linkType: hard
"corser@npm:^2.0.1":
version: 2.0.1
resolution: "corser@npm:2.0.1"
checksum: 1f319a752a560342dd22d936e5a4c158bfcbc332524ef5b05a7277236dad8b0b2868fd5cf818559f29954ec4d777d82e797fccd76601fcfe431610e4143c8acc
languageName: node
linkType: hard
"create-ecdh@npm:^4.0.0":
version: 4.0.4
resolution: "create-ecdh@npm:4.0.4"
@@ -6335,7 +6384,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:3.X":
"debug@npm:3.X, debug@npm:^3.2.7":
version: 3.2.7
resolution: "debug@npm:3.2.7"
dependencies:
@@ -6458,6 +6507,17 @@ __metadata:
languageName: node
linkType: hard
"define-data-property@npm:^1.1.4":
version: 1.1.4
resolution: "define-data-property@npm:1.1.4"
dependencies:
es-define-property: "npm:^1.0.0"
es-errors: "npm:^1.3.0"
gopd: "npm:^1.0.1"
checksum: dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37
languageName: node
linkType: hard
"define-lazy-prop@npm:^2.0.0":
version: 2.0.0
resolution: "define-lazy-prop@npm:2.0.0"
@@ -6977,6 +7037,22 @@ __metadata:
languageName: node
linkType: hard
"es-define-property@npm:^1.0.0":
version: 1.0.0
resolution: "es-define-property@npm:1.0.0"
dependencies:
get-intrinsic: "npm:^1.2.4"
checksum: 6bf3191feb7ea2ebda48b577f69bdfac7a2b3c9bcf97307f55fd6ef1bbca0b49f0c219a935aca506c993d8c5d8bddd937766cb760cd5e5a1071351f2df9f9aa4
languageName: node
linkType: hard
"es-errors@npm:^1.3.0":
version: 1.3.0
resolution: "es-errors@npm:1.3.0"
checksum: 0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85
languageName: node
linkType: hard
"es-get-iterator@npm:^1.1.3":
version: 1.1.3
resolution: "es-get-iterator@npm:1.1.3"
@@ -7349,6 +7425,13 @@ __metadata:
languageName: node
linkType: hard
"eventemitter3@npm:^4.0.0":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"
checksum: 5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b
languageName: node
linkType: hard
"events@npm:^3.0.0, events@npm:^3.3.0":
version: 3.3.0
resolution: "events@npm:3.3.0"
@@ -7845,6 +7928,16 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.0.0":
version: 1.15.6
resolution: "follow-redirects@npm:1.15.6"
peerDependenciesMeta:
debug:
optional: true
checksum: 9ff767f0d7be6aa6870c82ac79cf0368cd73e01bbc00e9eb1c2a16fbb198ec105e3c9b6628bb98e9f3ac66fe29a957b9645bcb9a490bb7aa0d35f908b6b85071
languageName: node
linkType: hard
"for-each@npm:^0.3.3":
version: 0.3.3
resolution: "for-each@npm:0.3.3"
@@ -7925,6 +8018,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "frontend@workspace:."
dependencies:
"@playwright/test": "npm:^1.42.1"
"@storybook/addon-essentials": "npm:^7.6.17"
"@storybook/addon-interactions": "npm:^7.6.17"
"@storybook/addon-links": "npm:^7.6.17"
@@ -7952,6 +8046,7 @@ __metadata:
gulp-sourcemaps: "npm:^3.0.0"
gulp-svg-sprite: "npm:^2.0.3"
highlight.js: "npm:^11.9.0"
http-server: "npm:^14.1.1"
js-beautify: "npm:^1.15.1"
jsdom: "npm:^24.0.0"
jszip: "npm:^3.10.1"
@@ -8059,6 +8154,16 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:2.3.2":
version: 2.3.2
resolution: "fsevents@npm:2.3.2"
dependencies:
node-gyp: "npm:latest"
checksum: be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@npm:^1.2.7":
version: 1.2.13
resolution: "fsevents@npm:1.2.13"
@@ -8080,6 +8185,15 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
dependencies:
node-gyp: "npm:latest"
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A^1.2.7#optional!builtin<compat/fsevents>":
version: 1.2.13
resolution: "fsevents@patch:fsevents@npm%3A1.2.13#optional!builtin<compat/fsevents>::version=1.2.13&hash=d11327"
@@ -8174,6 +8288,19 @@ __metadata:
languageName: node
linkType: hard
"get-intrinsic@npm:^1.2.4":
version: 1.2.4
resolution: "get-intrinsic@npm:1.2.4"
dependencies:
es-errors: "npm:^1.3.0"
function-bind: "npm:^1.1.2"
has-proto: "npm:^1.0.1"
has-symbols: "npm:^1.0.3"
hasown: "npm:^2.0.0"
checksum: 0a9b82c16696ed6da5e39b1267104475c47e3a9bdbe8b509dfe1710946e38a87be70d759f4bb3cda042d76a41ef47fe769660f3b7c0d1f68750299344ffb15b7
languageName: node
linkType: hard
"get-nonce@npm:^1.0.0":
version: 1.0.1
resolution: "get-nonce@npm:1.0.1"
@@ -8664,6 +8791,15 @@ __metadata:
languageName: node
linkType: hard
"has-property-descriptors@npm:^1.0.2":
version: 1.0.2
resolution: "has-property-descriptors@npm:1.0.2"
dependencies:
es-define-property: "npm:^1.0.0"
checksum: 253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236
languageName: node
linkType: hard
"has-proto@npm:^1.0.1":
version: 1.0.1
resolution: "has-proto@npm:1.0.1"
@@ -8756,6 +8892,15 @@ __metadata:
languageName: node
linkType: hard
"he@npm:^1.2.0":
version: 1.2.0
resolution: "he@npm:1.2.0"
bin:
he: bin/he
checksum: a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17
languageName: node
linkType: hard
"highlight.js@npm:^11.9.0":
version: 11.9.0
resolution: "highlight.js@npm:11.9.0"
@@ -8790,6 +8935,15 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^3.0.0":
version: 3.0.0
resolution: "html-encoding-sniffer@npm:3.0.0"
dependencies:
whatwg-encoding: "npm:^2.0.0"
checksum: b17b3b0fb5d061d8eb15121c3b0b536376c3e295ecaf09ba48dd69c6b6c957839db124fe1e2b3f11329753a4ee01aa7dedf63b7677999e86da17fbbdd82c5386
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
@@ -8836,6 +8990,40 @@ __metadata:
languageName: node
linkType: hard
"http-proxy@npm:^1.18.1":
version: 1.18.1
resolution: "http-proxy@npm:1.18.1"
dependencies:
eventemitter3: "npm:^4.0.0"
follow-redirects: "npm:^1.0.0"
requires-port: "npm:^1.0.0"
checksum: 148dfa700a03fb421e383aaaf88ac1d94521dfc34072f6c59770528c65250983c2e4ec996f2f03aa9f3fe46cd1270a593126068319311e3e8d9e610a37533e94
languageName: node
linkType: hard
"http-server@npm:^14.1.1":
version: 14.1.1
resolution: "http-server@npm:14.1.1"
dependencies:
basic-auth: "npm:^2.0.1"
chalk: "npm:^4.1.2"
corser: "npm:^2.0.1"
he: "npm:^1.2.0"
html-encoding-sniffer: "npm:^3.0.0"
http-proxy: "npm:^1.18.1"
mime: "npm:^1.6.0"
minimist: "npm:^1.2.6"
opener: "npm:^1.5.1"
portfinder: "npm:^1.0.28"
secure-compare: "npm:3.0.1"
union: "npm:~0.5.0"
url-join: "npm:^4.0.1"
bin:
http-server: bin/http-server
checksum: c5770ddd722dd520ce0af25efee6bfb7c6300ff4e934636d4eec83fa995739e64de2e699e89e7a795b3a1894bcc37bec226617c1023600aacd7871fd8d6ffe6d
languageName: node
linkType: hard
"https-browserify@npm:^1.0.0":
version: 1.0.0
resolution: "https-browserify@npm:1.0.0"
@@ -10196,7 +10384,7 @@ __metadata:
languageName: node
linkType: hard
"lodash@npm:^4.17.21":
"lodash@npm:^4.17.14, lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c
@@ -10591,7 +10779,7 @@ __metadata:
languageName: node
linkType: hard
"mime@npm:1.6.0":
"mime@npm:1.6.0, mime@npm:^1.6.0":
version: 1.6.0
resolution: "mime@npm:1.6.0"
bin:
@@ -10788,7 +10976,7 @@ __metadata:
languageName: node
linkType: hard
"mkdirp@npm:^0.5.4":
"mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.6":
version: 0.5.6
resolution: "mkdirp@npm:0.5.6"
dependencies:
@@ -11362,6 +11550,15 @@ __metadata:
languageName: node
linkType: hard
"opener@npm:^1.5.1":
version: 1.5.2
resolution: "opener@npm:1.5.2"
bin:
opener: bin/opener-bin.js
checksum: dd56256ab0cf796585617bc28e06e058adf09211781e70b264c76a1dbe16e90f868c974e5bf5309c93469157c7d14b89c35dc53fe7293b0e40b4d2f92073bc79
languageName: node
linkType: hard
"opentype.js@npm:^1.3.4":
version: 1.3.4
resolution: "opentype.js@npm:1.3.4"
@@ -11881,6 +12078,30 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.42.1":
version: 1.42.1
resolution: "playwright-core@npm:1.42.1"
bin:
playwright-core: cli.js
checksum: 9bb0be6defa32eb1b01429615f10c2ad17dcf701656c081a250369c1eb3b0dcc2a0ee21188cd653cdd2303ca73ff94df0d270b178fe3897eba274793dab368ce
languageName: node
linkType: hard
"playwright@npm:1.42.1":
version: 1.42.1
resolution: "playwright@npm:1.42.1"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.42.1"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 91dcbfe92d75ca9eb4bfff69bb1ec28007b5a96f6187f48e52aa0f6acf8c24f6039ed6467c152964cc92f4ab64b85dc665b13c52b2fb9f7b9182ddb9db404e37
languageName: node
linkType: hard
"plugin-error@npm:^1.0.0, plugin-error@npm:^1.0.1":
version: 1.0.1
resolution: "plugin-error@npm:1.0.1"
@@ -11911,6 +12132,17 @@ __metadata:
languageName: node
linkType: hard
"portfinder@npm:^1.0.28":
version: 1.0.32
resolution: "portfinder@npm:1.0.32"
dependencies:
async: "npm:^2.6.4"
debug: "npm:^3.2.7"
mkdirp: "npm:^0.5.6"
checksum: cef8b567b78aabccc59fe8e103bac8b394bb45a6a69be626608f099f454124c775aaf47b274c006332c07ab3f501cde55e49aaeb9d49d78d90362d776a565cbf
languageName: node
linkType: hard
"posix-character-classes@npm:^0.1.0":
version: 0.1.1
resolution: "posix-character-classes@npm:0.1.1"
@@ -12337,6 +12569,15 @@ __metadata:
languageName: node
linkType: hard
"qs@npm:^6.4.0":
version: 6.12.0
resolution: "qs@npm:6.12.0"
dependencies:
side-channel: "npm:^1.0.6"
checksum: e165a77ac5f3ca60c15c5f3d51b321ddec7aa438804436b29d160117bc6fb7bf7dab94abd0c7d7c0785890d3a75ae41e1d6346e158aaf1540c6fe53a31f11675
languageName: node
linkType: hard
"querystring-es3@npm:^0.2.0":
version: 0.2.1
resolution: "querystring-es3@npm:0.2.1"
@@ -13474,6 +13715,13 @@ __metadata:
languageName: node
linkType: hard
"secure-compare@npm:3.0.1":
version: 3.0.1
resolution: "secure-compare@npm:3.0.1"
checksum: af3102f3f555d917c8ffff7a5f6f00f70195708f4faf82d48794485c9f3cb365cee0dd4da6b4e53e8964f172970bce6069b6101ba3ce8c309bff54f460d1f650
languageName: node
linkType: hard
"semver-greatest-satisfied-range@npm:^1.1.0":
version: 1.1.0
resolution: "semver-greatest-satisfied-range@npm:1.1.0"
@@ -13564,6 +13812,20 @@ __metadata:
languageName: node
linkType: hard
"set-function-length@npm:^1.2.1":
version: 1.2.2
resolution: "set-function-length@npm:1.2.2"
dependencies:
define-data-property: "npm:^1.1.4"
es-errors: "npm:^1.3.0"
function-bind: "npm:^1.1.2"
get-intrinsic: "npm:^1.2.4"
gopd: "npm:^1.0.1"
has-property-descriptors: "npm:^1.0.2"
checksum: 82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c
languageName: node
linkType: hard
"set-function-name@npm:^2.0.0":
version: 2.0.1
resolution: "set-function-name@npm:2.0.1"
@@ -13695,6 +13957,18 @@ __metadata:
languageName: node
linkType: hard
"side-channel@npm:^1.0.6":
version: 1.0.6
resolution: "side-channel@npm:1.0.6"
dependencies:
call-bind: "npm:^1.0.7"
es-errors: "npm:^1.3.0"
get-intrinsic: "npm:^1.2.4"
object-inspect: "npm:^1.13.1"
checksum: d2afd163dc733cc0a39aa6f7e39bf0c436293510dbccbff446733daeaf295857dbccf94297092ec8c53e2503acac30f0b78830876f0485991d62a90e9cad305f
languageName: node
linkType: hard
"siginfo@npm:^2.0.0":
version: 2.0.0
resolution: "siginfo@npm:2.0.0"
@@ -15035,6 +15309,15 @@ __metadata:
languageName: node
linkType: hard
"union@npm:~0.5.0":
version: 0.5.0
resolution: "union@npm:0.5.0"
dependencies:
qs: "npm:^6.4.0"
checksum: 9ac158d99991063180e56f408f5991e808fa07594713439c098116da09215c154672ee8c832e16a6b39b037609c08bcaff8ff07c1e3e46c3cc622897972af2aa
languageName: node
linkType: hard
"unique-filename@npm:^3.0.0":
version: 3.0.0
resolution: "unique-filename@npm:3.0.0"
@@ -15178,6 +15461,13 @@ __metadata:
languageName: node
linkType: hard
"url-join@npm:^4.0.1":
version: 4.0.1
resolution: "url-join@npm:4.0.1"
checksum: ac65e2c7c562d7b49b68edddcf55385d3e922bc1dd5d90419ea40b53b6de1607d1e45ceb71efb9d60da02c681d13c6cb3a1aa8b13fc0c989dfc219df97ee992d
languageName: node
linkType: hard
"url-parse@npm:^1.5.3":
version: 1.5.10
resolution: "url-parse@npm:1.5.10"
@@ -15633,6 +15923,15 @@ __metadata:
languageName: node
linkType: hard
"whatwg-encoding@npm:^2.0.0":
version: 2.0.0
resolution: "whatwg-encoding@npm:2.0.0"
dependencies:
iconv-lite: "npm:0.6.3"
checksum: 91b90a49f312dc751496fd23a7e68981e62f33afe938b97281ad766235c4872fc4e66319f925c5e9001502b3040dd25a33b02a9c693b73a4cbbfdc4ad10c3e3e
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"

View File

@@ -1 +1 @@
2.0.0
2.1.0