mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
1126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
588dd5452a | ||
|
|
a02a316165 | ||
|
|
bbfcff0772 | ||
|
|
6b9f7e9922 | ||
|
|
21cdd1200a | ||
|
|
aeda6271cd | ||
|
|
63ee99d46c | ||
|
|
058c3707c8 | ||
|
|
b81ffa422f | ||
|
|
e54f213b3f | ||
|
|
97072b112c | ||
|
|
16ff29538b | ||
|
|
48b72229c3 | ||
|
|
fa8af898ba | ||
|
|
42e4a4a2dd | ||
|
|
e778bc8b18 | ||
|
|
6d60ca0474 | ||
|
|
41f906110e | ||
|
|
8d7be1e273 | ||
|
|
13adc88637 | ||
|
|
84f063a5b4 | ||
|
|
647357a892 | ||
|
|
c1eed3a364 | ||
|
|
2dd7f241d3 | ||
|
|
66c6e0232e | ||
|
|
4de43b32e8 | ||
|
|
1d735e5b12 | ||
|
|
15d3107c48 | ||
|
|
2d1158efa3 | ||
|
|
f78f843f7c | ||
|
|
07e40e78cd | ||
|
|
57f5e3c30a | ||
|
|
32cd388b06 | ||
|
|
d3829ec630 | ||
|
|
1be204e22d | ||
|
|
5d4511fc6a | ||
|
|
dd1997e23c | ||
|
|
36914d1dc4 | ||
|
|
1fdc724761 | ||
|
|
3c6403224d | ||
|
|
88fb5e7ab5 | ||
|
|
8ed508012e | ||
|
|
58d744a342 | ||
|
|
8f72d8583e | ||
|
|
57d7dfaa0a | ||
|
|
4f4ef6f1f2 | ||
|
|
6eadea8485 | ||
|
|
607e0c5c1d | ||
|
|
e31b4b58ce | ||
|
|
e659b8eb6e | ||
|
|
962408c1ae | ||
|
|
c1d213a0cd | ||
|
|
7e5115ecd9 | ||
|
|
539d5dfc08 | ||
|
|
343f63a7cc | ||
|
|
e3268739ed | ||
|
|
df416af19b | ||
|
|
c0f026c332 | ||
|
|
6b7665947e | ||
|
|
4f7659fbf8 | ||
|
|
d4cf817b83 | ||
|
|
669bca5fa5 | ||
|
|
49b4eabe8b | ||
|
|
03acfc2b3c | ||
|
|
d2f30d2b12 | ||
|
|
ccaadeb582 | ||
|
|
b149f96500 | ||
|
|
132b1800c2 | ||
|
|
65ee2f9081 | ||
|
|
4faa9ddd8d | ||
|
|
48909dc3c4 | ||
|
|
f1941681ab | ||
|
|
72313c770c | ||
|
|
c05a69509e | ||
|
|
44ffdc4f97 | ||
|
|
5a6c2c5054 | ||
|
|
9d0ca089fe | ||
|
|
1c49dd80a4 | ||
|
|
cbc92e9f1e | ||
|
|
2fa81474b9 | ||
|
|
9b7d0563b9 | ||
|
|
e1e13bcfb1 | ||
|
|
134c23c70c | ||
|
|
e369b70aeb | ||
|
|
c3970255e6 | ||
|
|
7823eaf890 | ||
|
|
c89b6e2d6d | ||
|
|
0d08549a04 | ||
|
|
960f095c1b | ||
|
|
d9eff00a71 | ||
|
|
7b196e1ca5 | ||
|
|
0981517bc6 | ||
|
|
8ae29ceaa2 | ||
|
|
ec0079461e | ||
|
|
9eaa55b711 | ||
|
|
0c4b1cc4fc | ||
|
|
5501859fa6 | ||
|
|
70a1a7a5ea | ||
|
|
96f8832bcf | ||
|
|
0b54215b84 | ||
|
|
33ff74e534 | ||
|
|
23d3661ea5 | ||
|
|
c3dc165c4c | ||
|
|
46a6aff4da | ||
|
|
cb21eeda94 | ||
|
|
b27edb4259 | ||
|
|
73d85b9884 | ||
|
|
5a3619c737 | ||
|
|
dc41fe7616 | ||
|
|
227f06c1ec | ||
|
|
48c41df054 | ||
|
|
946dac3c9f | ||
|
|
b160ba1793 | ||
|
|
b86f2ba90d | ||
|
|
bf5a7e20d9 | ||
|
|
3393963363 | ||
|
|
e641e93fd5 | ||
|
|
99fcd3556e | ||
|
|
b82679deaf | ||
|
|
0cd446421d | ||
|
|
6014612046 | ||
|
|
eb211c0c8e | ||
|
|
e6a9e27802 | ||
|
|
33d51a51d1 | ||
|
|
ab4be85669 | ||
|
|
6c0dce580d | ||
|
|
4ccd84f9c4 | ||
|
|
59050a7bc6 | ||
|
|
3334fb0e99 | ||
|
|
96102fc878 | ||
|
|
24268bbf33 | ||
|
|
cd3f8f0c43 | ||
|
|
d3a8954605 | ||
|
|
1cda61e230 | ||
|
|
7f93060872 | ||
|
|
c3ef12f145 | ||
|
|
39106c1e14 | ||
|
|
607deb31dc | ||
|
|
9a35c04bf0 | ||
|
|
aca3e3db4f | ||
|
|
74f9166f3d | ||
|
|
977a2090fb | ||
|
|
14e6ea9393 | ||
|
|
3eb35f0aa6 | ||
|
|
92b7a35c58 | ||
|
|
366bca5f93 | ||
|
|
99807b4cd4 | ||
|
|
97a1bf15ef | ||
|
|
9409078069 | ||
|
|
5b35cf7456 | ||
|
|
a9d3dfab1a | ||
|
|
32126d1874 | ||
|
|
5f4af76d28 | ||
|
|
bff415c7cd | ||
|
|
1d84835fd5 | ||
|
|
88296480ec | ||
|
|
4f5bc77379 | ||
|
|
b4f868be91 | ||
|
|
b6f35a5c1e | ||
|
|
55fda698ec | ||
|
|
c2467e12ba | ||
|
|
df74ad0e18 | ||
|
|
e2bdc67bd2 | ||
|
|
3932054ea6 | ||
|
|
acf273e4e3 | ||
|
|
410bf7edfd | ||
|
|
345d97792f | ||
|
|
243fd17305 | ||
|
|
6b07c4179c | ||
|
|
74d911f856 | ||
|
|
ecb7f0a2f6 | ||
|
|
1e0e0aabf2 | ||
|
|
fa4f2aa5cc | ||
|
|
4b8febd7dc | ||
|
|
7cb8eb783a | ||
|
|
ef679f6722 | ||
|
|
7c73e44ab8 | ||
|
|
e533762f33 | ||
|
|
40c118df55 | ||
|
|
f43fc282d3 | ||
|
|
8616e2f25c | ||
|
|
4299fd28f0 | ||
|
|
302ff92b31 | ||
|
|
b62cc9c8e9 | ||
|
|
225c2ca6e6 | ||
|
|
e5bdd852ca | ||
|
|
591788403a | ||
|
|
f1b82e289d | ||
|
|
6443db64d7 | ||
|
|
e1fb022878 | ||
|
|
96bb282674 | ||
|
|
4623f36042 | ||
|
|
29e0964ebc | ||
|
|
043c23899a | ||
|
|
4cf5dc0791 | ||
|
|
deaf6ef068 | ||
|
|
75011ca0ff | ||
|
|
ba3f84fd6c | ||
|
|
f8c7f84c18 | ||
|
|
7772ac0a85 | ||
|
|
9329c2ebd9 | ||
|
|
65f182001b | ||
|
|
fe83c5faea | ||
|
|
5b860ee601 | ||
|
|
cb9839223e | ||
|
|
15c42fba5e | ||
|
|
f4ae8ea5ac | ||
|
|
e7d7291947 | ||
|
|
04121efb13 | ||
|
|
d9310d651a | ||
|
|
6b817d102b | ||
|
|
08a9371322 | ||
|
|
32b9134722 | ||
|
|
8b60200ec6 | ||
|
|
7155b6a191 | ||
|
|
f96da090d6 | ||
|
|
a57a772394 | ||
|
|
e9f6eefaeb | ||
|
|
4188f074ca | ||
|
|
e0a4ec8b87 | ||
|
|
122acb3eee | ||
|
|
9728f1ba80 | ||
|
|
93ca268ee7 | ||
|
|
b852dc86c0 | ||
|
|
361b5decbe | ||
|
|
cd3c2b4bf7 | ||
|
|
5718c1f287 | ||
|
|
8d8f203b8a | ||
|
|
f40ffacfbd | ||
|
|
d7caf5ed1a | ||
|
|
aa7cbc9f08 | ||
|
|
afb5ab7430 | ||
|
|
c2ba7cdbc7 | ||
|
|
2f8be445d6 | ||
|
|
ae435f67a5 | ||
|
|
09e1bac41c | ||
|
|
fc7fe41c98 | ||
|
|
c7308ce634 | ||
|
|
e45fa1380d | ||
|
|
e4eb80f643 | ||
|
|
cc6e071f48 | ||
|
|
283ea16627 | ||
|
|
15b33488c6 | ||
|
|
0e2be44e17 | ||
|
|
f949649ba3 | ||
|
|
b31a6f33a5 | ||
|
|
51ecbf15a9 | ||
|
|
0fd783e65e | ||
|
|
1da5fd106a | ||
|
|
17aafe6775 | ||
|
|
d89dfc5e30 | ||
|
|
cd586c81ee | ||
|
|
9064b9f849 | ||
|
|
9fc8760dc5 | ||
|
|
16e1e01234 | ||
|
|
22800e71df | ||
|
|
352b09a891 | ||
|
|
68f560e29b | ||
|
|
4622dd0e0d | ||
|
|
ed822d9f46 | ||
|
|
c1359d9677 | ||
|
|
ac33df2054 | ||
|
|
b800fcafb4 | ||
|
|
6b997928e5 | ||
|
|
ee533e2644 | ||
|
|
d6da8afdce | ||
|
|
b3fcbd91e4 | ||
|
|
51c6abb261 | ||
|
|
43b86d403c | ||
|
|
6a1399dd50 | ||
|
|
fe6c9f24d3 | ||
|
|
6e62472759 | ||
|
|
6c9f4a8fd5 | ||
|
|
8618cb950f | ||
|
|
4fb5d3fb20 | ||
|
|
e0669ebbf8 | ||
|
|
015fd5bc3a | ||
|
|
88d85706ad | ||
|
|
782d733bc9 | ||
|
|
1318019ccb | ||
|
|
fe314cf146 | ||
|
|
97a880c946 | ||
|
|
df66955594 | ||
|
|
07f055bd49 | ||
|
|
22d5b125bd | ||
|
|
1aa2c0f9de | ||
|
|
bd08e99080 | ||
|
|
66530ca868 | ||
|
|
ef3b4a5895 | ||
|
|
b4c2f2ecaa | ||
|
|
a739688780 | ||
|
|
790f6ce4ed | ||
|
|
40d7bb04b4 | ||
|
|
22d7cfc7fa | ||
|
|
d4c775b1f4 | ||
|
|
02611029fb | ||
|
|
14e4e6d6ea | ||
|
|
536c25c206 | ||
|
|
6fb65de100 | ||
|
|
043c4105db | ||
|
|
823792339f | ||
|
|
226ab7233b | ||
|
|
cf150891df | ||
|
|
9170c70f2a | ||
|
|
83d8bf37a6 | ||
|
|
c841ed6419 | ||
|
|
1fb21d537c | ||
|
|
ac80e9a1ac | ||
|
|
dbbb8e76ab | ||
|
|
916f055aec | ||
|
|
6d8c183160 | ||
|
|
9d2f484aa3 | ||
|
|
2dc0cfdee3 | ||
|
|
a25abd0ca4 | ||
|
|
3a9119cf29 | ||
|
|
c236e0765b | ||
|
|
f8fad95fef | ||
|
|
97ae295cb9 | ||
|
|
bd888dcde2 | ||
|
|
784274f8ae | ||
|
|
65c1eb3a63 | ||
|
|
eda6c6a4c3 | ||
|
|
7d7594818c | ||
|
|
8165635fad | ||
|
|
7cc8f67e24 | ||
|
|
87fc3bbb8e | ||
|
|
286a834f4a | ||
|
|
bbb2cc972f | ||
|
|
c14d28dc1e | ||
|
|
6a07e6ae01 | ||
|
|
556ec45efc | ||
|
|
754e09b0de | ||
|
|
308b2d95f3 | ||
|
|
87dfd2b3c8 | ||
|
|
b0bfb8006d | ||
|
|
d46274abf2 | ||
|
|
23f7889cff | ||
|
|
534659cdc6 | ||
|
|
1e68d4ec87 | ||
|
|
1779fd3e8b | ||
|
|
3c496ddd9d | ||
|
|
66053ae9df | ||
|
|
77348bb9a4 | ||
|
|
14257ae422 | ||
|
|
e64c956693 | ||
|
|
4a6b246f0f | ||
|
|
6f7bc54a39 | ||
|
|
47bc9d8ef1 | ||
|
|
a3a5fe056d | ||
|
|
fbb3271c81 | ||
|
|
ecc93d9246 | ||
|
|
302672f5b0 | ||
|
|
4f16ea2d2d | ||
|
|
b7a0b7d629 | ||
|
|
bd6f1bef10 | ||
|
|
c4941bb102 | ||
|
|
b8a606a35f | ||
|
|
370eebeb64 | ||
|
|
35bcb082a0 | ||
|
|
0c8678eb87 | ||
|
|
dd220e228e | ||
|
|
7b63aa4a4f | ||
|
|
33a07346dd | ||
|
|
abd77559ab | ||
|
|
28878caca9 | ||
|
|
74f3379b5d | ||
|
|
379770343a | ||
|
|
6327286328 | ||
|
|
3a2677a91a | ||
|
|
fcd232aa35 | ||
|
|
5dd14b929a | ||
|
|
f194e2c1c6 | ||
|
|
ea6731e22b | ||
|
|
002b1679c3 | ||
|
|
45f3a67950 | ||
|
|
c6917bb0cf | ||
|
|
f777845d14 | ||
|
|
a1f5bcae80 | ||
|
|
3e11b4aa74 | ||
|
|
4f48236fee | ||
|
|
ffadf29ad7 | ||
|
|
352efcb610 | ||
|
|
334e83479f | ||
|
|
476eedbd2c | ||
|
|
ae7e28b71b | ||
|
|
be30174a49 | ||
|
|
8373654f80 | ||
|
|
471c636580 | ||
|
|
635c6efe42 | ||
|
|
d570048f78 | ||
|
|
dcc49dafd3 | ||
|
|
7398f7ce0d | ||
|
|
06fadc45f2 | ||
|
|
f633a673c4 | ||
|
|
76479a2486 | ||
|
|
31f62dcc12 | ||
|
|
3d7df5b005 | ||
|
|
c16a116707 | ||
|
|
f7f06f59ce | ||
|
|
d1277afee6 | ||
|
|
a510d01136 | ||
|
|
0e651df65f | ||
|
|
758e0458bc | ||
|
|
e18b4666ba | ||
|
|
864088eecd | ||
|
|
0b39318b33 | ||
|
|
d5a9961ec8 | ||
|
|
7dac7de365 | ||
|
|
dd0721e91e | ||
|
|
e8d2c5e30d | ||
|
|
21fde2e991 | ||
|
|
ca1893164d | ||
|
|
b619ac3e08 | ||
|
|
2992ec064f | ||
|
|
911281b7b9 | ||
|
|
d7eb86c86d | ||
|
|
6c4f216da8 | ||
|
|
f786a00e89 | ||
|
|
47cecb2ac4 | ||
|
|
5d6ceec803 | ||
|
|
bec11220e3 | ||
|
|
9b802e1c7d | ||
|
|
16cf16c422 | ||
|
|
4e1eee197e | ||
|
|
91c8af9e38 | ||
|
|
58593a9428 | ||
|
|
21aa8b0703 | ||
|
|
17cf57f7ca | ||
|
|
f7cfe36f37 | ||
|
|
c26f909565 | ||
|
|
6db7fe5f7b | ||
|
|
a207114d95 | ||
|
|
b8299a5ea5 | ||
|
|
1fa461e996 | ||
|
|
2e3745099b | ||
|
|
03ebeb0657 | ||
|
|
6892cffe54 | ||
|
|
19a613e90c | ||
|
|
7fe95f218b | ||
|
|
a1fc785771 | ||
|
|
e0034dc205 | ||
|
|
bd9eab08b7 | ||
|
|
e2e6e7db85 | ||
|
|
b5121657ee | ||
|
|
4f04dbc294 | ||
|
|
2b2a84da64 | ||
|
|
21dd9a260c | ||
|
|
7b9b5bafc1 | ||
|
|
41ebba6ce0 | ||
|
|
61446592b3 | ||
|
|
b82c6326cf | ||
|
|
a2f466810b | ||
|
|
1bd1782d66 | ||
|
|
ea6a1c05fa | ||
|
|
4f84e77b10 | ||
|
|
fa75a3539f | ||
|
|
fa12d9785a | ||
|
|
c578e31ae2 | ||
|
|
749c369080 | ||
|
|
4ad4057878 | ||
|
|
2dea0b52ed | ||
|
|
ca257d1caf | ||
|
|
e164692391 | ||
|
|
b58edea544 | ||
|
|
9a587c91a8 | ||
|
|
7590a7ce4d | ||
|
|
aae1571a5c | ||
|
|
ebaf30727c | ||
|
|
884ceb052b | ||
|
|
cc7ed497e8 | ||
|
|
cd6a739abb | ||
|
|
f0cecfd517 | ||
|
|
f5f255e2d5 | ||
|
|
5ffa56be3d | ||
|
|
e65c0d9f48 | ||
|
|
076cb0e35b | ||
|
|
2a90ca6546 | ||
|
|
2b492134be | ||
|
|
a26deafa75 | ||
|
|
cf705e352b | ||
|
|
86c5ca4213 | ||
|
|
179d534237 | ||
|
|
162507264c | ||
|
|
7e0a8b6227 | ||
|
|
b50fcee079 | ||
|
|
9bca42c14a | ||
|
|
475d14edf4 | ||
|
|
214733c880 | ||
|
|
979828ffe3 | ||
|
|
65bb795199 | ||
|
|
7bdc97fbfa | ||
|
|
a0546b2e63 | ||
|
|
d6f6d78b1e | ||
|
|
8c1fba5160 | ||
|
|
fb39dd5440 | ||
|
|
dd0c5b7806 | ||
|
|
9e94cf7b99 | ||
|
|
b882b9e283 | ||
|
|
cdcff62232 | ||
|
|
c8caca77a3 | ||
|
|
f291125377 | ||
|
|
0ce981a68c | ||
|
|
a8814dcaba | ||
|
|
229eeae6db | ||
|
|
d03788af93 | ||
|
|
017aad6454 | ||
|
|
5059d71509 | ||
|
|
042b3a71d8 | ||
|
|
767ec37b83 | ||
|
|
89f64e0c49 | ||
|
|
eadae5e2cd | ||
|
|
d108ad904e | ||
|
|
6564736d3e | ||
|
|
7f9c4df284 | ||
|
|
d01cd70c6b | ||
|
|
ea7768117c | ||
|
|
5bfb39cdf6 | ||
|
|
76c054a591 | ||
|
|
4471dca3f3 | ||
|
|
29f1c2bdad | ||
|
|
e79f9ba40f | ||
|
|
452aabdec6 | ||
|
|
6c3b82ed85 | ||
|
|
f91434433e | ||
|
|
b6f82be56a | ||
|
|
9e3f8e7827 | ||
|
|
3a4e9ccc5a | ||
|
|
860e32d965 | ||
|
|
495f9dfa84 | ||
|
|
133ca33cb5 | ||
|
|
1c69a9fd8a | ||
|
|
15faa57e01 | ||
|
|
f5510234cf | ||
|
|
c633970f9d | ||
|
|
95e5b1ec5e | ||
|
|
eb720b053a | ||
|
|
efc61241a0 | ||
|
|
5b0331611d | ||
|
|
f8f1c58f61 | ||
|
|
3f34aa92fa | ||
|
|
c99102e49b | ||
|
|
d583661e58 | ||
|
|
cfad1d178f | ||
|
|
ffa326e08f | ||
|
|
c24b2dadec | ||
|
|
03040ed40b | ||
|
|
5e89cd1cb3 | ||
|
|
bf202473e9 | ||
|
|
9a3b5337d7 | ||
|
|
396cbb27b2 | ||
|
|
b4e6f8bc73 | ||
|
|
d88f28f5c2 | ||
|
|
886c0c596f | ||
|
|
b15b394c65 | ||
|
|
e36cf1d963 | ||
|
|
caf78a6b4d | ||
|
|
6a161267ba | ||
|
|
a0bb5e5ef3 | ||
|
|
34cc211912 | ||
|
|
e95713c1df | ||
|
|
e189dc965d | ||
|
|
a180c33a32 | ||
|
|
ea8febdb7d | ||
|
|
f765cc8dbc | ||
|
|
81b7972347 | ||
|
|
1281670c61 | ||
|
|
b8c6103858 | ||
|
|
b2c0bed84c | ||
|
|
9619fcbc1f | ||
|
|
e9c55e9eb4 | ||
|
|
53f580ad40 | ||
|
|
cf0045681e | ||
|
|
488d034a58 | ||
|
|
762a883b39 | ||
|
|
a63ded1ba1 | ||
|
|
8d66275187 | ||
|
|
f812b28892 | ||
|
|
59063e861c | ||
|
|
873c9b1903 | ||
|
|
9da891e9b0 | ||
|
|
a6de12323e | ||
|
|
f84b3187a5 | ||
|
|
087f779fef | ||
|
|
edeb16bc26 | ||
|
|
52d099c80e | ||
|
|
b2010e5fd8 | ||
|
|
5808bd3743 | ||
|
|
d5f5c440dd | ||
|
|
90d947391a | ||
|
|
729f679c0f | ||
|
|
9f52709a42 | ||
|
|
47cc80a93f | ||
|
|
85444f5a47 | ||
|
|
50df2279a7 | ||
|
|
71ba0242c7 | ||
|
|
1f8cfde1cf | ||
|
|
5f2ec595cb | ||
|
|
37a6446e32 | ||
|
|
be84b1cb01 | ||
|
|
9fb91b3052 | ||
|
|
689aab32c9 | ||
|
|
c642f4afa2 | ||
|
|
a62a083294 | ||
|
|
2a13c2ec00 | ||
|
|
f3525b9ff2 | ||
|
|
d2509f4b97 | ||
|
|
6d8c424710 | ||
|
|
93e8657f73 | ||
|
|
91f6c001c0 | ||
|
|
0dd8300a80 | ||
|
|
298db46722 | ||
|
|
c2b97b13a1 | ||
|
|
8feb5dabb0 | ||
|
|
8aaa04b1f8 | ||
|
|
a28117b301 | ||
|
|
0117a4767d | ||
|
|
0c6b0598fa | ||
|
|
f2a2d772b0 | ||
|
|
bf60bf1848 | ||
|
|
c581395df2 | ||
|
|
8a44fb689a | ||
|
|
9fd36526ef | ||
|
|
78f4d9cc5d | ||
|
|
bd2a3e197a | ||
|
|
d703205921 | ||
|
|
1e73bec2b9 | ||
|
|
1abbeb0273 | ||
|
|
d25424d325 | ||
|
|
cc98ac5853 | ||
|
|
05750c3b38 | ||
|
|
3ddecef5a7 | ||
|
|
7fd96a0533 | ||
|
|
19d6f4381a | ||
|
|
25e9129a8e | ||
|
|
569674452a | ||
|
|
2643dae0a7 | ||
|
|
1f53e48032 | ||
|
|
ef3a47b492 | ||
|
|
f302c724c4 | ||
|
|
4ce654511b | ||
|
|
2f68310a7c | ||
|
|
67cd855e97 | ||
|
|
ceaafdbb1c | ||
|
|
ca283c2d26 | ||
|
|
8dea5d5158 | ||
|
|
7ea5c79393 | ||
|
|
9008eb218b | ||
|
|
089b77379d | ||
|
|
215148ca81 | ||
|
|
897960194e | ||
|
|
5d97f4b924 | ||
|
|
e7b663749a | ||
|
|
aa5999b2e0 | ||
|
|
736d75a93c | ||
|
|
c81a17ada5 | ||
|
|
401a28f317 | ||
|
|
f16caa2b98 | ||
|
|
868af29d14 | ||
|
|
a091c9c910 | ||
|
|
1d1d4d9371 | ||
|
|
d578659b21 | ||
|
|
2be9cebb0e | ||
|
|
982b900066 | ||
|
|
4c1bc7c3c1 | ||
|
|
fb09959459 | ||
|
|
5601ed7071 | ||
|
|
5ab282c344 | ||
|
|
5eb4b28834 | ||
|
|
4cdea36b7c | ||
|
|
f2c1d4d83d | ||
|
|
9dd0cd57ce | ||
|
|
3026bd8aaf | ||
|
|
2ec27de353 | ||
|
|
f73e5446ab | ||
|
|
df255b5a6f | ||
|
|
9a3c953f0f | ||
|
|
02c031a0ef | ||
|
|
a5114369ba | ||
|
|
4765685440 | ||
|
|
3f3c3a3df4 | ||
|
|
baa52d432f | ||
|
|
89562d0231 | ||
|
|
3df9c88bb7 | ||
|
|
cacee40d11 | ||
|
|
1782837a38 | ||
|
|
de05521b57 | ||
|
|
c86afca1d0 | ||
|
|
5617ca24b8 | ||
|
|
c0cd980f5f | ||
|
|
cd51f2f652 | ||
|
|
2f99d17885 | ||
|
|
63ffa704f5 | ||
|
|
129b7afda9 | ||
|
|
00bb988ecc | ||
|
|
5efc56eb5a | ||
|
|
0ccae600bc | ||
|
|
b0af94415f | ||
|
|
1c09670e3c | ||
|
|
0db1eed87f | ||
|
|
db52d98595 | ||
|
|
e46b5b3f57 | ||
|
|
d1d3b4353a | ||
|
|
3c0944ebfc | ||
|
|
a2725ed8fe | ||
|
|
84bfef9938 | ||
|
|
6169f5c2e8 | ||
|
|
fc14871d01 | ||
|
|
bcfa1d4394 | ||
|
|
2361b2b63a | ||
|
|
380ead2ad6 | ||
|
|
3df45d697d | ||
|
|
efb70f0b97 | ||
|
|
d9a941cb32 | ||
|
|
f43ca1869b | ||
|
|
a34b06cfb4 | ||
|
|
f5b910d391 | ||
|
|
924c1d60f9 | ||
|
|
6b80f19e5f | ||
|
|
b57e68f4b6 | ||
|
|
000d2c3935 | ||
|
|
d2311f066a | ||
|
|
91435bf372 | ||
|
|
aa39de4ea8 | ||
|
|
215f6fc0ab | ||
|
|
ea5c22c244 | ||
|
|
fc333ae098 | ||
|
|
d3e891eec3 | ||
|
|
66d30e35c7 | ||
|
|
b8693c3f85 | ||
|
|
4b2742efca | ||
|
|
e07c1bba7a | ||
|
|
ec56a4149b | ||
|
|
314742a563 | ||
|
|
38c9e3e7cc | ||
|
|
c1435fba95 | ||
|
|
b13148265e | ||
|
|
c0174ab501 | ||
|
|
77c45ed109 | ||
|
|
7df68bb8bd | ||
|
|
ab461ba560 | ||
|
|
280252d40e | ||
|
|
09b6989491 | ||
|
|
f96bbb38b0 | ||
|
|
f0e3dc2f0e | ||
|
|
d2937a76d9 | ||
|
|
3219c150d4 | ||
|
|
ba167f256b | ||
|
|
0e92bcc0de | ||
|
|
f6bfe3931c | ||
|
|
253b9e5bd8 | ||
|
|
604f80de20 | ||
|
|
0aa8f7bfc6 | ||
|
|
4cc2c736e9 | ||
|
|
9455b56c07 | ||
|
|
d6f528acd2 | ||
|
|
9344fb958a | ||
|
|
2533d0ebc0 | ||
|
|
6ee9025f13 | ||
|
|
3a2e868d80 | ||
|
|
86a732600b | ||
|
|
22f3dba842 | ||
|
|
bde1cd3f5f | ||
|
|
f187012469 | ||
|
|
3917215952 | ||
|
|
5f0c036ad5 | ||
|
|
f0342f25ea | ||
|
|
f5a8c806d9 | ||
|
|
f8cbe1dfbf | ||
|
|
c6c9e7d179 | ||
|
|
36ac81bb43 | ||
|
|
1515498a23 | ||
|
|
d0059cbe29 | ||
|
|
34cf6e9e58 | ||
|
|
05e283baec | ||
|
|
457da6f23e | ||
|
|
0f16f65d30 | ||
|
|
1c2b6f5ab5 | ||
|
|
f011d94339 | ||
|
|
d527bbd8b5 | ||
|
|
2015c484d0 | ||
|
|
480d5ba7c3 | ||
|
|
6b3bfc85b3 | ||
|
|
2b2bc73564 | ||
|
|
7cdfd5a6d3 | ||
|
|
8bcc2a4932 | ||
|
|
deef8abca5 | ||
|
|
f683d65990 | ||
|
|
93ec352f4a | ||
|
|
e0b009c538 | ||
|
|
5c61d874be | ||
|
|
5cf64c1440 | ||
|
|
ed91c7ca32 | ||
|
|
fdbec9917c | ||
|
|
5ae28da709 | ||
|
|
dba94237dc | ||
|
|
802846a838 | ||
|
|
c2aa4f4893 | ||
|
|
25c875dc55 | ||
|
|
2c5289d338 | ||
|
|
00c5d58203 | ||
|
|
5cf54c6384 | ||
|
|
7c75af83b3 | ||
|
|
39b2c1722a | ||
|
|
40910703ee | ||
|
|
cfc01e03f6 | ||
|
|
10ef9d696c | ||
|
|
138ece085e | ||
|
|
19b2f330dd | ||
|
|
e48aa909da | ||
|
|
b122db447a | ||
|
|
642c4fc9d1 | ||
|
|
4d57f33371 | ||
|
|
60c63e4558 | ||
|
|
aa90232bf9 | ||
|
|
cf7439b1b4 | ||
|
|
60cba6c9f3 | ||
|
|
3eaa997145 | ||
|
|
28cca332e7 | ||
|
|
025034cb71 | ||
|
|
7fd75336a5 | ||
|
|
af5a189d04 | ||
|
|
7ec80a375a | ||
|
|
285119b2e5 | ||
|
|
0539337121 | ||
|
|
50fe715fba | ||
|
|
e5daa00d73 | ||
|
|
fd58813ec9 | ||
|
|
111add1ed6 | ||
|
|
12d65c7743 | ||
|
|
a100d1d11a | ||
|
|
28c4053ad3 | ||
|
|
e84d9358d1 | ||
|
|
2d25df33ce | ||
|
|
c51778f391 | ||
|
|
20333d8179 | ||
|
|
6454e878dd | ||
|
|
d13b9ef3ea | ||
|
|
26fa2a71ea | ||
|
|
46ed61f070 | ||
|
|
09cd45fd8b | ||
|
|
d162e3e11b | ||
|
|
a5dd2683cd | ||
|
|
ebda46f748 | ||
|
|
0d8c98dcfe | ||
|
|
174ca6bed5 | ||
|
|
271be57c99 | ||
|
|
423d2fbb92 | ||
|
|
21b15167dd | ||
|
|
59005e3bb8 | ||
|
|
60f637e947 | ||
|
|
8ded4811bb | ||
|
|
3900b37f5c | ||
|
|
cfb5a98cac | ||
|
|
03ddb32556 | ||
|
|
1398bcbc8c | ||
|
|
93d9438f6c | ||
|
|
0fce6c5ebb | ||
|
|
a03eeb63e7 | ||
|
|
4dd7880744 | ||
|
|
5ab7123566 | ||
|
|
ba4732c526 | ||
|
|
73fb95976c | ||
|
|
b473b7905d | ||
|
|
ba36023ae6 | ||
|
|
48e7cd28b3 | ||
|
|
54711b0d25 | ||
|
|
7e61acc4da | ||
|
|
d7ca4d49dc | ||
|
|
df858c2c7d | ||
|
|
9d3a282c0a | ||
|
|
9174bb140b | ||
|
|
54da6832f3 | ||
|
|
508f4fcd3c | ||
|
|
eaaff76aad | ||
|
|
0d0b5ead86 | ||
|
|
7f6bfacff1 | ||
|
|
7bd5d31094 | ||
|
|
645bc32121 | ||
|
|
6e55260160 | ||
|
|
2abbb0d359 | ||
|
|
b13238eb30 | ||
|
|
f09cfedebc | ||
|
|
0a86d9d515 | ||
|
|
808ed6a98b | ||
|
|
2bb59671dd | ||
|
|
fb6ebcd074 | ||
|
|
44a2a63fb8 | ||
|
|
eae19e8252 | ||
|
|
1b05e7aa36 | ||
|
|
be9a2767ea | ||
|
|
6a18791c30 | ||
|
|
2cc3f65323 | ||
|
|
63d1b558d1 | ||
|
|
cfbfda925b | ||
|
|
fd1ab29920 | ||
|
|
35f4a07d27 | ||
|
|
9ce3f6da45 | ||
|
|
b8b199c5ec | ||
|
|
e492284abe | ||
|
|
3d5e064358 | ||
|
|
6f8ce1fc5a | ||
|
|
5ae30ea9bc | ||
|
|
8959d39356 | ||
|
|
aded9f1a36 | ||
|
|
3a34eb1357 | ||
|
|
c2564eaf65 | ||
|
|
53d3b2abbc | ||
|
|
5d90c463a3 | ||
|
|
5309da2eee | ||
|
|
d1dd13fde9 | ||
|
|
0a83306015 | ||
|
|
203a39f07c | ||
|
|
85579b253f | ||
|
|
1ca751bc42 | ||
|
|
f344eee778 | ||
|
|
28c2197ba7 | ||
|
|
0721760900 | ||
|
|
45c77f97ce | ||
|
|
05fb46a573 | ||
|
|
ad6a864478 | ||
|
|
37fcc74ef8 | ||
|
|
c4cf745d77 | ||
|
|
85108672bf | ||
|
|
969c9105fd | ||
|
|
073621f29a | ||
|
|
bbc0089166 | ||
|
|
268f1d40aa | ||
|
|
e9a28b034f | ||
|
|
bfca324623 | ||
|
|
279c6337e4 | ||
|
|
368917f7f5 | ||
|
|
f973faa409 | ||
|
|
a0339132dd | ||
|
|
40f947fc9f | ||
|
|
d2bedec59c | ||
|
|
1b93ccdec9 | ||
|
|
2921b62b37 | ||
|
|
83f3ae1fbd | ||
|
|
66d97cb2e0 | ||
|
|
4ecaaba1e5 | ||
|
|
c52da573c5 | ||
|
|
4ac18e2ef0 | ||
|
|
f05e1354ff | ||
|
|
bafe2ab985 | ||
|
|
0d13a12d64 | ||
|
|
deb0fab156 | ||
|
|
c181887266 | ||
|
|
a624a10c85 | ||
|
|
6295fbf7e2 | ||
|
|
c6a7ad0520 | ||
|
|
de89dfe27f | ||
|
|
a8463f349a | ||
|
|
3b6ed823b9 | ||
|
|
80e17f8cfc | ||
|
|
8fae4550c3 | ||
|
|
9125b46ca5 | ||
|
|
7ed25d2dba | ||
|
|
0fa8aca6e2 | ||
|
|
7be79c10fd | ||
|
|
f095e1b29f | ||
|
|
96822082bd | ||
|
|
c637912337 | ||
|
|
5b6fc19e00 | ||
|
|
f6435461a1 | ||
|
|
83e51abd35 | ||
|
|
dab88404c0 | ||
|
|
c66ff74f21 | ||
|
|
769655f565 | ||
|
|
139dd7d80f | ||
|
|
62cea62356 | ||
|
|
58fa10a0d5 | ||
|
|
395a91c00c | ||
|
|
741bf3b666 | ||
|
|
fbce59e81f | ||
|
|
ac58a5b8fa | ||
|
|
42230f2630 | ||
|
|
9a188fd792 | ||
|
|
6c68a34d63 | ||
|
|
746b448f30 | ||
|
|
b5aba58aac | ||
|
|
2bb7726180 | ||
|
|
24b607cad3 | ||
|
|
087d0e90e6 | ||
|
|
e448951d6f | ||
|
|
ecbedf847f | ||
|
|
3efd5cb9e8 | ||
|
|
e1dc964c4c | ||
|
|
295c932646 | ||
|
|
5b9f4128f3 | ||
|
|
100f1229ce | ||
|
|
adf24cd274 | ||
|
|
de1d154e9c | ||
|
|
4345584384 | ||
|
|
ba1adf91ca | ||
|
|
024b08de06 | ||
|
|
4e5eabbf05 | ||
|
|
c9258b5526 | ||
|
|
ce9fb80558 | ||
|
|
cd8e2540de | ||
|
|
9a2ee806e4 | ||
|
|
b192887d19 | ||
|
|
b6e918b024 | ||
|
|
ed174d16d1 | ||
|
|
6b8108afda | ||
|
|
49fd000217 | ||
|
|
0c3f47b0c3 | ||
|
|
2d53b96a15 | ||
|
|
540875c41e | ||
|
|
595f153d85 | ||
|
|
0cbc3487b0 | ||
|
|
978ac68474 | ||
|
|
045e83e871 | ||
|
|
0239139b4b | ||
|
|
8c411e7727 | ||
|
|
16d0b925fa | ||
|
|
f306ddb51f | ||
|
|
4faf9bbff1 | ||
|
|
44b6d1a516 | ||
|
|
4ed6e1e8ec | ||
|
|
4437fc43e4 | ||
|
|
253e5a11fc | ||
|
|
6e734f2eac | ||
|
|
30edca024a | ||
|
|
c14f783d94 | ||
|
|
8f3452c0af | ||
|
|
97a1b59861 | ||
|
|
41aad7558b | ||
|
|
d48616d510 | ||
|
|
8296e72887 | ||
|
|
2648e71f5b | ||
|
|
5771f2f8aa | ||
|
|
f86156b619 | ||
|
|
8ff0015458 | ||
|
|
84ecb99400 | ||
|
|
5160ae364d | ||
|
|
b9e40b4d82 | ||
|
|
4b67c0593d | ||
|
|
fb1429956a | ||
|
|
c70d20f95d | ||
|
|
47d211cd87 | ||
|
|
7fd223893b | ||
|
|
1794859468 | ||
|
|
c5c8be4b4a | ||
|
|
e13d543dcd | ||
|
|
69fad7a920 | ||
|
|
2da5dcb619 | ||
|
|
3260130042 | ||
|
|
c77316f91e | ||
|
|
018464aedf | ||
|
|
2982dde7df | ||
|
|
acb7ca5440 | ||
|
|
8a7fdaa6ca | ||
|
|
27c574f0a7 | ||
|
|
6d82d54560 | ||
|
|
cba62c0172 | ||
|
|
47b455ba87 | ||
|
|
3f29071139 | ||
|
|
ccdfac7b8e | ||
|
|
be959ca90c | ||
|
|
2929783d35 | ||
|
|
838843be45 | ||
|
|
1225d72917 | ||
|
|
9c4a310d84 | ||
|
|
d3e9fd9a36 | ||
|
|
d2c63e0857 | ||
|
|
52c00367d7 | ||
|
|
6ee844b48f | ||
|
|
542ad58c32 | ||
|
|
4781cf1037 | ||
|
|
5aa3b432bf | ||
|
|
47401aae99 | ||
|
|
27473003b7 | ||
|
|
fa8bc7d40d | ||
|
|
f352cdf9a3 | ||
|
|
3d68d454fe | ||
|
|
9a9eca7813 | ||
|
|
f3d82c915b | ||
|
|
5185d1eaa5 | ||
|
|
6abf792e0f | ||
|
|
4d56f86719 | ||
|
|
f033814f96 | ||
|
|
2e3dbfeb27 | ||
|
|
a50e431c7a | ||
|
|
59e18e8d4c | ||
|
|
731299fe60 | ||
|
|
5cb2c1bbe4 | ||
|
|
5362d9f5e9 | ||
|
|
981cd92b26 | ||
|
|
77b70cac60 | ||
|
|
ac8775565b | ||
|
|
5651b72c0f | ||
|
|
3e54ab1c3c | ||
|
|
81482c8350 | ||
|
|
9f25c6eb09 | ||
|
|
4d778e71fe | ||
|
|
36d0e6eb36 | ||
|
|
492a975a3a | ||
|
|
ac14a6315b | ||
|
|
37abe7d7f1 | ||
|
|
0cebd89c01 | ||
|
|
2f49b419bd | ||
|
|
c144a20012 | ||
|
|
1cf79fb56e | ||
|
|
68724e6236 | ||
|
|
f529b339c6 | ||
|
|
b991391667 | ||
|
|
88540eeedd | ||
|
|
7e87362a39 | ||
|
|
c6da42ee35 | ||
|
|
f88bb4e204 | ||
|
|
c0919aff51 | ||
|
|
3bb5db6490 | ||
|
|
33166032f1 | ||
|
|
5233654da2 | ||
|
|
4d4a3a512d | ||
|
|
411fe5448b | ||
|
|
f052c81ee1 | ||
|
|
b170a619cd | ||
|
|
4d2f82e03a | ||
|
|
da738ba1e9 | ||
|
|
2f84978274 | ||
|
|
1d88c7e92d | ||
|
|
5af9bb1cdb | ||
|
|
c8130e9453 | ||
|
|
ccd687cbf3 | ||
|
|
ca73a79cfe |
@@ -1,6 +1,124 @@
|
||||
version: 2
|
||||
version: 2.1
|
||||
jobs:
|
||||
build:
|
||||
test-common:
|
||||
docker:
|
||||
- image: penpotapp/devenv:latest
|
||||
|
||||
working_directory: ~/repo
|
||||
resource_class: medium+
|
||||
|
||||
environment:
|
||||
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "common/deps.edn"}}
|
||||
|
||||
- run:
|
||||
name: "fmt check & linter"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "JVM tests"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
clojure -M:dev:test
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "common/deps.edn"}}
|
||||
|
||||
|
||||
test-frontend:
|
||||
docker:
|
||||
- image: penpotapp/devenv:latest
|
||||
|
||||
working_directory: ~/repo
|
||||
resource_class: medium+
|
||||
|
||||
environment:
|
||||
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "frontend/deps.edn"}}
|
||||
|
||||
- run:
|
||||
name: "prepopulate linter cache"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "fmt check & linter"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
yarn run fmt:js:check
|
||||
yarn run lint:scss
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "unit tests"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run test
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "frontend/deps.edn"}}
|
||||
|
||||
|
||||
test-integration:
|
||||
docker:
|
||||
- image: penpotapp/devenv:latest
|
||||
|
||||
working_directory: ~/repo
|
||||
resource_class: large
|
||||
|
||||
environment:
|
||||
JAVA_OPTS: -Xmx6g -Xms2g
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "frontend/deps.edn"}}
|
||||
|
||||
- run:
|
||||
name: "integration tests"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run build:app:assets
|
||||
yarn run build:app
|
||||
yarn run build:app:libs
|
||||
yarn run playwright install --with-deps chromium
|
||||
yarn run test:e2e -x --workers=4
|
||||
|
||||
test-backend:
|
||||
docker:
|
||||
- image: penpotapp/devenv:latest
|
||||
- image: cimg/postgres:14.5
|
||||
@@ -20,103 +138,30 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- run: cd .clj-kondo && cat config.edn
|
||||
- run: cat .cljfmt.edn
|
||||
- run: clj-kondo --version
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "backend/deps.edn" }}
|
||||
|
||||
- run:
|
||||
name: "backend fmt check"
|
||||
working_directory: "./backend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
|
||||
- run:
|
||||
name: "exporter fmt check"
|
||||
working_directory: "./exporter"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
|
||||
- run:
|
||||
name: "common fmt check"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
|
||||
- run:
|
||||
name: "frontend fmt check"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
|
||||
- run:
|
||||
name: "common linter check"
|
||||
name: "prepopulate linter cache"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "frontend linter check"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run lint:scss
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "backend linter check"
|
||||
name: "fmt check & linter"
|
||||
working_directory: "./backend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "exporter linter check"
|
||||
working_directory: "./exporter"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "common tests"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn test
|
||||
clojure -M:dev:test
|
||||
|
||||
- run:
|
||||
name: "frontend tests"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn test
|
||||
|
||||
- run:
|
||||
name: "frontend integration tests"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run compile
|
||||
clojure -M:dev:shadow-cljs release main
|
||||
yarn playwright install --with-deps chromium
|
||||
yarn e2e:test
|
||||
|
||||
- run:
|
||||
name: "backend tests"
|
||||
name: "tests"
|
||||
working_directory: "./backend"
|
||||
command: |
|
||||
clojure -M:dev:test
|
||||
clojure -M:dev:test --reporter kaocha.report/documentation
|
||||
|
||||
environment:
|
||||
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
||||
@@ -125,7 +170,45 @@ jobs:
|
||||
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "backend/deps.edn" }}
|
||||
|
||||
|
||||
test-exporter:
|
||||
docker:
|
||||
- image: penpotapp/devenv:latest
|
||||
|
||||
working_directory: ~/repo
|
||||
resource_class: medium+
|
||||
|
||||
environment:
|
||||
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: "prepopulate linter cache"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run lint:clj
|
||||
|
||||
- run:
|
||||
name: "fmt check & linter"
|
||||
working_directory: "./exporter"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
yarn run lint:clj
|
||||
|
||||
workflows:
|
||||
penpot:
|
||||
jobs:
|
||||
- test-frontend
|
||||
- test-integration
|
||||
- test-backend
|
||||
- test-common
|
||||
- test-exporter
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
promesa.core/->> clojure.core/->>
|
||||
promesa.core/-> clojure.core/->
|
||||
promesa.exec.csp/go-loop clojure.core/loop
|
||||
rumext.v2/defc clojure.core/defn
|
||||
promesa.util/with-open clojure.core/with-open
|
||||
app.common.schema.generators/let clojure.core/let
|
||||
app.common.data/export clojure.core/def
|
||||
@@ -20,6 +19,7 @@
|
||||
app.db/with-atomic hooks.export/penpot-with-atomic
|
||||
potok.v2.core/reify hooks.export/potok-reify
|
||||
rumext.v2/fnc hooks.export/rumext-fnc
|
||||
rumext.v2/defc hooks.export/rumext-defc
|
||||
rumext.v2/lazy-component hooks.export/rumext-lazycomponent
|
||||
shadow.lazy/loadable hooks.export/rumext-lazycomponent
|
||||
}}
|
||||
|
||||
@@ -67,12 +67,86 @@
|
||||
(let [[cname mdata params & body] (rest (:children node))
|
||||
[params body] (if (api/vector-node? mdata)
|
||||
[mdata (cons params body)]
|
||||
[params body])]
|
||||
(let [result (api/list-node
|
||||
(into [(api/token-node 'fn)
|
||||
params]
|
||||
(cons mdata body)))]
|
||||
{:node result})))
|
||||
[params body])
|
||||
|
||||
result (api/list-node
|
||||
(into [(api/token-node 'fn) params]
|
||||
(cons mdata body)))]
|
||||
|
||||
{:node result}))
|
||||
|
||||
|
||||
(defn- parse-defc
|
||||
[{:keys [children] :as node}]
|
||||
(let [args (rest children)
|
||||
|
||||
[cname args]
|
||||
(if (api/token-node? (first args))
|
||||
[(first args) (rest args)]
|
||||
(throw (ex-info "unexpected1" {})))
|
||||
|
||||
[docs args]
|
||||
(if (api/string-node? (first args))
|
||||
[(first args) (rest args)]
|
||||
["" args])
|
||||
|
||||
[mdata args]
|
||||
(if (api/map-node? (first args))
|
||||
[(first args) (rest args)]
|
||||
[(api/map-node []) args])
|
||||
|
||||
[params body]
|
||||
(if (api/vector-node? (first args))
|
||||
[(first args) (rest args)]
|
||||
(throw (ex-info "unexpected2" {})))]
|
||||
|
||||
[cname docs mdata params body]))
|
||||
|
||||
(defn rumext-defc
|
||||
[{:keys [node]}]
|
||||
(let [[cname docs mdata params body] (parse-defc node)
|
||||
|
||||
param1 (first (:children params))
|
||||
paramN (rest (:children params))
|
||||
|
||||
param1 (if (api/map-node? param1)
|
||||
(let [param1 (into {} (comp
|
||||
(partition-all 2)
|
||||
(map (fn [[k v]]
|
||||
[(if (api/keyword-node? k)
|
||||
(:k k)
|
||||
k)
|
||||
(if (api/vector-node? v)
|
||||
(vec (:children v))
|
||||
v)])))
|
||||
(:children param1))
|
||||
|
||||
binding (:rest param1)
|
||||
param1 (if binding
|
||||
(if (contains? param1 :as)
|
||||
(update param1 :keys (fnil conj []) binding)
|
||||
(assoc param1 :as binding))
|
||||
param1)]
|
||||
(->> (dissoc param1 :rest)
|
||||
(mapcat (fn [[k v]]
|
||||
[(if (keyword? k)
|
||||
(api/keyword-node k)
|
||||
k)
|
||||
(if (vector? v)
|
||||
(api/vector-node v)
|
||||
v)]))
|
||||
(api/map-node)))
|
||||
param1)
|
||||
|
||||
result (api/list-node
|
||||
(into [(api/token-node 'defn)
|
||||
cname
|
||||
(api/vector-node (filter some? (cons param1 paramN)))]
|
||||
(cons mdata body)))]
|
||||
|
||||
;; (prn (api/sexpr result))
|
||||
|
||||
{:node result}))
|
||||
|
||||
|
||||
(defn rumext-lazycomponent
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -48,6 +48,8 @@
|
||||
/deploy
|
||||
/docker/images/bundle*
|
||||
/exporter/target
|
||||
/frontend/.storybook/preview-body.html
|
||||
/frontend/.storybook/preview-head.html
|
||||
/frontend/cypress/fixtures/validuser.json
|
||||
/frontend/cypress/videos/*/
|
||||
/frontend/cypress/videos/*/
|
||||
@@ -68,8 +70,9 @@
|
||||
/web
|
||||
clj-profiler/
|
||||
node_modules
|
||||
frontend/.storybook/preview-body.html
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/**/.yarn/*
|
||||
|
||||
239
CHANGES.md
239
CHANGES.md
@@ -1,5 +1,242 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.5.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
||||
## 2.4.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Use [nginx-unprivileged](https://hub.docker.com/r/nginxinc/nginx-unprivileged) as base image for
|
||||
Penpot's frontend docker image. Now all the docker images runs with the same unprivileged user
|
||||
(penpot). Because of that, the default NGINX listen port is now 8080 instead of 80, so
|
||||
you will have to modify your infrastructure to apply this change.
|
||||
|
||||
- Redis 7.2 is explicitly pinned in our example docker-compose.yml file. This is done because,
|
||||
starting with the next versions, Redis is no longer distributed under an open-source license.
|
||||
On-premise users are obviously free to upgrade to the version they are using or a more modern one.
|
||||
Keep in mind that if you were using a version other than 7.2, you may have to recreate the volume
|
||||
associated with the Redis container because the 7.2 storage format may not be compatible with what
|
||||
you already have stored on the volume, and Redis may not start. In the near future, we will evaluate
|
||||
whether to move to an open-source version of Redis (such as https://valkey.io/).
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Viewer role for team members [Taiga #1056 & #6590](https://tree.taiga.io/project/penpot/us/1056 & https://tree.taiga.io/project/penpot/us/6590)
|
||||
- File history versions management [Taiga](https://tree.taiga.io/project/penpot/us/187?milestone=411120)
|
||||
- Rename selected layer via keyboard shortcut and context menu option [Taiga #8882](https://tree.taiga.io/project/penpot/us/8882)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
||||
## 2.3.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem creating manual overlay interactions [Taiga #9146](https://tree.taiga.io/project/penpot/issue/9146)
|
||||
|
||||
## 2.3.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix null pointer exception on number checking functions
|
||||
- Fix problem with grid layout ordering after moving [Taiga #9179](https://tree.taiga.io/project/penpot/issue/9179)
|
||||
|
||||
### :books: Documentation
|
||||
|
||||
- Add initial documentation for Kubernetes
|
||||
|
||||
|
||||
## 2.3.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix unexpected issue on interaction between plugins sandbox and
|
||||
internal impl of promise
|
||||
|
||||
|
||||
## 2.3.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- **New plugin system.**
|
||||
|
||||
Penpot now supports custom plugins. Read everything about developing your plugins [HERE](https://help.penpot.app/plugins/)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- All our plugins beta testers :heart:.
|
||||
- Fix problem when translating multiple path points by @eeropic [#4459](https://github.com/penpot/penpot/issues/4459)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- **Replace Draft.js completely with a custom editor** [Taiga #7706](https://tree.taiga.io/project/penpot/us/7706)
|
||||
|
||||
This refactor adds better IME support, more performant text editing
|
||||
experience and a better clipboard support while keeping full
|
||||
retrocompatibility with previous editor.
|
||||
|
||||
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
|
||||
- Fix problem with go back button on error page [Taiga #8887](https://tree.taiga.io/project/penpot/issue/8887)
|
||||
- Fix problem with shadows in text for Safari [Taiga #8770](https://tree.taiga.io/project/penpot/issue/8770)
|
||||
- Fix a regression with feedback form subject and content limits [Taiga #8908](https://tree.taiga.io/project/penpot/issue/8908)
|
||||
- Fix problem with stroke and filter ordering in frames [Github #5058](https://github.com/penpot/penpot/issues/5058)
|
||||
- Fix problem with hover layers when hidden/blocked [Github #5074](https://github.com/penpot/penpot/issues/5074)
|
||||
- Fix problem with precision on boolean calculation [Taiga #8482](https://tree.taiga.io/project/penpot/issue/8482)
|
||||
- Fix problem when translating multiple path points [Github #4459](https://github.com/penpot/penpot/issues/4459)
|
||||
- Fix problem on importing (and exporting) files with flows [Taiga #8914](https://tree.taiga.io/project/penpot/issue/8914)
|
||||
- Fix Internal Error page: "go to your penpot" wrong design [Taiga #8922](https://tree.taiga.io/project/penpot/issue/8922)
|
||||
- Fix problem updating layout when toggle visibility in component copy [Github #5143](https://github.com/penpot/penpot/issues/5143)
|
||||
- Fix "Done" button on toolbar on inspect mode should go to design mode [Taiga #8933](https://tree.taiga.io/project/penpot/issue/8933)
|
||||
- Fix problem with shortcuts in text editor [Github #5078](https://github.com/penpot/penpot/issues/5078)
|
||||
- Fix problems with show in viewer and interactions [Github #4868](https://github.com/penpot/penpot/issues/4868)
|
||||
- Add visual feedback when moving an element into a board [Github #3210](https://github.com/penpot/penpot/issues/3210)
|
||||
- Fix percent calculation on grid layout tracks [Github #4688](https://github.com/penpot/penpot/issues/4688)
|
||||
- Fix problem with caps and inner shadows [Github #4517](https://github.com/penpot/penpot/issues/4517)
|
||||
- Fix problem with horizontal/vertical lines and shadows [Github #4516](https://github.com/penpot/penpot/issues/4516)
|
||||
- Fix problem with layers overflowing panel [Taiga #9021](https://tree.taiga.io/project/penpot/issue/9021)
|
||||
- Fix in workspace you can manage rulers on view mode [Taiga #8966](https://tree.taiga.io/project/penpot/issue/8966)
|
||||
- Fix problem with swap components in grid layout [Taiga #9066](https://tree.taiga.io/project/penpot/issue/9066)
|
||||
|
||||
## 2.2.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with Ctrl+F shortcut on the dashboard [Taiga #8876](https://tree.taiga.io/project/penpot/issue/8876)
|
||||
- Fix visual problem with the font-size dropdown in assets [Taiga #8872](https://tree.taiga.io/project/penpot/issue/8872)
|
||||
- Add limits for invitation RPC methods (hard limit 25 emails per request)
|
||||
|
||||
## 2.2.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
|
||||
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
|
||||
time being.
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946)
|
||||
- Correct a spelling in onboarding.edn (by @n-stha) [#4936](https://github.com/penpot/penpot/pull/4936)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- **Tiered File Data Storage** [Taiga #8376](https://tree.taiga.io/project/penpot/us/8376)
|
||||
|
||||
This feature allows offloading file data that is not actively used
|
||||
from the database to object storage (e.g., filesystem, S3), thereby
|
||||
freeing up space in the database. It can be enabled with the
|
||||
`enable-enable-tiered-file-data-storage` flag.
|
||||
|
||||
*(On-Premise feature, EXPERIMENTAL).*
|
||||
|
||||
- **JSON Interoperability for HTTP API** [Taiga #8372](https://tree.taiga.io/project/penpot/us/8372)
|
||||
|
||||
Enables full JSON interoperability for our HTTP API. Previously,
|
||||
JSON was only barely supported for output when the
|
||||
`application/json` media type was specified in the `Accept` header,
|
||||
or when `_fmt=json` was passed as a query parameter. With this
|
||||
update, we now offer proper bi-directional support for using our API
|
||||
with plain JSON, instead of Transit.
|
||||
|
||||
- **Automatic File Snapshotting**
|
||||
|
||||
Adds the ability to automatically take and maintain a limited set of
|
||||
snapshots of active files without explicit user intervention. This
|
||||
feature allows on-premise administrators to recover the state of a
|
||||
file from a past point in time in a limited manner.
|
||||
|
||||
It can be enabled with the `enable-auto-file-snapshot` flag and
|
||||
configured with the following settings:
|
||||
|
||||
```bash
|
||||
# Take snapshots every 10 update operations
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_EVERY=10
|
||||
|
||||
# Take a snapshot if it has been more than 3 hours since the file was last modified
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_TIMEOUT=3h
|
||||
|
||||
# The total number of snapshots to keep
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_TOTAL=10
|
||||
```
|
||||
|
||||
Snapshots are only taken during update operations; there is NO
|
||||
active background process for this.
|
||||
|
||||
- Add separated flag `enable-oidc-registration` for enable the
|
||||
registration only for OIDC authentication backend [Github
|
||||
#4882](https://github.com/penpot/penpot/issues/4882)
|
||||
|
||||
- Update templates in libraries & templates in dashboard modal [Taiga #8145](https://tree.taiga.io/project/penpot/us/8145)
|
||||
|
||||
- **Design System**
|
||||
|
||||
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
|
||||
|
||||
- **Storybook** [Taiga #6329](https://tree.taiga.io/project/penpot/us/6329)
|
||||
|
||||
The Design System components are now published in a Storybook, available at `/storybook`.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix webhook checkbox position [Taiga #8634](https://tree.taiga.io/project/penpot/issue/8634)
|
||||
- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254)
|
||||
- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351)
|
||||
- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353)
|
||||
- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273)
|
||||
- Fix problem with SVG import [Github #4888](https://github.com/penpot/penpot/issues/4888)
|
||||
- Fix problem with overlay positions in viewer [Taiga #8464](https://tree.taiga.io/project/penpot/issue/8464)
|
||||
- Fix layer panel overflowing [Taiga #8665](https://tree.taiga.io/project/penpot/issue/8665)
|
||||
- Fix problem when creating a component instance from grid layout [Github #4881](https://github.com/penpot/penpot/issues/4881)
|
||||
- Fix problem when dismissing shared library update [Taiga #8669](https://tree.taiga.io/project/penpot/issue/8669)
|
||||
- Fix visual problem with stroke cap menu [Taiga #8730](https://tree.taiga.io/project/penpot/issue/8730)
|
||||
- Fix issue when exporting libraries when merging libraries [Taiga #8758](https://tree.taiga.io/project/penpot/issue/8758)
|
||||
- Fix problem with comments max length [Taiga #8778](https://tree.taiga.io/project/penpot/issue/8778)
|
||||
- Fix copy/paste images in Safari [Taiga #8771](https://tree.taiga.io/project/penpot/issue/8771)
|
||||
- Fix swap when the copy is the only child of a group [#5075](https://github.com/penpot/penpot/issues/5075)
|
||||
- Fix file builder hangs when exporting [#5099](https://github.com/penpot/penpot/issues/5099)
|
||||
|
||||
## 2.1.5
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix broken webhooks [Taiga #8370](https://tree.taiga.io/project/penpot/issue/8370)
|
||||
|
||||
## 2.1.4
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix json encoding on zip encoding decoding.
|
||||
- Add schema validation for color changes.
|
||||
- Fix render of some texts without position data.
|
||||
|
||||
## 2.1.3
|
||||
|
||||
- Don't allow registration when registration is disabled and invitation token is used [Github #4975](https://github.com/penpot/penpot/issues/4975)
|
||||
|
||||
## 2.1.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -27,7 +264,7 @@
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
### :heart: Communityq contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ quick win.
|
||||
If is going to be your first pull request, You can learn how from this
|
||||
free video series:
|
||||
|
||||
https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
|
||||
https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github
|
||||
|
||||
We will use the `easy fix` mark for tag for indicate issues that are
|
||||
easy for beginners.
|
||||
|
||||
14
README.md
14
README.md
@@ -8,10 +8,12 @@
|
||||
<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>
|
||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
|
||||
<p align="center">
|
||||
<a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img alt="License: MPL-2.0" src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||
<a href="https://community.penpot.app" rel="nofollow"><img alt="Penpot Community" src="https://img.shields.io/discourse/posts?server=https%3A%2F%2Fcommunity.penpot.app" style="max-width:100%;"></a>
|
||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img alt="Managed with Taiga.io" src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img alt="Gitpod ready-to-code" 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> •
|
||||
@@ -50,6 +52,7 @@ Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
|
||||
- [Why Penpot](#why-penpot)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Community](#community)
|
||||
- [Contributing](#contributing)
|
||||
- [Resources](#resources)
|
||||
- [License](#license)
|
||||
|
||||
@@ -57,6 +60,9 @@ Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the
|
||||
|
||||
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.
|
||||
|
||||
### Plugin system ###
|
||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||
|
||||
### 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".
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/clojure {:mvn/version "1.12.0-alpha12"}
|
||||
org.clojure/clojure {:mvn/version "1.12.0"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.6-3"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.6-6"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
@@ -17,33 +17,33 @@
|
||||
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.3.2.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.4.0.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v10.0"
|
||||
:git/sha "520613f"
|
||||
{:git/tag "v11.4"
|
||||
:git/sha "ce50d42"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}
|
||||
metosin/reitit-core {:mvn/version "0.7.0"}
|
||||
nrepl/nrepl {:mvn/version "1.1.2"}
|
||||
cider/cider-nrepl {:mvn/version "0.48.0"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.955"}
|
||||
metosin/reitit-core {:mvn/version "0.7.2"}
|
||||
nrepl/nrepl {:mvn/version "1.3.0"}
|
||||
cider/cider-nrepl {:mvn/version "0.50.2"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.3"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.46.0.0"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.4"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.46.1.3"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "5.1.0"}
|
||||
com.zaxxer/HikariCP {:mvn/version "6.0.0"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
|
||||
buddy/buddy-hashers {:mvn/version "2.0.167"}
|
||||
buddy/buddy-sign {:mvn/version "3.5.351"}
|
||||
buddy/buddy-sign {:mvn/version "3.6.1-359"}
|
||||
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.8"}
|
||||
|
||||
org.jsoup/jsoup {:mvn/version "1.17.2"}
|
||||
org.jsoup/jsoup {:mvn/version "1.18.1"}
|
||||
org.im4java/im4java
|
||||
{:git/tag "1.4.0-penpot-2"
|
||||
:git/sha "e2b3e16"
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.25.63"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.28.26"}
|
||||
}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}}
|
||||
{io.github.clojure/tools.build {:git/tag "v0.10.5" :git/sha "2a21b7a"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
[app.common.schema.desc-native :as smdn]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.spec :as us]
|
||||
[app.common.json :as json]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -29,7 +30,6 @@
|
||||
[app.srepl.helpers :as srepl.helpers]
|
||||
[app.srepl.main :as srepl]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[clj-async-profiler.core :as prof]
|
||||
[clojure.contrib.humanize :as hum]
|
||||
@@ -137,7 +137,6 @@
|
||||
;; :v6 v6
|
||||
;; }])))
|
||||
|
||||
|
||||
(defn calculate-frames
|
||||
[{:keys [data]}]
|
||||
(->> (vals (:pages-index data))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.2.2",
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,29 +171,43 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We received a request to change your current email to {{ pending-email }}.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
We received a request to change your current email to {{ pending-email }}.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Click to the link below to confirm the change:</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Click to the link below to confirm the change:</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Confirm email change </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Confirm email change </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -194,17 +215,24 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, please consider changing your password for security reasons.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, please consider changing your password for security
|
||||
reasons.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -221,258 +249,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
323
backend/resources/app/email/includes/footer.html
Normal file
323
backend/resources/app/email/includes/footer.html
Normal file
@@ -0,0 +1,323 @@
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot is the first Open Source design and prototyping platform meant for
|
||||
cross-domain teams.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-uxbox.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://x.com/penpotapp" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-x.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-github.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.linkedin.com/company/penpotdesign/"
|
||||
target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-linkedin.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://fosstodon.org/@penpot/" target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-mastodon.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot"
|
||||
target="_blank">
|
||||
<img height="24"
|
||||
src="{{ public-uri }}/images/email/logo-taiga.png"
|
||||
style="border-radius:3px;display:block;"
|
||||
width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
|
||||
Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,36 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Accept invite </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Accept invite </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,12 +208,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -211,258 +234,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
244
backend/resources/app/email/join-team/en.html
Normal file
244
backend/resources/app/email/join-team/en.html
Normal file
@@ -0,0 +1,244 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{
|
||||
team|abbreviate:25}}”.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/projects"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Go to the Team </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
backend/resources/app/email/join-team/en.subj
Normal file
1
backend/resources/app/email/join-team/en.subj
Normal file
@@ -0,0 +1 @@
|
||||
You have joined {{team}}
|
||||
10
backend/resources/app/email/join-team/en.txt
Normal file
10
backend/resources/app/email/join-team/en.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Hello!
|
||||
|
||||
As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{ team|abbreviate:25}}”.
|
||||
|
||||
Go to the team with this link:
|
||||
|
||||
{{ public-uri }}/#/dashboard/team/{{team-id}}
|
||||
|
||||
Enjoy!
|
||||
The Penpot team.
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,37 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">We have received a request to reset your password. Click the link below to choose a new one:</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
We have received a request to reset your password. Click the link below to choose a new one:
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Reset password </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/recovery?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Reset password </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,17 +209,24 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">If you received this email by mistake, you can safely ignore it. Your password won't be changed.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
If you received this email by mistake, you can safely ignore it. Your password won't be changed.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -216,258 +243,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
@@ -110,15 +111,20 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png" style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;" width="97" />
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -151,7 +157,8 @@
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
@@ -164,24 +171,37 @@
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">Hello {{name|abbreviate:25}}!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello {{name|abbreviate:25}}!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Thanks for signing up for your Penpot account! Please verify your email using the link below and
|
||||
get started building mockups and prototypes today!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;" valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}" style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Verify email </a>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Verify email </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -189,12 +209,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Enjoy!</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Enjoy!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">The Penpot team.</div>
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -211,258 +235,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:24px 0 0 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:425px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-px-425 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot is the first Open Source design and prototyping platform meant for cross-domain teams.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
>
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-uxbox.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://twitter.com/penpotapp" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-twitter.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/penpot/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-github.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.instagram.com/penpot.app/" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-instagram.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tr>
|
||||
<td style="padding:0 8px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://tree.taiga.io/project/penpot" target="_blank">
|
||||
<img height="24" src="{{ public-uri }}/images/email/logo-taiga.png" style="border-radius:3px;display:block;" width="24" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0 0 24px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -0,0 +1,254 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the
|
||||
file named “{{file-name|abbreviate:25}}”.
|
||||
</p>
|
||||
<p>
|
||||
Since this file is in your Penpot team, you can provide access by sending a view-only link.
|
||||
This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
|
||||
</p>
|
||||
<p>To proceed, please click the button below to generate and send the view-only link:</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Send a View-Only link </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
|
||||
<p>Thank you</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Request View-Only Access to “{{file-name|abbreviate:25}}”
|
||||
@@ -0,0 +1,17 @@
|
||||
Hello!
|
||||
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the file named “{{file-name|abbreviate:25}}”.
|
||||
|
||||
Since this file is in your Penpot team, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
|
||||
|
||||
To proceed, please click the link below to generate and send the view-only link:
|
||||
|
||||
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true
|
||||
|
||||
|
||||
|
||||
If you do not wish to grant access at this time, you can simply disregard this email.
|
||||
Thank you
|
||||
|
||||
|
||||
The Penpot team.
|
||||
@@ -0,0 +1,277 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named
|
||||
“{{file-name|abbreviate:25}}”.
|
||||
</p>
|
||||
<p>
|
||||
Please note that the file is currently in Your Penpot 's team, so direct access cannot be
|
||||
granted. However, you have two options to provide the requested access:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Move the File to Another Team:</p>
|
||||
<p>You can move the file to another team and then give access to that team, inviting
|
||||
{{requested-by|abbreviate:25}}.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<ul>
|
||||
<li>
|
||||
<p>Send a View-Only Link:</p>
|
||||
<p>Alternatively, you can create and share a view-only link to the file. This will allow
|
||||
{{requested-by|abbreviate:25}} to view the content without making any changes.</p>
|
||||
<p>Click the button below to generate and send the link:</p>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Send a View-Only link </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
|
||||
<p>Thank you</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Request Access to “{{file-name|abbreviate:25}}”
|
||||
@@ -0,0 +1,30 @@
|
||||
Hello!
|
||||
|
||||
|
||||
Hello!
|
||||
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”.
|
||||
|
||||
Please note that the file is currently in Your Penpot 's team, so direct access cannot be granted. However, you have two options to provide the requested access:
|
||||
|
||||
- Move the File to Another Team:
|
||||
|
||||
You can move the file to another team and then give access to that team, inviting {{requested-by|abbreviate:25}}.
|
||||
|
||||
|
||||
|
||||
- Send a View-Only Link:
|
||||
|
||||
Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
|
||||
|
||||
Click the link below to generate and send the link:
|
||||
|
||||
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true
|
||||
|
||||
|
||||
|
||||
If you do not wish to grant access at this time, you can simply disregard this email.
|
||||
Thank you
|
||||
|
||||
|
||||
The Penpot team.
|
||||
295
backend/resources/app/email/request-file-access/en.html
Normal file
295
backend/resources/app/email/request-file-access/en.html
Normal file
@@ -0,0 +1,295 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named
|
||||
“{{file-name|abbreviate:25}}”.
|
||||
</p>
|
||||
<p>
|
||||
To provide this access, you have the following options:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Give Access to the “{{team-name|abbreviate:25}}” Team:</p>
|
||||
<p>This will automatically include {{requested-by|abbreviate:25}} in the team, so the user
|
||||
can see all the projects and files in it.</p>
|
||||
<p>Click the button below to provide team access:</p>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape }}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Give access to “{{team-name|abbreviate:25}}” Team </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<ul>
|
||||
<li>
|
||||
<p>Send a View-Only Link:</p>
|
||||
<p>Alternatively, you can create and share a view-only link to the file. This will allow
|
||||
{{requested-by|abbreviate:25}} to view the content without making any changes.</p>
|
||||
<p>Click the button below to generate and send the link:</p>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Send a View-Only link </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
|
||||
<p>Thank you</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
backend/resources/app/email/request-file-access/en.subj
Normal file
1
backend/resources/app/email/request-file-access/en.subj
Normal file
@@ -0,0 +1 @@
|
||||
Request Access to “{{file-name|abbreviate:25}}”
|
||||
34
backend/resources/app/email/request-file-access/en.txt
Normal file
34
backend/resources/app/email/request-file-access/en.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
Hello!
|
||||
|
||||
|
||||
Hello!
|
||||
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”.
|
||||
|
||||
To provide this access, you have the following options:
|
||||
|
||||
- Give Access to the “{{team-name|abbreviate:25}}” Team:
|
||||
|
||||
This will automatically include {{requested-by|abbreviate:25}} in the team, so the user can see all the projects and files in it.
|
||||
|
||||
Click the link below to provide team access:
|
||||
|
||||
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
|
||||
|
||||
|
||||
|
||||
- Send a View-Only Link:
|
||||
|
||||
Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes.
|
||||
|
||||
Click the link below to generate and send the link:
|
||||
|
||||
{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true
|
||||
|
||||
|
||||
|
||||
If you do not wish to grant access at this time, you can simply disregard this email.
|
||||
Thank you
|
||||
|
||||
|
||||
The Penpot team.
|
||||
252
backend/resources/app/email/request-team-access/en.html
Normal file
252
backend/resources/app/email/request-team-access/en.html
Normal file
@@ -0,0 +1,252 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:24px;font-weight:600;line-height:150%;text-align:left;color:#000000;">
|
||||
Hello!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the
|
||||
“{{team-name|abbreviate:25}}” Team.
|
||||
</p>
|
||||
<p>
|
||||
To provide access, please click the button below:
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#31EFB8" role="presentation"
|
||||
style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#31EFB8;"
|
||||
valign="middle">
|
||||
<a href="{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}"
|
||||
style="display:inline-block;background:#31EFB8;color:#1F1F1F;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"
|
||||
target="_blank"> Give access to “{{team-name|abbreviate:25}}” </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<p>If you do not wish to grant access at this time, you can simply disregard this email.</p>
|
||||
<p>Thank you</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
backend/resources/app/email/request-team-access/en.subj
Normal file
1
backend/resources/app/email/request-team-access/en.subj
Normal file
@@ -0,0 +1 @@
|
||||
Request Access to “{{team-name|abbreviate:25}}”
|
||||
14
backend/resources/app/email/request-team-access/en.txt
Normal file
14
backend/resources/app/email/request-team-access/en.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
Hello!
|
||||
|
||||
{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the “{{team-name|abbreviate:25}}” Team.
|
||||
|
||||
To provide access, please click the link below:
|
||||
|
||||
{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}}
|
||||
|
||||
|
||||
If you do not wish to grant access at this time, you can simply disregard this email.
|
||||
Thank you
|
||||
|
||||
|
||||
The Penpot team.
|
||||
@@ -1,39 +1,42 @@
|
||||
[{:id "wireframing-kit"
|
||||
:name "Wireframe library"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}
|
||||
{:id "prototype-examples"
|
||||
:name "Prototipe template"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"}
|
||||
:name "Prototype template"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Prototype%20examples%20v1.1.penpot"}
|
||||
{:id "plants-app"
|
||||
:name "UI mockup example"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Plants-app.penpot"}
|
||||
{:id "penpot-design-system"
|
||||
:name "Design system example"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Penpot%20-%20Design%20System%20v2.1.penpot"}
|
||||
{:id "tutorial-for-beginners"
|
||||
:name "Tutorial for beginners"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/tutorial-for-beginners.penpot"}
|
||||
{:id "lucide-icons"
|
||||
:name "Lucide Icons"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Lucide-icons.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Lucide-icons.penpot"}
|
||||
{:id "font-awesome"
|
||||
:name "Font Awesome"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/FontAwesome.penpot"}
|
||||
{:id "black-white-mobile-templates"
|
||||
:name "Black & White Mobile Templates"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Black-&-White-Mobile-Templates.penpot"}
|
||||
{:id "avataaars"
|
||||
:name "Avataaars"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Avataaars-by-Pablo-Stanley.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Avataaars-by-Pablo-Stanley.penpot"}
|
||||
{:id "ux-notes"
|
||||
:name "UX Notes"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/UX-Notes.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/UX-Notes.penpot"}
|
||||
{:id "whiteboarding-kit"
|
||||
:name "Whiteboarding Kit"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Whiteboarding-mapping-kit.penpot"}
|
||||
{:id "open-color-scheme"
|
||||
:name "Open Color Scheme"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/Open%20Color%20Scheme%20(v1.9.1).penpot"}
|
||||
{:id "flex-layout-playground"
|
||||
:name "Flex Layout Playground"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"}]
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Flex%20Layout%20Playground%20v2.0.penpot"}
|
||||
{:id "welcome"
|
||||
:name "Welcome"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/main/welcome.penpot"}]
|
||||
|
||||
@@ -20,12 +20,19 @@
|
||||
<span>WEBHOOK</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if item.params-schema-js %}
|
||||
<span class="tag">
|
||||
<span>SCHEMA</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if item.spec %}
|
||||
<span class="tag">
|
||||
<span>SPEC</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if item.sse %}
|
||||
<span class="tag">
|
||||
<span>SSE</span>
|
||||
|
||||
@@ -7,7 +7,7 @@ Debug Main Page
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div class="title">
|
||||
<h1>ADMIN DEBUG INTERFACE</h1>
|
||||
<h1>ADMIN DEBUG INTERFACE (VERSION: {{version}})</h1>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="dashboard">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" monitorInterval="30">
|
||||
<Configuration status="fatal" monitorInterval="30">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" monitorInterval="30">
|
||||
<Configuration status="fatal" monitorInterval="30">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" monitorInterval="30">
|
||||
<Configuration status="fatal" monitorInterval="30">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" monitorInterval="60">
|
||||
<Configuration status="fatal" monitorInterval="60">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
|
||||
|
||||
@@ -7,6 +7,8 @@ set -ex
|
||||
rm -rf target;
|
||||
mkdir -p target/classes;
|
||||
mkdir -p target/dist;
|
||||
mkdir -p target/dist/scripts;
|
||||
|
||||
echo "$CURRENT_VERSION" > target/classes/version.txt;
|
||||
cp ../CHANGES.md target/classes/changelog.md;
|
||||
|
||||
@@ -15,6 +17,7 @@ mv target/penpot.jar target/dist/penpot.jar
|
||||
cp resources/log4j2.xml target/dist/log4j2.xml
|
||||
cp scripts/run.template.sh target/dist/run.sh;
|
||||
cp scripts/manage.py target/dist/manage.py
|
||||
cp scripts/svgo-cli.js target/dist/scripts/;
|
||||
chmod +x target/dist/run.sh;
|
||||
chmod +x target/dist/manage.py
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_TENANT=dev
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
enable-login-with-ldap \
|
||||
@@ -23,9 +22,12 @@ export PENPOT_FLAGS="\
|
||||
enable-urepl-server \
|
||||
enable-rpc-climit \
|
||||
enable-rpc-rlimit \
|
||||
enable-quotes \
|
||||
enable-soft-rpc-rlimit \
|
||||
enable-auto-file-snapshot \
|
||||
enable-webhooks \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
|
||||
@@ -61,9 +63,11 @@ mc mb penpot-s3/penpot -p -q
|
||||
|
||||
export AWS_ACCESS_KEY_ID=penpot-devenv
|
||||
export AWS_SECRET_ACCESS_KEY=penpot-devenv
|
||||
export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
|
||||
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
|
||||
|
||||
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
|
||||
|
||||
export OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_TENANT=dev
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
enable-prepl-server \
|
||||
@@ -10,6 +9,7 @@ export PENPOT_FLAGS="\
|
||||
enable-webhooks \
|
||||
enable-backend-asserts \
|
||||
enable-audit-log \
|
||||
enable-login-with-ldap \
|
||||
enable-transit-readable-response \
|
||||
enable-demo-users \
|
||||
enable-feature-fdata-pointer-map \
|
||||
@@ -17,7 +17,10 @@ export PENPOT_FLAGS="\
|
||||
disable-secure-session-cookies \
|
||||
enable-rpc-climit \
|
||||
enable-smtp \
|
||||
enable-quotes \
|
||||
enable-file-snapshot \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
|
||||
@@ -55,9 +58,9 @@ mc mb penpot-s3/penpot -p -q
|
||||
|
||||
export AWS_ACCESS_KEY_ID=penpot-devenv
|
||||
export AWS_SECRET_ACCESS_KEY=penpot-devenv
|
||||
export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
|
||||
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
|
||||
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
|
||||
entrypoint=${1:-app.main};
|
||||
|
||||
|
||||
214
backend/scripts/svgo-cli.js
Normal file
214
backend/scripts/svgo-cli.js
Normal file
File diff suppressed because one or more lines are too long
@@ -8,10 +8,8 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.common.schema :as sm]
|
||||
[clj-ldap.client :as ldap]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -59,21 +57,26 @@
|
||||
:email email
|
||||
:backend "ldap"})))
|
||||
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
(def ^:private schema:info-data
|
||||
[:map
|
||||
[:fullname ::sm/text]
|
||||
[:email ::sm/email]
|
||||
[:backend ::sm/text]])
|
||||
|
||||
(s/def ::info-data
|
||||
(s/keys :req-un [::fullname ::email ::backend]))
|
||||
(def ^:private valid-info-data?
|
||||
(sm/lazy-validator schema:info-data))
|
||||
|
||||
(def ^:private explain-info-data
|
||||
(sm/lazy-explainer schema:info-data))
|
||||
|
||||
(defn authenticate
|
||||
[cfg params]
|
||||
(with-open [conn (connect cfg)]
|
||||
(when-let [user (-> (assoc cfg ::conn conn)
|
||||
(retrieve-user params))]
|
||||
(when-not (s/valid? ::info-data user)
|
||||
(let [explain (s/explain-str ::info-data user)]
|
||||
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
|
||||
(when-not (valid-info-data? user)
|
||||
(let [explain (explain-info-data user)]
|
||||
(l/warn :hint "invalid response from ldap, looks like ldap is not configured correctly" :data user)
|
||||
(ex/raise :type :restriction
|
||||
:code :wrong-ldap-response
|
||||
:explain explain)))
|
||||
@@ -103,37 +106,31 @@
|
||||
:host (:host cfg) :port (:port cfg) :cause cause)
|
||||
nil))))
|
||||
|
||||
(s/def ::enabled? ::us/boolean)
|
||||
(s/def ::host ::cf/ldap-host)
|
||||
(s/def ::port ::cf/ldap-port)
|
||||
(s/def ::ssl ::cf/ldap-ssl)
|
||||
(s/def ::tls ::cf/ldap-starttls)
|
||||
(s/def ::query ::cf/ldap-user-query)
|
||||
(s/def ::base-dn ::cf/ldap-base-dn)
|
||||
(s/def ::bind-dn ::cf/ldap-bind-dn)
|
||||
(s/def ::bind-password ::cf/ldap-bind-password)
|
||||
(s/def ::attrs-email ::cf/ldap-attrs-email)
|
||||
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
|
||||
(s/def ::attrs-username ::cf/ldap-attrs-username)
|
||||
(def ^:private schema:params
|
||||
[:map
|
||||
[:host {:optional true} :string]
|
||||
[:port {:optional true} ::sm/int]
|
||||
[:bind-dn {:optional true} :string]
|
||||
[:bind-passwor {:optional true} :string]
|
||||
[:query {:optional true} :string]
|
||||
[:base-dn {:optional true} :string]
|
||||
[:attrs-email {:optional true} :string]
|
||||
[:attrs-username {:optional true} :string]
|
||||
[:attrs-fullname {:optional true} :string]
|
||||
[:ssl {:optional true} ::sm/boolean]
|
||||
[:tls {:optional true} ::sm/boolean]])
|
||||
|
||||
(s/def ::provider-params
|
||||
(s/keys :opt-un [::host ::port
|
||||
::ssl ::tls
|
||||
::enabled?
|
||||
::bind-dn
|
||||
::bind-password
|
||||
::query
|
||||
::attrs-email
|
||||
::attrs-username
|
||||
::attrs-fullname]))
|
||||
(s/def ::provider
|
||||
(s/nilable ::provider-params))
|
||||
(def ^:private check-params
|
||||
(sm/check-fn schema:params :hint "Invalid LDAP provider parameters"))
|
||||
|
||||
(defmethod ig/pre-init-spec ::provider
|
||||
[_]
|
||||
(s/spec ::provider))
|
||||
(defmethod ig/assert-key ::provider
|
||||
[_ params]
|
||||
(when (:enabled params)
|
||||
(some->> params check-params)))
|
||||
|
||||
(defmethod ig/init-key ::provider
|
||||
[_ cfg]
|
||||
(when (:enabled? cfg)
|
||||
(when (:enabled cfg)
|
||||
(try-connectivity cfg)))
|
||||
|
||||
(sm/register! ::provider schema:params)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -32,11 +32,10 @@
|
||||
[buddy.sign.jwk :as jwk]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[ring.request :as rreq]
|
||||
[ring.response :as-alias rres]))
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
@@ -140,8 +139,9 @@
|
||||
(l/warn :hint "unable to retrieve JWKs (unexpected exception)"
|
||||
:cause cause)))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::providers/generic [_]
|
||||
(s/keys :req [::http/client]))
|
||||
(defmethod ig/assert-key ::providers/generic
|
||||
[_ params]
|
||||
(assert (http/client? (::http/client params)) "expected a valid http client"))
|
||||
|
||||
(defmethod ig/init-key ::providers/generic
|
||||
[_ cfg]
|
||||
@@ -197,6 +197,10 @@
|
||||
;; GITHUB AUTH PROVIDER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- int-in-range?
|
||||
[val start end]
|
||||
(and (<= start val) (< val end)))
|
||||
|
||||
(defn- retrieve-github-email
|
||||
[cfg tdata props]
|
||||
(or (some-> props :github/email)
|
||||
@@ -207,7 +211,7 @@
|
||||
|
||||
{:keys [status body]} (http/req! cfg params {:sync? true})]
|
||||
|
||||
(when-not (s/int-in-range? 200 300 status)
|
||||
(when-not (int-in-range? status 200 300)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-github-emails
|
||||
:hint "unable to retrieve github emails"
|
||||
@@ -217,8 +221,9 @@
|
||||
|
||||
(->> body json/decode (filter :primary) first :email))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::providers/github [_]
|
||||
(s/keys :req [::http/client]))
|
||||
(defmethod ig/assert-key ::providers/github
|
||||
[_ params]
|
||||
(assert (http/client? (::http/client params)) "expected a valid http client"))
|
||||
|
||||
(defmethod ig/init-key ::providers/github
|
||||
[_ cfg]
|
||||
@@ -394,7 +399,7 @@
|
||||
:status (:status response)
|
||||
:body (:body response))
|
||||
|
||||
(when-not (s/int-in-range? 200 300 (:status response))
|
||||
(when-not (int-in-range? (:status response) 200 300)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-user-info
|
||||
:hint "unable to retrieve user info"
|
||||
@@ -418,15 +423,15 @@
|
||||
(l/warn :hint "unable to get user info from JWT token (unexpected exception)"
|
||||
:cause cause))))
|
||||
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
(s/def ::email ::us/not-empty-string)
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::props (s/map-of ::us/keyword any?))
|
||||
(s/def ::info
|
||||
(s/keys :req-un [::backend
|
||||
::email
|
||||
::fullname
|
||||
::props]))
|
||||
(def ^:private schema:info
|
||||
[:map
|
||||
[:backend ::sm/text]
|
||||
[:email ::sm/email]
|
||||
[:fullname ::sm/text]
|
||||
[:props [:map-of :keyword :any]]])
|
||||
|
||||
(def ^:private valid-info?
|
||||
(sm/validator schema:info))
|
||||
|
||||
(defn- get-info
|
||||
[{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}]
|
||||
@@ -444,7 +449,7 @@
|
||||
|
||||
(l/trc :hint "user info" :info info)
|
||||
|
||||
(when-not (s/valid? ::info info)
|
||||
(when-not (valid-info? info)
|
||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
|
||||
(ex/raise :type :internal
|
||||
:code :incomplete-user-info
|
||||
@@ -492,8 +497,8 @@
|
||||
|
||||
(defn- redirect-response
|
||||
[uri]
|
||||
{::rres/status 302
|
||||
::rres/headers {"location" (str uri)}})
|
||||
{::yres/status 302
|
||||
::yres/headers {"location" (str uri)}})
|
||||
|
||||
(defn- redirect-with-error
|
||||
([error] (redirect-with-error error nil))
|
||||
@@ -567,7 +572,6 @@
|
||||
(tokens/generate (::setup/props cfg)
|
||||
{:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:props (:props info)
|
||||
:profile-id (:id profile)}))
|
||||
props (audit/profile->props profile)
|
||||
context (d/without-nils {:external-session-id (:external-session-id info)})]
|
||||
@@ -599,7 +603,7 @@
|
||||
|
||||
(defn- get-external-session-id
|
||||
[request]
|
||||
(let [session-id (rreq/get-header request "x-external-session-id")]
|
||||
(let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
(when (string? session-id)
|
||||
(if (or (> (count session-id) 256)
|
||||
(= session-id "null")
|
||||
@@ -619,8 +623,8 @@
|
||||
state (tokens/generate (::setup/props cfg)
|
||||
(d/without-nils params))
|
||||
uri (build-auth-uri cfg state)]
|
||||
{::rres/status 200
|
||||
::rres/body {:redirect-uri uri}}))
|
||||
{::yres/status 200
|
||||
::yres/body {:redirect-uri uri}}))
|
||||
|
||||
(defn- callback-handler
|
||||
[{:keys [::provider] :as cfg} request]
|
||||
@@ -656,46 +660,37 @@
|
||||
: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)
|
||||
(s/def ::token-uri ::cf/oidc-token-uri)
|
||||
(s/def ::auth-uri ::cf/oidc-auth-uri)
|
||||
(s/def ::user-uri ::cf/oidc-user-uri)
|
||||
(s/def ::scopes ::cf/oidc-scopes)
|
||||
(s/def ::roles ::cf/oidc-roles)
|
||||
(s/def ::roles-attr ::cf/oidc-roles-attr)
|
||||
(s/def ::email-attr ::cf/oidc-email-attr)
|
||||
(s/def ::name-attr ::cf/oidc-name-attr)
|
||||
(def ^:private schema:provider
|
||||
[:map {:title "provider"}
|
||||
[:client-id ::sm/text]
|
||||
[:client-secret ::sm/text]
|
||||
[:base-uri {:optional true} ::sm/text]
|
||||
[:token-uri {:optional true} ::sm/text]
|
||||
[:auth-uri {:optional true} ::sm/text]
|
||||
[:user-uri {:optional true} ::sm/text]
|
||||
[:scopes {:optional true}
|
||||
[::sm/set ::sm/text]]
|
||||
[:roles {:optional true}
|
||||
[::sm/set ::sm/text]]
|
||||
[:roles-attr {:optional true} ::sm/text]
|
||||
[:email-attr {:optional true} ::sm/text]
|
||||
[:name-attr {:optional true} ::sm/text]])
|
||||
|
||||
(s/def ::provider
|
||||
(s/keys :req-un [::client-id
|
||||
::client-secret]
|
||||
:opt-un [::base-uri
|
||||
::token-uri
|
||||
::auth-uri
|
||||
::user-uri
|
||||
::scopes
|
||||
::roles
|
||||
::roles-attr
|
||||
::email-attr
|
||||
::name-attr]))
|
||||
(def ^:private schema:routes-params
|
||||
[:map
|
||||
::session/manager
|
||||
::http/client
|
||||
::setup/props
|
||||
::db/pool
|
||||
[::providers [:map-of :keyword [:maybe schema:provider]]]])
|
||||
|
||||
(s/def ::providers (s/map-of ::us/keyword (s/nilable ::provider)))
|
||||
|
||||
(s/def ::routes vector?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes
|
||||
[_]
|
||||
(s/keys :req [::session/manager
|
||||
::http/client
|
||||
::setup/props
|
||||
::db/pool
|
||||
::providers]))
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (sm/check schema:routes-params params)))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
(let [cfg (update cfg :provider d/without-nils)]
|
||||
(let [cfg (update cfg :providers d/without-nils)]
|
||||
["" {:middleware [[session/authz cfg]
|
||||
[provider-lookup cfg]]}
|
||||
["/auth/oauth"
|
||||
|
||||
@@ -37,6 +37,21 @@
|
||||
(def ^:dynamic *state* nil)
|
||||
(def ^:dynamic *options* nil)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DEFAULTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Threshold in MiB when we pass from using
|
||||
;; in-memory byte-array's to use temporal files.
|
||||
(def temp-file-threshold
|
||||
(* 1024 1024 2))
|
||||
|
||||
;; A maximum (storage) object size allowed: 100MiB
|
||||
(def ^:const max-object-size
|
||||
(* 1024 1024 100))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def xf-map-id
|
||||
(map :id))
|
||||
|
||||
@@ -56,6 +71,13 @@
|
||||
(def conj-vec
|
||||
(fnil conj []))
|
||||
|
||||
(defn initial-state
|
||||
[]
|
||||
{:storage-objects #{}
|
||||
:files #{}
|
||||
:teams #{}
|
||||
:projects #{}})
|
||||
|
||||
(defn collect-storage-objects
|
||||
[state items]
|
||||
(update state :storage-objects into xf-map-media-id items))
|
||||
@@ -87,6 +109,8 @@
|
||||
attrs))
|
||||
|
||||
(defn update-index
|
||||
([coll]
|
||||
(update-index {} coll identity))
|
||||
([index coll]
|
||||
(update-index index coll identity))
|
||||
([index coll attr]
|
||||
@@ -114,6 +138,16 @@
|
||||
[cfg project-id]
|
||||
(db/get cfg :project {:id project-id}))
|
||||
|
||||
(def ^:private sql:get-teams
|
||||
"SELECT t.* FROM team WHERE id = ANY(?)")
|
||||
|
||||
(defn get-teams
|
||||
[cfg ids]
|
||||
(let [conn (db/get-connection cfg)
|
||||
ids (db/create-array conn "uuid" ids)]
|
||||
(->> (db/exec! conn [sql:get-teams ids])
|
||||
(map decode-row))))
|
||||
|
||||
(defn get-team
|
||||
[cfg team-id]
|
||||
(-> (db/get cfg :team {:id team-id})
|
||||
@@ -167,9 +201,10 @@
|
||||
(defn get-file-object-thumbnails
|
||||
"Return all file object thumbnails for a given file."
|
||||
[cfg file-id]
|
||||
(db/query cfg :file-tagged-object-thumbnail
|
||||
{:file-id file-id
|
||||
:deleted-at nil}))
|
||||
(->> (db/query cfg :file-tagged-object-thumbnail
|
||||
{:file-id file-id
|
||||
:deleted-at nil})
|
||||
(not-empty)))
|
||||
|
||||
(defn get-file-thumbnail
|
||||
"Return the thumbnail for the specified file-id"
|
||||
@@ -224,26 +259,26 @@
|
||||
(->> (db/exec! conn [sql ids])
|
||||
(mapv #(assoc % :file-id id)))))))
|
||||
|
||||
(def ^:private sql:get-team-files
|
||||
(def ^:private sql:get-team-files-ids
|
||||
"SELECT f.id FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE p.team_id = ?")
|
||||
|
||||
(defn get-team-files
|
||||
(defn get-team-files-ids
|
||||
"Get a set of file ids for the specified team-id"
|
||||
[{:keys [::db/conn]} team-id]
|
||||
(->> (db/exec! conn [sql:get-team-files team-id])
|
||||
(->> (db/exec! conn [sql:get-team-files-ids team-id])
|
||||
(into #{} xf-map-id)))
|
||||
|
||||
(def ^:private sql:get-team-projects
|
||||
"SELECT p.id FROM project AS p
|
||||
"SELECT p.* FROM project AS p
|
||||
WHERE p.team_id = ?
|
||||
AND p.deleted_at IS NULL")
|
||||
|
||||
(defn get-team-projects
|
||||
"Get a set of project ids for the team"
|
||||
[{:keys [::db/conn]} team-id]
|
||||
(->> (db/exec! conn [sql:get-team-projects team-id])
|
||||
[cfg team-id]
|
||||
(->> (db/exec! cfg [sql:get-team-projects team-id])
|
||||
(into #{} xf-map-id)))
|
||||
|
||||
(def ^:private sql:get-project-files
|
||||
@@ -257,6 +292,10 @@
|
||||
(->> (db/exec! conn [sql:get-project-files project-id])
|
||||
(into #{} xf-map-id)))
|
||||
|
||||
(defn remap-thumbnail-object-id
|
||||
[object-id file-id]
|
||||
(str/replace-first object-id #"^(.*?)/" (str file-id "/")))
|
||||
|
||||
(defn- relink-shapes
|
||||
"A function responsible to analyze all file data and
|
||||
replace the old :component-file reference with the new
|
||||
@@ -339,6 +378,12 @@
|
||||
data
|
||||
library-ids)))
|
||||
|
||||
(defn disable-database-timeouts!
|
||||
[cfg]
|
||||
(let [conn (db/get-connection cfg)]
|
||||
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])))
|
||||
|
||||
(defn- fix-version
|
||||
[file]
|
||||
(let [file (fmg/fix-version file)]
|
||||
@@ -432,6 +477,20 @@
|
||||
|
||||
file))
|
||||
|
||||
|
||||
(defn register-pending-migrations
|
||||
"All features that are enabled and requires explicit migration are
|
||||
added to the state for a posterior migration step."
|
||||
[cfg {:keys [id features] :as file}]
|
||||
(doseq [feature (-> (::features cfg)
|
||||
(set/difference cfeat/no-migration-features)
|
||||
(set/difference cfeat/backend-only-features)
|
||||
(set/difference features))]
|
||||
(vswap! *state* update :pending-to-migrate (fnil conj []) [feature id]))
|
||||
|
||||
file)
|
||||
|
||||
|
||||
(defn apply-pending-migrations!
|
||||
"Apply alredy registered pending migrations to files"
|
||||
[cfg]
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@@ -50,15 +49,6 @@
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DEFAULTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Threshold in MiB when we pass from using
|
||||
;; in-memory byte-array's to use temporal files.
|
||||
(def temp-file-threshold
|
||||
(* 1024 1024 2))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; LOW LEVEL STREAM IO API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -66,11 +56,6 @@
|
||||
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
|
||||
(def ^:const penpot-magic-number 800099563638710213)
|
||||
|
||||
|
||||
;; A maximum (storage) object size allowed: 100MiB
|
||||
(def ^:const max-object-size
|
||||
(* 1024 1024 100))
|
||||
|
||||
(def ^:dynamic *position* nil)
|
||||
|
||||
(defn get-mark
|
||||
@@ -237,7 +222,7 @@
|
||||
|
||||
(defn copy-stream!
|
||||
[^OutputStream output ^InputStream input ^long size]
|
||||
(let [written (io/copy! input output :size size)]
|
||||
(let [written (io/copy input output :size size)]
|
||||
(l/trace :fn "copy-stream!" :position @*position* :size size :written written ::l/sync? true)
|
||||
(swap! *position* + written)
|
||||
written))
|
||||
@@ -259,18 +244,18 @@
|
||||
p (tmp/tempfile :prefix "penpot.binfile.")]
|
||||
(assert-mark m :stream)
|
||||
|
||||
(when (> s max-object-size)
|
||||
(when (> s bfc/max-object-size)
|
||||
(ex/raise :type :validation
|
||||
:code :max-file-size-reached
|
||||
:hint (str/ffmt "unable to import storage object with size % bytes" s)))
|
||||
|
||||
(if (> s temp-file-threshold)
|
||||
(if (> s bfc/temp-file-threshold)
|
||||
(with-open [^OutputStream output (io/output-stream p)]
|
||||
(let [readed (io/copy! input output :offset 0 :size s)]
|
||||
(let [readed (io/copy input output :offset 0 :size s)]
|
||||
(l/trace :fn "read-stream*!" :expected s :readed readed :position @*position* ::l/sync? true)
|
||||
(swap! *position* + readed)
|
||||
[s p]))
|
||||
[s (io/read-as-bytes input :size s)])))
|
||||
[s (io/read input :size s)])))
|
||||
|
||||
(defmacro assert-read-label!
|
||||
[input expected-label]
|
||||
@@ -382,10 +367,12 @@
|
||||
::l/sync? true)
|
||||
|
||||
(doseq [item media]
|
||||
(l/dbg :hint "write penpot file media object" :id (:id item) ::l/sync? true))
|
||||
(l/dbg :hint "write penpot file media object"
|
||||
:id (:id item) ::l/sync? true))
|
||||
|
||||
(doseq [item thumbnails]
|
||||
(l/dbg :hint "write penpot file object thumbnail" :media-id (str (:media-id item)) ::l/sync? true))
|
||||
(l/dbg :hint "write penpot file object thumbnail"
|
||||
:media-id (str (:media-id item)) ::l/sync? true))
|
||||
|
||||
(doto output
|
||||
(write-obj! file)
|
||||
@@ -403,9 +390,9 @@
|
||||
(write-obj! output rels)))
|
||||
|
||||
(defmethod write-section :v1/sobjects
|
||||
[{:keys [::sto/storage ::output]}]
|
||||
[{:keys [::output] :as cfg}]
|
||||
(let [sids (-> bfc/*state* deref :sids)
|
||||
storage (media/configure-assets-storage storage)]
|
||||
storage (sto/resolve cfg)]
|
||||
|
||||
(l/dbg :hint "found sobjects"
|
||||
:items (count sids)
|
||||
@@ -467,8 +454,8 @@
|
||||
|
||||
(defn- read-import-v1
|
||||
[{:keys [::db/conn ::project-id ::profile-id ::input] :as cfg}]
|
||||
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"])
|
||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
|
||||
(pu/with-open [input (zstd-input-stream input)
|
||||
input (io/data-input-stream input)]
|
||||
@@ -560,7 +547,9 @@
|
||||
|
||||
(when (seq thumbnails)
|
||||
(let [thumbnails (remap-thumbnails thumbnails file-id')]
|
||||
(l/dbg :hint "updated index with thumbnails" :total (count thumbnails) ::l/sync? true)
|
||||
(l/dbg :hint "updated index with thumbnails"
|
||||
:total (count thumbnails)
|
||||
::l/sync? true)
|
||||
(vswap! bfc/*state* update :thumbnails bfc/into-vec thumbnails)))
|
||||
|
||||
(when (seq media)
|
||||
@@ -620,8 +609,8 @@
|
||||
::l/sync? true))))))
|
||||
|
||||
(defmethod read-section :v1/sobjects
|
||||
[{:keys [::sto/storage ::db/conn ::input ::bfc/overwrite ::bfc/timestamp]}]
|
||||
(let [storage (media/configure-assets-storage storage)
|
||||
[{:keys [::db/conn ::input ::bfc/overwrite ::bfc/timestamp] :as cfg}]
|
||||
(let [storage (sto/resolve cfg)
|
||||
ids (read-obj! input)
|
||||
thumb? (into #{} (map :media-id) (:thumbnails @bfc/*state*))]
|
||||
|
||||
@@ -710,7 +699,7 @@
|
||||
|
||||
(dm/assert!
|
||||
"expected instance of jio/IOFactory for `input`"
|
||||
(satisfies? jio/IOFactory output))
|
||||
(io/coercible? output))
|
||||
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
@@ -739,7 +728,7 @@
|
||||
:cause @cs)))))
|
||||
|
||||
(defn import-files!
|
||||
[cfg input]
|
||||
[{:keys [::input] :as cfg}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid profile-id and project-id on `cfg`"
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.db.sql :as sql]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.events :as events]
|
||||
@@ -142,16 +141,15 @@
|
||||
(write! cfg :team-font-variant id font))))
|
||||
|
||||
(defn- write-project!
|
||||
[cfg project-id]
|
||||
(let [project (bfc/get-project cfg project-id)]
|
||||
(events/tap :progress
|
||||
{:op :export
|
||||
:section :write-project
|
||||
:id project-id
|
||||
:name (:name project)})
|
||||
(l/trc :hint "write" :obj "project" :id (str project-id))
|
||||
(write! cfg :project (str project-id) project)
|
||||
(vswap! bfc/*state* update :projects conj project-id)))
|
||||
[cfg project]
|
||||
(events/tap :progress
|
||||
{:op :export
|
||||
:section :write-project
|
||||
:id (:id project)
|
||||
:name (:name project)})
|
||||
(l/trc :hint "write" :obj "project" :id (str (:id project)))
|
||||
(write! cfg :project (str (:id project)) project)
|
||||
(vswap! bfc/*state* update :projects conj (:id project)))
|
||||
|
||||
(defn- write-file!
|
||||
[cfg file-id]
|
||||
@@ -192,7 +190,7 @@
|
||||
[{:keys [::sto/storage] :as cfg} id]
|
||||
(let [sobj (sto/get-object storage id)
|
||||
data (with-open [input (sto/get-object-data storage sobj)]
|
||||
(io/read-as-bytes input))]
|
||||
(io/read input))]
|
||||
|
||||
(l/trc :hint "write" :obj "storage-object" :id (str id) :size (:size sobj))
|
||||
(write! cfg :storage-object id (meta sobj) data)))
|
||||
@@ -347,9 +345,7 @@
|
||||
[cfg team-id]
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
|
||||
cfg (-> (create-database cfg)
|
||||
(update ::sto/storage media/configure-assets-storage))]
|
||||
cfg (create-database cfg)]
|
||||
|
||||
(l/inf :hint "start"
|
||||
:operation "export"
|
||||
@@ -366,7 +362,7 @@
|
||||
(bfc/get-team-projects cfg team-id))
|
||||
|
||||
(run! (partial write-file! cfg)
|
||||
(bfc/get-team-files cfg team-id))
|
||||
(bfc/get-team-files-ids cfg team-id))
|
||||
|
||||
(run! (partial write-storage-object! cfg)
|
||||
(-> bfc/*state* deref :storage-objects))
|
||||
@@ -390,7 +386,6 @@
|
||||
tp (dt/tpoint)
|
||||
|
||||
cfg (-> (create-database cfg path)
|
||||
(update ::sto/storage media/configure-assets-storage)
|
||||
(assoc ::bfc/timestamp (dt/now)))]
|
||||
|
||||
(l/inf :hint "start"
|
||||
|
||||
957
backend/src/app/binfile/v3.clj
Normal file
957
backend/src/app/binfile/v3.clj
Normal file
@@ -0,0 +1,957 @@
|
||||
;; 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.binfile.v3
|
||||
"A ZIP based binary file exportation"
|
||||
(:refer-clojure :exclude [read])
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.thumbnails :as cth]
|
||||
[app.common.types.color :as ctcl]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.plugins :as ctpg]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.typography :as cty]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.storage.impl :as sto.impl]
|
||||
[app.util.events :as events]
|
||||
[app.util.time :as dt]
|
||||
[clojure.java.io :as jio]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
java.io.InputStream
|
||||
java.io.OutputStreamWriter
|
||||
java.util.zip.ZipEntry
|
||||
java.util.zip.ZipFile
|
||||
java.util.zip.ZipOutputStream))
|
||||
|
||||
;; --- SCHEMA
|
||||
|
||||
(def ^:private schema:manifest
|
||||
[:map {:title "Manifest"}
|
||||
[:version ::sm/int]
|
||||
[:type :string]
|
||||
|
||||
[:generated-by {:optional true} :string]
|
||||
|
||||
[:files
|
||||
[:vector
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:project-id ::sm/uuid]]]]
|
||||
|
||||
[:relations {:optional true}
|
||||
[:vector
|
||||
[:tuple ::sm/uuid ::sm/uuid]]]])
|
||||
|
||||
(def ^:private schema:storage-object
|
||||
[:map {:title "StorageObject"}
|
||||
[:id ::sm/uuid]
|
||||
[:size ::sm/int]
|
||||
[:content-type :string]
|
||||
[:bucket [::sm/one-of {:format :string} sto/valid-buckets]]
|
||||
[:hash :string]])
|
||||
|
||||
(def ^:private schema:file-thumbnail
|
||||
[:map {:title "FileThumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:tag :string]
|
||||
[:media-id ::sm/uuid]])
|
||||
|
||||
;; --- ENCODERS
|
||||
|
||||
(def encode-file
|
||||
(sm/encoder ::ctf/file sm/json-transformer))
|
||||
|
||||
(def encode-page
|
||||
(sm/encoder ::ctp/page sm/json-transformer))
|
||||
|
||||
(def encode-shape
|
||||
(sm/encoder ::cts/shape sm/json-transformer))
|
||||
|
||||
(def encode-media
|
||||
(sm/encoder ::ctf/media sm/json-transformer))
|
||||
|
||||
(def encode-component
|
||||
(sm/encoder ::ctc/component sm/json-transformer))
|
||||
|
||||
(def encode-color
|
||||
(sm/encoder ::ctcl/color sm/json-transformer))
|
||||
|
||||
(def encode-typography
|
||||
(sm/encoder ::cty/typography sm/json-transformer))
|
||||
|
||||
(def encode-plugin-data
|
||||
(sm/encoder ::ctpg/plugin-data sm/json-transformer))
|
||||
|
||||
(def encode-storage-object
|
||||
(sm/encoder schema:storage-object sm/json-transformer))
|
||||
|
||||
(def encode-file-thumbnail
|
||||
(sm/encoder schema:file-thumbnail sm/json-transformer))
|
||||
|
||||
;; --- DECODERS
|
||||
|
||||
(def decode-manifest
|
||||
(sm/decoder schema:manifest sm/json-transformer))
|
||||
|
||||
(def decode-media
|
||||
(sm/decoder ::ctf/media sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decoder ::ctc/component sm/json-transformer))
|
||||
|
||||
(def decode-color
|
||||
(sm/decoder ::ctcl/color sm/json-transformer))
|
||||
|
||||
(def decode-file
|
||||
(sm/decoder ::ctf/file sm/json-transformer))
|
||||
|
||||
(def decode-page
|
||||
(sm/decoder ::ctp/page sm/json-transformer))
|
||||
|
||||
(def decode-shape
|
||||
(sm/decoder ::cts/shape sm/json-transformer))
|
||||
|
||||
(def decode-typography
|
||||
(sm/decoder ::cty/typography sm/json-transformer))
|
||||
|
||||
(def decode-plugin-data
|
||||
(sm/decoder ::ctpg/plugin-data sm/json-transformer))
|
||||
|
||||
(def decode-storage-object
|
||||
(sm/decoder schema:storage-object sm/json-transformer))
|
||||
|
||||
(def decode-file-thumbnail
|
||||
(sm/decoder schema:file-thumbnail sm/json-transformer))
|
||||
|
||||
;; --- VALIDATORS
|
||||
|
||||
(def validate-manifest
|
||||
(sm/check-fn schema:manifest))
|
||||
|
||||
(def validate-file
|
||||
(sm/check-fn ::ctf/file))
|
||||
|
||||
(def validate-page
|
||||
(sm/check-fn ::ctp/page))
|
||||
|
||||
(def validate-shape
|
||||
(sm/check-fn ::cts/shape))
|
||||
|
||||
(def validate-media
|
||||
(sm/check-fn ::ctf/media))
|
||||
|
||||
(def validate-color
|
||||
(sm/check-fn ::ctcl/color))
|
||||
|
||||
(def validate-component
|
||||
(sm/check-fn ::ctc/component))
|
||||
|
||||
(def validate-typography
|
||||
(sm/check-fn ::cty/typography))
|
||||
|
||||
(def validate-plugin-data
|
||||
(sm/check-fn ::ctpg/plugin-data))
|
||||
|
||||
(def validate-storage-object
|
||||
(sm/check-fn schema:storage-object))
|
||||
|
||||
(def validate-file-thumbnail
|
||||
(sm/check-fn schema:file-thumbnail))
|
||||
|
||||
;; --- EXPORT IMPL
|
||||
|
||||
(defn- write-entry!
|
||||
[^ZipOutputStream output ^String path data]
|
||||
(.putNextEntry output (ZipEntry. path))
|
||||
(let [writer (OutputStreamWriter. output "UTF-8")]
|
||||
(json/write writer data :indent true :key-fn json/write-camel-key)
|
||||
(.flush writer))
|
||||
(.closeEntry output))
|
||||
|
||||
(defn- get-file
|
||||
[{:keys [::embed-assets ::include-libraries] :as cfg} file-id]
|
||||
|
||||
(when (and include-libraries embed-assets)
|
||||
(throw (IllegalArgumentException.
|
||||
"the `include-libraries` and `embed-assets` are mutally excluding options")))
|
||||
|
||||
(let [detach? (and (not embed-assets) (not include-libraries))]
|
||||
(cond-> (bfc/get-file cfg file-id)
|
||||
detach?
|
||||
(-> (ctf/detach-external-references file-id)
|
||||
(dissoc :libraries))
|
||||
|
||||
embed-assets
|
||||
(update :data #(bfc/embed-assets cfg % file-id)))))
|
||||
|
||||
(defn- resolve-extension
|
||||
[mtype]
|
||||
(case mtype
|
||||
"image/png" ".png"
|
||||
"image/jpeg" ".jpg"
|
||||
"image/gif" ".gif"
|
||||
"image/svg+xml" ".svg"
|
||||
"image/webp" ".webp"
|
||||
"font/woff" ".woff"
|
||||
"font/woff2" ".woff2"
|
||||
"font/ttf" ".ttf"
|
||||
"font/otf" ".otf"
|
||||
"application/octet-stream" ".bin"))
|
||||
|
||||
(defn- export-storage-objects
|
||||
[{:keys [::output] :as cfg}]
|
||||
(let [storage (sto/resolve cfg)]
|
||||
(doseq [id (-> bfc/*state* deref :storage-objects not-empty)]
|
||||
(let [sobject (sto/get-object storage id)
|
||||
smeta (meta sobject)
|
||||
ext (resolve-extension (:content-type smeta))
|
||||
path (str "objects/" id ".json")
|
||||
params (-> (meta sobject)
|
||||
(assoc :id (:id sobject))
|
||||
(assoc :size (:size sobject))
|
||||
(encode-storage-object))]
|
||||
|
||||
(write-entry! output path params)
|
||||
|
||||
(with-open [input (sto/get-object-data storage sobject)]
|
||||
(.putNextEntry output (ZipEntry. (str "objects/" id ext)))
|
||||
(io/copy input output :size (:size sobject))
|
||||
(.closeEntry output))))))
|
||||
|
||||
(defn- export-file
|
||||
[{:keys [::file-id ::output] :as cfg}]
|
||||
(let [file (get-file cfg file-id)
|
||||
media (->> (bfc/get-file-media cfg file)
|
||||
(map (fn [media]
|
||||
(dissoc media :file-id))))
|
||||
|
||||
data (:data file)
|
||||
typographies (:typographies data)
|
||||
components (:components data)
|
||||
colors (:colors data)
|
||||
|
||||
pages (:pages data)
|
||||
pages-index (:pages-index data)
|
||||
|
||||
thumbnails (bfc/get-file-object-thumbnails cfg file-id)]
|
||||
|
||||
(vswap! bfc/*state* update :files assoc file-id
|
||||
{:id file-id
|
||||
:project-id (:project-id file)
|
||||
:name (:name file)})
|
||||
|
||||
(let [file (cond-> (dissoc file :data)
|
||||
(:options data)
|
||||
(assoc :options (:options data))
|
||||
:always
|
||||
(encode-file))
|
||||
path (str "files/" file-id ".json")]
|
||||
(write-entry! output path file))
|
||||
|
||||
(doseq [[index page-id] (d/enumerate pages)]
|
||||
(let [path (str "files/" file-id "/pages/" page-id ".json")
|
||||
page (get pages-index page-id)
|
||||
objects (:objects page)
|
||||
page (-> page
|
||||
(dissoc :objects)
|
||||
(assoc :index index))
|
||||
page (encode-page page)]
|
||||
|
||||
(write-entry! output path page)
|
||||
|
||||
(doseq [[shape-id shape] objects]
|
||||
(let [path (str "files/" file-id "/pages/" page-id "/" shape-id ".json")
|
||||
shape (assoc shape :page-id page-id)
|
||||
shape (encode-shape shape)]
|
||||
(write-entry! output path shape)))))
|
||||
|
||||
(vswap! bfc/*state* bfc/collect-storage-objects media)
|
||||
(vswap! bfc/*state* bfc/collect-storage-objects thumbnails)
|
||||
|
||||
(doseq [{:keys [id] :as media} media]
|
||||
(let [path (str "files/" file-id "/media/" id ".json")
|
||||
media (encode-media media)]
|
||||
(write-entry! output path media)))
|
||||
|
||||
(doseq [thumbnail thumbnails]
|
||||
(let [data (cth/parse-object-id (:object-id thumbnail))
|
||||
path (str "files/" file-id "/thumbnails/" (:tag data) "/" (:page-id data)
|
||||
"/" (:frame-id data) ".json")
|
||||
data (-> data
|
||||
(assoc :media-id (:media-id thumbnail))
|
||||
(encode-file-thumbnail))]
|
||||
(write-entry! output path data)))
|
||||
|
||||
(doseq [[id component] components]
|
||||
(let [path (str "files/" file-id "/components/" id ".json")
|
||||
component (encode-component component)]
|
||||
(write-entry! output path component)))
|
||||
|
||||
(doseq [[id color] colors]
|
||||
(let [path (str "files/" file-id "/colors/" id ".json")
|
||||
color (-> (encode-color color)
|
||||
(dissoc :file-id))
|
||||
color (cond-> color
|
||||
(and (contains? color :path)
|
||||
(str/empty? (:path color)))
|
||||
(dissoc :path))]
|
||||
(write-entry! output path color)))
|
||||
|
||||
(doseq [[id object] typographies]
|
||||
(let [path (str "files/" file-id "/typographies/" id ".json")
|
||||
color (encode-typography object)]
|
||||
(write-entry! output path color)))))
|
||||
|
||||
(defn- export-files
|
||||
[{:keys [::ids ::include-libraries ::output] :as cfg}]
|
||||
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
|
||||
rels (if include-libraries
|
||||
(->> (bfc/get-files-rels cfg ids)
|
||||
(mapv (juxt :file-id :library-file-id)))
|
||||
[])]
|
||||
|
||||
(vswap! bfc/*state* assoc :files (d/ordered-map))
|
||||
|
||||
;; Write all the exporting files
|
||||
(doseq [[index file-id] (d/enumerate ids)]
|
||||
(-> cfg
|
||||
(assoc ::file-id file-id)
|
||||
(assoc ::file-seqn index)
|
||||
(export-file)))
|
||||
|
||||
;; Write manifest file
|
||||
(let [files (:files @bfc/*state*)
|
||||
params {:type "penpot/export-files"
|
||||
:version 1
|
||||
:generated-by (str "penpot/" (:full cf/version))
|
||||
:files (vec (vals files))
|
||||
:relations rels}]
|
||||
(write-entry! output "manifest.json" params))))
|
||||
|
||||
;; --- IMPORT IMPL
|
||||
|
||||
(defn- read-zip-entries
|
||||
[^ZipFile input]
|
||||
(into #{} (iterator-seq (.entries input))))
|
||||
|
||||
(defn- get-zip-entry*
|
||||
[^ZipFile input ^String path]
|
||||
(.getEntry input path))
|
||||
|
||||
(defn- get-zip-entry
|
||||
[input path]
|
||||
(let [entry (get-zip-entry* input path)]
|
||||
(when-not entry
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:hint "the penpot file seems corrupt, missing underlying zip entry"
|
||||
:path path))
|
||||
entry))
|
||||
|
||||
(defn- get-zip-entry-size
|
||||
[^ZipEntry entry]
|
||||
(.getSize entry))
|
||||
|
||||
(defn- zip-entry-name
|
||||
[^ZipEntry entry]
|
||||
(.getName entry))
|
||||
|
||||
(defn- zip-entry-stream
|
||||
^InputStream
|
||||
[^ZipFile input ^ZipEntry entry]
|
||||
(.getInputStream input entry))
|
||||
|
||||
(defn- zip-entry-reader
|
||||
[^ZipFile input ^ZipEntry entry]
|
||||
(-> (zip-entry-stream input entry)
|
||||
(io/reader :encoding "UTF-8")))
|
||||
|
||||
(defn- zip-entry-storage-content
|
||||
"Wraps a ZipFile and ZipEntry into a penpot storage compatible
|
||||
object and avoid creating temporal objects"
|
||||
[input entry]
|
||||
(let [hash (delay (->> entry
|
||||
(zip-entry-stream input)
|
||||
(sto.impl/calculate-hash)))]
|
||||
(reify
|
||||
sto.impl/IContentObject
|
||||
(get-size [_]
|
||||
(get-zip-entry-size entry))
|
||||
|
||||
sto.impl/IContentHash
|
||||
(get-hash [_]
|
||||
(deref hash))
|
||||
|
||||
jio/IOFactory
|
||||
(make-reader [this opts]
|
||||
(jio/make-reader this opts))
|
||||
(make-writer [_ _]
|
||||
(throw (UnsupportedOperationException. "not implemented")))
|
||||
|
||||
(make-input-stream [_ _]
|
||||
(zip-entry-stream input entry))
|
||||
(make-output-stream [_ _]
|
||||
(throw (UnsupportedOperationException. "not implemented"))))))
|
||||
|
||||
(defn- read-manifest
|
||||
[^ZipFile input]
|
||||
(let [entry (get-zip-entry input "manifest.json")]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(let [manifest (json/read reader :key-fn json/read-kebab-key)]
|
||||
(decode-manifest manifest)))))
|
||||
|
||||
(defn- match-media-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/media/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
|
||||
(defn- match-color-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/colors/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
|
||||
(defn- match-component-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/components/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
|
||||
(defn- match-typography-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/typographies/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
|
||||
(defn- match-thumbnail-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/thumbnails/([^/]+)/([^/]+)/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ tag page-id frame-id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:tag tag
|
||||
:page-id (parse-uuid page-id)
|
||||
:frame-id (parse-uuid frame-id)
|
||||
:file-id file-id}))))
|
||||
|
||||
(defn- match-page-entry-fn
|
||||
[file-id]
|
||||
(let [pattern (str "^files/" file-id "/pages/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
|
||||
(defn- match-shape-entry-fn
|
||||
[file-id page-id]
|
||||
(let [pattern (str "^files/" file-id "/pages/" page-id "/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:page-id page-id
|
||||
:id (parse-uuid id)}))))
|
||||
|
||||
(defn- match-storage-entry-fn
|
||||
[]
|
||||
(let [pattern (str "^objects/([^/]+).json$")
|
||||
pattern (re-pattern pattern)]
|
||||
(fn [entry]
|
||||
(when-let [[_ id] (re-matches pattern (zip-entry-name entry))]
|
||||
{:entry entry
|
||||
:id (parse-uuid id)}))))
|
||||
|
||||
(defn- read-entry
|
||||
[^ZipFile input entry]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(json/read reader :key-fn json/read-kebab-key)))
|
||||
|
||||
(defn- read-file
|
||||
[{:keys [::input ::file-id]}]
|
||||
(let [path (str "files/" file-id ".json")
|
||||
entry (get-zip-entry input path)]
|
||||
(-> (read-entry input entry)
|
||||
(decode-file)
|
||||
(validate-file))))
|
||||
|
||||
(defn- read-file-plugin-data
|
||||
[{:keys [::input ::file-id]}]
|
||||
(let [path (str "files/" file-id "/plugin-data.json")
|
||||
entry (get-zip-entry* input path)]
|
||||
(some->> entry
|
||||
(read-entry input)
|
||||
(decode-plugin-data)
|
||||
(validate-plugin-data))))
|
||||
|
||||
(defn- read-file-media
|
||||
[{:keys [::input ::file-id ::entries]}]
|
||||
(->> (keep (match-media-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-media)
|
||||
(validate-media))
|
||||
object (assoc object :file-id file-id)]
|
||||
(if (= id (:id object))
|
||||
(conj result object)
|
||||
result)))
|
||||
[])
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-colors
|
||||
[{:keys [::input ::file-id ::entries]}]
|
||||
(->> (keep (match-color-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-color)
|
||||
(validate-color))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-components
|
||||
[{:keys [::input ::file-id ::entries]}]
|
||||
(->> (keep (match-component-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-component)
|
||||
(validate-component))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-typographies
|
||||
[{:keys [::input ::file-id ::entries]}]
|
||||
(->> (keep (match-typography-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-typography)
|
||||
(validate-typography))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-shapes
|
||||
[{:keys [::input ::file-id ::page-id ::entries] :as cfg}]
|
||||
(->> (keep (match-shape-entry-fn file-id page-id) entries)
|
||||
(reduce (fn [result {:keys [id entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-shape)
|
||||
(validate-shape))]
|
||||
(if (= id (:id object))
|
||||
(assoc result id object)
|
||||
result)))
|
||||
{})
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-pages
|
||||
[{:keys [::input ::file-id ::entries] :as cfg}]
|
||||
(->> (keep (match-page-entry-fn file-id) entries)
|
||||
(keep (fn [{:keys [id entry]}]
|
||||
(let [page (->> (read-entry input entry)
|
||||
(decode-page))
|
||||
page (dissoc page :options)]
|
||||
(when (= id (:id page))
|
||||
(let [objects (-> (assoc cfg ::page-id id)
|
||||
(read-file-shapes))]
|
||||
(assoc page :objects objects))))))
|
||||
(sort-by :index)
|
||||
(reduce (fn [result {:keys [id] :as page}]
|
||||
(assoc result id (dissoc page :index)))
|
||||
(d/ordered-map))))
|
||||
|
||||
(defn- read-file-thumbnails
|
||||
[{:keys [::input ::file-id ::entries] :as cfg}]
|
||||
(->> (keep (match-thumbnail-entry-fn file-id) entries)
|
||||
(reduce (fn [result {:keys [page-id frame-id tag entry]}]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-file-thumbnail)
|
||||
(validate-file-thumbnail))]
|
||||
(if (and (= frame-id (:frame-id object))
|
||||
(= page-id (:page-id object))
|
||||
(= tag (:tag object)))
|
||||
(conj result object)
|
||||
result)))
|
||||
[])
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-data
|
||||
[{:keys [] :as cfg}]
|
||||
(let [colors (read-file-colors cfg)
|
||||
typographies (read-file-typographies cfg)
|
||||
components (read-file-components cfg)
|
||||
plugin-data (read-file-plugin-data cfg)
|
||||
pages (read-file-pages cfg)]
|
||||
|
||||
{:pages (-> pages keys vec)
|
||||
:pages-index (into {} pages)
|
||||
:colors colors
|
||||
:typographies typographies
|
||||
:components components
|
||||
:plugin-data plugin-data}))
|
||||
|
||||
(defn- import-file
|
||||
[{:keys [::db/conn ::project-id ::file-id ::file-name] :as cfg}]
|
||||
(let [file-id' (bfc/lookup-index file-id)
|
||||
file (read-file cfg)
|
||||
media (read-file-media cfg)
|
||||
thumbnails (read-file-thumbnails cfg)]
|
||||
|
||||
(l/dbg :hint "processing file"
|
||||
:id (str file-id')
|
||||
:prev-id (str file-id)
|
||||
:features (str/join "," (:features file))
|
||||
:version (:version file)
|
||||
::l/sync? true)
|
||||
|
||||
(events/tap :progress {:section :file :name file-name})
|
||||
|
||||
(when media
|
||||
;; Update index with media
|
||||
(l/dbg :hint "update media index"
|
||||
:file-id (str file-id')
|
||||
:total (count media)
|
||||
::l/sync? true)
|
||||
|
||||
(vswap! bfc/*state* update :index bfc/update-index (map :id media))
|
||||
(vswap! bfc/*state* update :media into media))
|
||||
|
||||
(when thumbnails
|
||||
(l/dbg :hint "update thumbnails index"
|
||||
:file-id (str file-id')
|
||||
:total (count thumbnails)
|
||||
::l/sync? true)
|
||||
|
||||
(vswap! bfc/*state* update :index bfc/update-index (map :media-id thumbnails))
|
||||
(vswap! bfc/*state* update :thumbnails into thumbnails))
|
||||
|
||||
(let [data (-> (read-file-data cfg)
|
||||
(d/without-nils)
|
||||
(assoc :id file-id')
|
||||
(cond-> (:options file)
|
||||
(assoc :options (:options file))))
|
||||
|
||||
file (-> file
|
||||
(assoc :id file-id')
|
||||
(assoc :data data)
|
||||
(assoc :name file-name)
|
||||
(assoc :project-id project-id)
|
||||
(dissoc :options)
|
||||
(bfc/process-file))]
|
||||
|
||||
(->> file
|
||||
(bfc/register-pending-migrations cfg)
|
||||
(bfc/persist-file! cfg))
|
||||
|
||||
(when (::bfc/overwrite cfg)
|
||||
(db/delete! conn :file-thumbnail {:file-id file-id'}))
|
||||
|
||||
file-id')))
|
||||
|
||||
(defn- import-file-relations
|
||||
[{:keys [::db/conn ::manifest ::bfc/timestamp] :as cfg}]
|
||||
(events/tap :progress {:section :relations})
|
||||
(doseq [[file-id libr-id] (:relations manifest)]
|
||||
|
||||
(let [file-id (bfc/lookup-index file-id)
|
||||
libr-id (bfc/lookup-index libr-id)]
|
||||
|
||||
(when (and file-id libr-id)
|
||||
(l/dbg :hint "create file library link"
|
||||
:file-id (str file-id)
|
||||
:lib-id (str libr-id)
|
||||
::l/sync? true)
|
||||
(db/insert! conn :file-library-rel
|
||||
{:synced-at timestamp
|
||||
:file-id file-id
|
||||
:library-file-id libr-id})))))
|
||||
|
||||
(defn- import-storage-objects
|
||||
[{:keys [::input ::entries ::bfc/timestamp] :as cfg}]
|
||||
(events/tap :progress {:section :storage-objects})
|
||||
|
||||
(let [storage (sto/resolve cfg)
|
||||
entries (keep (match-storage-entry-fn) entries)]
|
||||
|
||||
(doseq [{:keys [id entry]} entries]
|
||||
(let [object (->> (read-entry input entry)
|
||||
(decode-storage-object)
|
||||
(validate-storage-object))]
|
||||
|
||||
(when (not= id (:id object))
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)"
|
||||
:expected-id (str id)
|
||||
:found-id (str (:id object))))
|
||||
|
||||
(let [ext (resolve-extension (:content-type object))
|
||||
path (str "objects/" id ext)
|
||||
content (->> path
|
||||
(get-zip-entry input)
|
||||
(zip-entry-storage-content input))]
|
||||
|
||||
(when (not= (:size object) (sto/get-size content))
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:hint "found corrupted storage object: size does not match"
|
||||
:path path
|
||||
:expected-size (:size object)
|
||||
:found-size (sto/get-size content)))
|
||||
|
||||
(when (not= (:hash object) (sto/get-hash content))
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:hint "found corrupted storage object: hash does not match"
|
||||
:path path
|
||||
:expected-hash (:hash object)
|
||||
:found-hash (sto/get-hash content)))
|
||||
|
||||
(let [params (-> object
|
||||
(dissoc :id :size)
|
||||
(assoc ::sto/content content)
|
||||
(assoc ::sto/deduplicate? true)
|
||||
(assoc ::sto/touched-at timestamp))
|
||||
sobject (sto/put-object! storage params)]
|
||||
|
||||
(l/dbg :hint "persisted storage object"
|
||||
:id (str (:id sobject))
|
||||
:prev-id (str id)
|
||||
:bucket (:bucket params)
|
||||
::l/sync? true)
|
||||
|
||||
(vswap! bfc/*state* update :index assoc id (:id sobject))))))))
|
||||
|
||||
(defn- import-file-media
|
||||
[{:keys [::db/conn] :as cfg}]
|
||||
(events/tap :progress {:section :media})
|
||||
|
||||
(doseq [item (:media @bfc/*state*)]
|
||||
(let [params (-> item
|
||||
(update :id bfc/lookup-index)
|
||||
(update :file-id bfc/lookup-index)
|
||||
(d/update-when :media-id bfc/lookup-index)
|
||||
(d/update-when :thumbnail-id bfc/lookup-index))]
|
||||
|
||||
(l/dbg :hint "inserting file media object"
|
||||
:id (str (:id params))
|
||||
:file-id (str (:file-id params))
|
||||
::l/sync? true)
|
||||
|
||||
(db/insert! conn :file-media-object params
|
||||
{::db/on-conflict-do-nothing? (::bfc/overwrite cfg)}))))
|
||||
|
||||
(defn- import-file-thumbnails
|
||||
[{:keys [::db/conn] :as cfg}]
|
||||
(events/tap :progress {:section :thumbnails})
|
||||
(doseq [item (:thumbnails @bfc/*state*)]
|
||||
(let [file-id (bfc/lookup-index (:file-id item))
|
||||
media-id (bfc/lookup-index (:media-id item))
|
||||
object-id (-> (assoc item :file-id file-id)
|
||||
(cth/fmt-object-id))
|
||||
params {:file-id file-id
|
||||
:object-id object-id
|
||||
:tag (:tag item)
|
||||
:media-id media-id}]
|
||||
|
||||
(l/dbg :hint "inserting file object thumbnail"
|
||||
:file-id (str file-id)
|
||||
:media-id (str media-id)
|
||||
::l/sync? true)
|
||||
|
||||
(db/insert! conn :file-tagged-object-thumbnail params
|
||||
{::db/on-conflict-do-nothing? (::bfc/overwrite cfg)}))))
|
||||
|
||||
(defn- import-files
|
||||
[{:keys [::bfc/timestamp ::input ::name] :or {timestamp (dt/now)} :as cfg}]
|
||||
|
||||
(dm/assert!
|
||||
"expected zip file"
|
||||
(instance? ZipFile input))
|
||||
|
||||
(dm/assert!
|
||||
"expected valid instant"
|
||||
(dt/instant? timestamp))
|
||||
|
||||
(let [manifest (-> (read-manifest input)
|
||||
(validate-manifest))
|
||||
entries (read-zip-entries input)]
|
||||
|
||||
(when-not (= "penpot/export-files" (:type manifest))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-binfile-v3-manifest
|
||||
:hint "unexpected type on manifest"
|
||||
:manifest manifest))
|
||||
|
||||
;; Check if all files referenced on manifest are present
|
||||
(doseq [{file-id :id} (:files manifest)]
|
||||
(let [path (str "files/" file-id ".json")]
|
||||
(when-not (get-zip-entry input path)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-binfile-v3
|
||||
:hint "some files referenced on manifest not found"
|
||||
:path path
|
||||
:file-id file-id))))
|
||||
|
||||
(events/tap :progress {:section :manifest})
|
||||
|
||||
(let [index (bfc/update-index (map :id (:files manifest)))
|
||||
state {:media [] :index index}
|
||||
cfg (-> cfg
|
||||
(assoc ::entries entries)
|
||||
(assoc ::manifest manifest)
|
||||
(assoc ::bfc/timestamp timestamp))]
|
||||
|
||||
(binding [bfc/*state* (volatile! state)]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
(let [ids (->> (:files manifest)
|
||||
(reduce (fn [result {:keys [id] :as file}]
|
||||
(let [name' (get file :name)
|
||||
name' (if (map? name)
|
||||
(get name id)
|
||||
name')]
|
||||
(conj result (-> cfg
|
||||
(assoc ::file-id id)
|
||||
(assoc ::file-name name')
|
||||
(import-file)))))
|
||||
[]))]
|
||||
(import-file-relations cfg)
|
||||
(import-storage-objects cfg)
|
||||
(import-file-media cfg)
|
||||
(import-file-thumbnails cfg)
|
||||
|
||||
(bfc/apply-pending-migrations! cfg)
|
||||
|
||||
ids)))))))
|
||||
|
||||
;; --- PUBLIC API
|
||||
|
||||
(defn export-files!
|
||||
"Do the exportation of a specified file in custom penpot binary
|
||||
format. There are some options available for customize the output:
|
||||
|
||||
`::include-libraries`: additionally to the specified file, all the
|
||||
linked libraries also will be included (including transitive
|
||||
dependencies).
|
||||
|
||||
`::embed-assets`: instead of including the libraries, embed in the
|
||||
same file library all assets used from external libraries."
|
||||
|
||||
[{:keys [::ids] :as cfg} output]
|
||||
|
||||
(dm/assert!
|
||||
"expected a set of uuid's for `::ids` parameter"
|
||||
(and (set? ids)
|
||||
(every? uuid? ids)))
|
||||
|
||||
(dm/assert!
|
||||
"expected instance of jio/IOFactory for `input`"
|
||||
(satisfies? jio/IOFactory output))
|
||||
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
ab (volatile! false)
|
||||
cs (volatile! nil)]
|
||||
(try
|
||||
(l/info :hint "start exportation" :export-id (str id))
|
||||
(binding [bfc/*state* (volatile! (bfc/initial-state))]
|
||||
(with-open [output (io/output-stream output)]
|
||||
(with-open [output (ZipOutputStream. output)]
|
||||
(let [cfg (assoc cfg ::output output)]
|
||||
(export-files cfg)
|
||||
(export-storage-objects cfg)))))
|
||||
|
||||
(catch java.util.zip.ZipException cause
|
||||
(vreset! cs cause)
|
||||
(vreset! ab true)
|
||||
(throw cause))
|
||||
|
||||
(catch java.io.IOException _cause
|
||||
;; Do nothing, EOF means client closes connection abruptly
|
||||
(vreset! ab true)
|
||||
nil)
|
||||
|
||||
(catch Throwable cause
|
||||
(vreset! cs cause)
|
||||
(vreset! ab true)
|
||||
(throw cause))
|
||||
|
||||
(finally
|
||||
(l/info :hint "exportation finished" :export-id (str id)
|
||||
:elapsed (str (inst-ms (tp)) "ms")
|
||||
:aborted @ab
|
||||
:cause @cs)))))
|
||||
|
||||
|
||||
(defn import-files!
|
||||
[{:keys [::input] :as cfg}]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid profile-id and project-id on `cfg`"
|
||||
(and (uuid? (::profile-id cfg))
|
||||
(uuid? (::project-id cfg))))
|
||||
|
||||
(dm/assert!
|
||||
"expected instance of jio/IOFactory for `input`"
|
||||
(io/coercible? input))
|
||||
|
||||
(let [id (uuid/next)
|
||||
tp (dt/tpoint)
|
||||
cs (volatile! nil)]
|
||||
|
||||
(l/info :hint "import: started" :id (str id))
|
||||
(try
|
||||
(with-open [input (ZipFile. (fs/file input))]
|
||||
(import-files (assoc cfg ::input input)))
|
||||
|
||||
(catch Throwable cause
|
||||
(vreset! cs cause)
|
||||
(throw cause))
|
||||
|
||||
(finally
|
||||
(l/info :hint "import: terminated"
|
||||
:id (str id)
|
||||
:elapsed (dt/format-duration (tp))
|
||||
:error? (some? @cs))))))
|
||||
@@ -11,52 +11,40 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.flags :as flags]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.version :as v]
|
||||
[app.util.overrides]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core :as c]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.pprint :as pprint]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[environ.core :refer [env]]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(prefer-method print-method
|
||||
clojure.lang.IRecord
|
||||
clojure.lang.IDeref)
|
||||
|
||||
(prefer-method print-method
|
||||
clojure.lang.IPersistentMap
|
||||
clojure.lang.IDeref)
|
||||
|
||||
(prefer-method pprint/simple-dispatch
|
||||
clojure.lang.IPersistentMap
|
||||
clojure.lang.IDeref)
|
||||
|
||||
(defmethod ig/init-key :default
|
||||
[_ data]
|
||||
(d/without-nils data))
|
||||
|
||||
(defmethod ig/prep-key :default
|
||||
[_ data]
|
||||
(if (map? data)
|
||||
(d/without-nils data)
|
||||
data))
|
||||
(defmethod ig/expand-key :default
|
||||
[k v]
|
||||
{k (if (map? v)
|
||||
(d/without-nils v)
|
||||
v)})
|
||||
|
||||
(def defaults
|
||||
(def default
|
||||
{:database-uri "postgresql://postgres/penpot"
|
||||
:database-username "penpot"
|
||||
:database-password "penpot"
|
||||
|
||||
:default-blob-version 5
|
||||
:default-blob-version 4
|
||||
|
||||
:rpc-rlimit-config (fs/path "resources/rlimit.edn")
|
||||
:rpc-climit-config (fs/path "resources/climit.edn")
|
||||
:rpc-rlimit-config "resources/rlimit.edn"
|
||||
:rpc-climit-config "resources/climit.edn"
|
||||
|
||||
:file-change-snapshot-every 5
|
||||
:file-change-snapshot-timeout "3h"
|
||||
:auto-file-snapshot-total 10
|
||||
:auto-file-snapshot-every 5
|
||||
:auto-file-snapshot-timeout "3h"
|
||||
|
||||
:public-uri "http://localhost:3449"
|
||||
:host "localhost"
|
||||
@@ -64,8 +52,8 @@
|
||||
|
||||
:redis-uri "redis://redis/0"
|
||||
|
||||
:assets-storage-backend :assets-fs
|
||||
:storage-assets-fs-directory "assets"
|
||||
:objects-storage-backend "fs"
|
||||
:objects-storage-fs-directory "assets"
|
||||
|
||||
:assets-path "/internal/assets/"
|
||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||
@@ -92,254 +80,153 @@
|
||||
;; time to avoid email sending after profile modification
|
||||
:email-verify-threshold "15m"})
|
||||
|
||||
(s/def ::default-rpc-rlimit ::us/vector-of-strings)
|
||||
(s/def ::rpc-rlimit-config ::fs/path)
|
||||
(s/def ::rpc-climit-config ::fs/path)
|
||||
(def schema:config
|
||||
(do #_sm/optional-keys
|
||||
[:map {:title "config"}
|
||||
[:flags {:optional true} [::sm/set :string]]
|
||||
[:admins {:optional true} [::sm/set ::sm/email]]
|
||||
[:secret-key {:optional true} :string]
|
||||
|
||||
(s/def ::media-max-file-size ::us/integer)
|
||||
[:tenant {:optional false} :string]
|
||||
[:public-uri {:optional false} :string]
|
||||
[:host {:optional false} :string]
|
||||
|
||||
(s/def ::flags ::us/vector-of-keywords)
|
||||
(s/def ::telemetry-enabled ::us/boolean)
|
||||
[:http-server-port {:optional true} ::sm/int]
|
||||
[:http-server-host {:optional true} :string]
|
||||
[:http-server-max-body-size {:optional true} ::sm/int]
|
||||
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
|
||||
[:http-server-io-threads {:optional true} ::sm/int]
|
||||
[:http-server-worker-threads {:optional true} ::sm/int]
|
||||
|
||||
(s/def ::audit-log-archive-uri ::us/string)
|
||||
(s/def ::audit-log-http-handler-concurrency ::us/integer)
|
||||
[:telemetry-uri {:optional true} :string]
|
||||
[:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE
|
||||
|
||||
(s/def ::email-domain-blacklist ::fs/path)
|
||||
(s/def ::email-domain-whitelist ::fs/path)
|
||||
[:auto-file-snapshot-total {:optional true} ::sm/int]
|
||||
[:auto-file-snapshot-every {:optional true} ::sm/int]
|
||||
[:auto-file-snapshot-timeout {:optional true} ::dt/duration]
|
||||
|
||||
(s/def ::deletion-delay ::dt/duration)
|
||||
[:media-max-file-size {:optional true} ::sm/int]
|
||||
[:deletion-delay {:optional true} ::dt/duration] ;; REVIEW
|
||||
[:telemetry-enabled {:optional true} ::sm/boolean]
|
||||
[:default-blob-version {:optional true} ::sm/int]
|
||||
[:allow-demo-users {:optional true} ::sm/boolean]
|
||||
[:error-report-webhook {:optional true} :string]
|
||||
[:user-feedback-destination {:optional true} :string]
|
||||
|
||||
(s/def ::admins ::us/set-of-valid-emails)
|
||||
(s/def ::file-change-snapshot-every ::us/integer)
|
||||
(s/def ::file-change-snapshot-timeout ::dt/duration)
|
||||
[:default-rpc-rlimit {:optional true} [::sm/vec :string]]
|
||||
[:rpc-rlimit-config {:optional true} ::fs/path]
|
||||
[:rpc-climit-config {:optional true} ::fs/path]
|
||||
|
||||
(s/def ::default-executor-parallelism ::us/integer)
|
||||
(s/def ::scheduled-executor-parallelism ::us/integer)
|
||||
[:audit-log-archive-uri {:optional true} :string]
|
||||
[:audit-log-http-handler-concurrency {:optional true} ::sm/int]
|
||||
|
||||
(s/def ::worker-default-parallelism ::us/integer)
|
||||
(s/def ::worker-webhook-parallelism ::us/integer)
|
||||
[:default-executor-parallelism {:optional true} ::sm/int] ;; REVIEW
|
||||
[:scheduled-executor-parallelism {:optional true} ::sm/int] ;; REVIEW
|
||||
[:worker-default-parallelism {:optional true} ::sm/int]
|
||||
[:worker-webhook-parallelism {:optional true} ::sm/int]
|
||||
|
||||
(s/def ::auth-data-cookie-domain ::us/string)
|
||||
(s/def ::auth-token-cookie-name ::us/string)
|
||||
(s/def ::auth-token-cookie-max-age ::dt/duration)
|
||||
[:database-password {:optional true} [:maybe :string]]
|
||||
[:database-uri {:optional true} ::sm/uri]
|
||||
[:database-username {:optional true} [:maybe :string]]
|
||||
[:database-readonly {:optional true} ::sm/boolean]
|
||||
[:database-min-pool-size {:optional true} ::sm/int]
|
||||
[:database-max-pool-size {:optional true} ::sm/int]
|
||||
|
||||
(s/def ::secret-key ::us/string)
|
||||
(s/def ::allow-demo-users ::us/boolean)
|
||||
(s/def ::assets-path ::us/string)
|
||||
(s/def ::database-password (s/nilable ::us/string))
|
||||
(s/def ::database-uri ::us/string)
|
||||
(s/def ::database-username (s/nilable ::us/string))
|
||||
(s/def ::database-readonly ::us/boolean)
|
||||
(s/def ::database-min-pool-size ::us/integer)
|
||||
(s/def ::database-max-pool-size ::us/integer)
|
||||
[:quotes-teams-per-profile {:optional true} ::sm/int]
|
||||
[:quotes-access-tokens-per-profile {:optional true} ::sm/int]
|
||||
[:quotes-projects-per-team {:optional true} ::sm/int]
|
||||
[:quotes-invitations-per-team {:optional true} ::sm/int]
|
||||
[:quotes-profiles-per-team {:optional true} ::sm/int]
|
||||
[:quotes-files-per-project {:optional true} ::sm/int]
|
||||
[:quotes-files-per-team {:optional true} ::sm/int]
|
||||
[:quotes-font-variants-per-team {:optional true} ::sm/int]
|
||||
[:quotes-comment-threads-per-file {:optional true} ::sm/int]
|
||||
[:quotes-comments-per-file {:optional true} ::sm/int]
|
||||
[:quotes-snapshots-per-file {:optional true} ::sm/int]
|
||||
[:quotes-snapshots-per-team {:optional true} ::sm/int]
|
||||
|
||||
(s/def ::quotes-teams-per-profile ::us/integer)
|
||||
(s/def ::quotes-access-tokens-per-profile ::us/integer)
|
||||
(s/def ::quotes-projects-per-team ::us/integer)
|
||||
(s/def ::quotes-invitations-per-team ::us/integer)
|
||||
(s/def ::quotes-profiles-per-team ::us/integer)
|
||||
(s/def ::quotes-files-per-project ::us/integer)
|
||||
(s/def ::quotes-files-per-team ::us/integer)
|
||||
(s/def ::quotes-font-variants-per-team ::us/integer)
|
||||
(s/def ::quotes-comment-threads-per-file ::us/integer)
|
||||
(s/def ::quotes-comments-per-file ::us/integer)
|
||||
[:auth-data-cookie-domain {:optional true} :string]
|
||||
[:auth-token-cookie-name {:optional true} :string]
|
||||
[:auth-token-cookie-max-age {:optional true} ::dt/duration]
|
||||
|
||||
(s/def ::default-blob-version ::us/integer)
|
||||
(s/def ::error-report-webhook ::us/string)
|
||||
(s/def ::user-feedback-destination ::us/string)
|
||||
(s/def ::github-client-id ::us/string)
|
||||
(s/def ::github-client-secret ::us/string)
|
||||
(s/def ::gitlab-base-uri ::us/string)
|
||||
(s/def ::gitlab-client-id ::us/string)
|
||||
(s/def ::gitlab-client-secret ::us/string)
|
||||
(s/def ::google-client-id ::us/string)
|
||||
(s/def ::google-client-secret ::us/string)
|
||||
(s/def ::oidc-client-id ::us/string)
|
||||
(s/def ::oidc-user-info-source ::us/keyword)
|
||||
(s/def ::oidc-client-secret ::us/string)
|
||||
(s/def ::oidc-base-uri ::us/string)
|
||||
(s/def ::oidc-token-uri ::us/string)
|
||||
(s/def ::oidc-auth-uri ::us/string)
|
||||
(s/def ::oidc-user-uri ::us/string)
|
||||
(s/def ::oidc-jwks-uri ::us/string)
|
||||
(s/def ::oidc-scopes ::us/set-of-strings)
|
||||
(s/def ::oidc-roles ::us/set-of-strings)
|
||||
(s/def ::oidc-roles-attr ::us/string)
|
||||
(s/def ::oidc-email-attr ::us/string)
|
||||
(s/def ::oidc-name-attr ::us/string)
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::http-server-port ::us/integer)
|
||||
(s/def ::http-server-host ::us/string)
|
||||
(s/def ::http-server-max-body-size ::us/integer)
|
||||
(s/def ::http-server-max-multipart-body-size ::us/integer)
|
||||
(s/def ::http-server-io-threads ::us/integer)
|
||||
(s/def ::http-server-worker-threads ::us/integer)
|
||||
(s/def ::ldap-attrs-email ::us/string)
|
||||
(s/def ::ldap-attrs-fullname ::us/string)
|
||||
(s/def ::ldap-attrs-username ::us/string)
|
||||
(s/def ::ldap-base-dn ::us/string)
|
||||
(s/def ::ldap-bind-dn ::us/string)
|
||||
(s/def ::ldap-bind-password ::us/string)
|
||||
(s/def ::ldap-host ::us/string)
|
||||
(s/def ::ldap-port ::us/integer)
|
||||
(s/def ::ldap-ssl ::us/boolean)
|
||||
(s/def ::ldap-starttls ::us/boolean)
|
||||
(s/def ::ldap-user-query ::us/string)
|
||||
(s/def ::media-directory ::us/string)
|
||||
(s/def ::media-uri ::us/string)
|
||||
(s/def ::profile-bounce-max-age ::dt/duration)
|
||||
(s/def ::profile-bounce-threshold ::us/integer)
|
||||
(s/def ::profile-complaint-max-age ::dt/duration)
|
||||
(s/def ::profile-complaint-threshold ::us/integer)
|
||||
(s/def ::public-uri ::us/string)
|
||||
(s/def ::redis-uri ::us/string)
|
||||
(s/def ::registration-domain-whitelist ::us/set-of-strings)
|
||||
[:registration-domain-whitelist {:optional true} [::sm/set :string]]
|
||||
[:email-verify-threshold {:optional true} ::dt/duration]
|
||||
|
||||
(s/def ::smtp-default-from ::us/string)
|
||||
(s/def ::smtp-default-reply-to ::us/string)
|
||||
(s/def ::smtp-host ::us/string)
|
||||
(s/def ::smtp-password (s/nilable ::us/string))
|
||||
(s/def ::smtp-port ::us/integer)
|
||||
(s/def ::smtp-ssl ::us/boolean)
|
||||
(s/def ::smtp-tls ::us/boolean)
|
||||
(s/def ::smtp-username (s/nilable ::us/string))
|
||||
(s/def ::urepl-host ::us/string)
|
||||
(s/def ::urepl-port ::us/integer)
|
||||
(s/def ::prepl-host ::us/string)
|
||||
(s/def ::prepl-port ::us/integer)
|
||||
(s/def ::assets-storage-backend ::us/keyword)
|
||||
(s/def ::storage-assets-fs-directory ::us/string)
|
||||
(s/def ::storage-assets-s3-bucket ::us/string)
|
||||
(s/def ::storage-assets-s3-region ::us/keyword)
|
||||
(s/def ::storage-assets-s3-endpoint ::us/string)
|
||||
(s/def ::storage-assets-s3-io-threads ::us/integer)
|
||||
(s/def ::telemetry-uri ::us/string)
|
||||
(s/def ::telemetry-with-taiga ::us/boolean)
|
||||
(s/def ::tenant ::us/string)
|
||||
(s/def ::email-verify-threshold ::dt/duration)
|
||||
[:github-client-id {:optional true} :string]
|
||||
[:github-client-secret {:optional true} :string]
|
||||
[:gitlab-base-uri {:optional true} :string]
|
||||
[:gitlab-client-id {:optional true} :string]
|
||||
[:gitlab-client-secret {:optional true} :string]
|
||||
[:google-client-id {:optional true} :string]
|
||||
[:google-client-secret {:optional true} :string]
|
||||
[:oidc-client-id {:optional true} :string]
|
||||
[:oidc-user-info-source {:optional true} :keyword]
|
||||
[:oidc-client-secret {:optional true} :string]
|
||||
[:oidc-base-uri {:optional true} :string]
|
||||
[:oidc-token-uri {:optional true} :string]
|
||||
[:oidc-auth-uri {:optional true} :string]
|
||||
[:oidc-user-uri {:optional true} :string]
|
||||
[:oidc-jwks-uri {:optional true} :string]
|
||||
[:oidc-scopes {:optional true} [::sm/set :string]]
|
||||
[:oidc-roles {:optional true} [::sm/set :string]]
|
||||
[:oidc-roles-attr {:optional true} :string]
|
||||
[:oidc-email-attr {:optional true} :string]
|
||||
[:oidc-name-attr {:optional true} :string]
|
||||
|
||||
(s/def ::config
|
||||
(s/keys :opt-un [::secret-key
|
||||
::flags
|
||||
::admins
|
||||
::deletion-delay
|
||||
::allow-demo-users
|
||||
::audit-log-archive-uri
|
||||
::audit-log-http-handler-concurrency
|
||||
::auth-token-cookie-name
|
||||
::auth-token-cookie-max-age
|
||||
::authenticated-cookie-domain
|
||||
::database-password
|
||||
::database-uri
|
||||
::database-username
|
||||
::database-readonly
|
||||
::database-min-pool-size
|
||||
::database-max-pool-size
|
||||
::default-blob-version
|
||||
::default-rpc-rlimit
|
||||
::email-domain-blacklist
|
||||
::email-domain-whitelist
|
||||
::error-report-webhook
|
||||
::default-executor-parallelism
|
||||
::scheduled-executor-parallelism
|
||||
::worker-default-parallelism
|
||||
::worker-webhook-parallelism
|
||||
::file-change-snapshot-every
|
||||
::file-change-snapshot-timeout
|
||||
::user-feedback-destination
|
||||
::github-client-id
|
||||
::github-client-secret
|
||||
::gitlab-base-uri
|
||||
::gitlab-client-id
|
||||
::gitlab-client-secret
|
||||
::google-client-id
|
||||
::google-client-secret
|
||||
::oidc-client-id
|
||||
::oidc-client-secret
|
||||
::oidc-user-info-source
|
||||
::oidc-base-uri
|
||||
::oidc-token-uri
|
||||
::oidc-auth-uri
|
||||
::oidc-user-uri
|
||||
::oidc-jwks-uri
|
||||
::oidc-scopes
|
||||
::oidc-roles-attr
|
||||
::oidc-email-attr
|
||||
::oidc-name-attr
|
||||
::oidc-roles
|
||||
::host
|
||||
::http-server-host
|
||||
::http-server-port
|
||||
::http-server-max-body-size
|
||||
::http-server-max-multipart-body-size
|
||||
::http-server-io-threads
|
||||
::http-server-worker-threads
|
||||
::ldap-attrs-email
|
||||
::ldap-attrs-fullname
|
||||
::ldap-attrs-username
|
||||
::ldap-base-dn
|
||||
::ldap-bind-dn
|
||||
::ldap-bind-password
|
||||
::ldap-host
|
||||
::ldap-port
|
||||
::ldap-ssl
|
||||
::ldap-starttls
|
||||
::ldap-user-query
|
||||
::local-assets-uri
|
||||
::media-max-file-size
|
||||
::profile-bounce-max-age
|
||||
::profile-bounce-threshold
|
||||
::profile-complaint-max-age
|
||||
::profile-complaint-threshold
|
||||
::public-uri
|
||||
[:ldap-attrs-email {:optional true} :string]
|
||||
[:ldap-attrs-fullname {:optional true} :string]
|
||||
[:ldap-attrs-username {:optional true} :string]
|
||||
[:ldap-base-dn {:optional true} :string]
|
||||
[:ldap-bind-dn {:optional true} :string]
|
||||
[:ldap-bind-password {:optional true} :string]
|
||||
[:ldap-host {:optional true} :string]
|
||||
[:ldap-port {:optional true} ::sm/int]
|
||||
[:ldap-ssl {:optional true} ::sm/boolean]
|
||||
[:ldap-starttls {:optional true} ::sm/boolean]
|
||||
[:ldap-user-query {:optional true} :string]
|
||||
|
||||
::quotes-teams-per-profile
|
||||
::quotes-access-tokens-per-profile
|
||||
::quotes-projects-per-team
|
||||
::quotes-invitations-per-team
|
||||
::quotes-profiles-per-team
|
||||
::quotes-files-per-project
|
||||
::quotes-files-per-team
|
||||
::quotes-font-variants-per-team
|
||||
::quotes-comment-threads-per-file
|
||||
::quotes-comments-per-file
|
||||
[:profile-bounce-max-age {:optional true} ::dt/duration]
|
||||
[:profile-bounce-threshold {:optional true} ::sm/int]
|
||||
[:profile-complaint-max-age {:optional true} ::dt/duration]
|
||||
[:profile-complaint-threshold {:optional true} ::sm/int]
|
||||
|
||||
::redis-uri
|
||||
::registration-domain-whitelist
|
||||
::rpc-rlimit-config
|
||||
::rpc-climit-config
|
||||
[:redis-uri {:optional true} ::sm/uri]
|
||||
|
||||
::semaphore-process-font
|
||||
::semaphore-process-image
|
||||
::semaphore-update-file
|
||||
::semaphore-auth
|
||||
[:email-domain-blacklist {:optional true} ::fs/path]
|
||||
[:email-domain-whitelist {:optional true} ::fs/path]
|
||||
|
||||
::smtp-default-from
|
||||
::smtp-default-reply-to
|
||||
::smtp-host
|
||||
::smtp-password
|
||||
::smtp-port
|
||||
::smtp-ssl
|
||||
::smtp-tls
|
||||
::smtp-username
|
||||
[:smtp-default-from {:optional true} :string]
|
||||
[:smtp-default-reply-to {:optional true} :string]
|
||||
[:smtp-host {:optional true} :string]
|
||||
[:smtp-password {:optional true} [:maybe :string]]
|
||||
[:smtp-port {:optional true} ::sm/int]
|
||||
[:smtp-ssl {:optional true} ::sm/boolean]
|
||||
[:smtp-tls {:optional true} ::sm/boolean]
|
||||
[:smtp-username {:optional true} [:maybe :string]]
|
||||
|
||||
::urepl-host
|
||||
::urepl-port
|
||||
::prepl-host
|
||||
::prepl-port
|
||||
[:urepl-host {:optional true} :string]
|
||||
[:urepl-port {:optional true} ::sm/int]
|
||||
[:prepl-host {:optional true} :string]
|
||||
[:prepl-port {:optional true} ::sm/int]
|
||||
|
||||
::assets-storage-backend
|
||||
::storage-assets-fs-directory
|
||||
::storage-assets-s3-bucket
|
||||
::storage-assets-s3-region
|
||||
::storage-assets-s3-endpoint
|
||||
::storage-assets-s3-io-threads
|
||||
::telemetry-enabled
|
||||
::telemetry-uri
|
||||
::telemetry-referer
|
||||
::telemetry-with-taiga
|
||||
::tenant
|
||||
::email-verify-threshold]))
|
||||
[:media-directory {:optional true} :string] ;; REVIEW
|
||||
[:media-uri {:optional true} :string]
|
||||
[:assets-path {:optional true} :string]
|
||||
|
||||
;; Legacy, will be removed in 2.5
|
||||
[:assets-storage-backend {:optional true} :keyword]
|
||||
[:storage-assets-fs-directory {:optional true} :string]
|
||||
[:storage-assets-s3-bucket {:optional true} :string]
|
||||
[:storage-assets-s3-region {:optional true} :keyword]
|
||||
[:storage-assets-s3-endpoint {:optional true} ::sm/uri]
|
||||
[:storage-assets-s3-io-threads {:optional true} ::sm/int]
|
||||
|
||||
[:objects-storage-backend {:optional true} :keyword]
|
||||
[:objects-storage-fs-directory {:optional true} :string]
|
||||
[:objects-storage-s3-bucket {:optional true} :string]
|
||||
[:objects-storage-s3-region {:optional true} :keyword]
|
||||
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
|
||||
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
|
||||
|
||||
(def default-flags
|
||||
[:enable-backend-api-doc
|
||||
@@ -367,20 +254,22 @@
|
||||
{}
|
||||
env)))
|
||||
|
||||
(defn- read-config
|
||||
[]
|
||||
(try
|
||||
(->> (read-env "penpot")
|
||||
(merge defaults)
|
||||
(us/conform ::config))
|
||||
(catch Throwable e
|
||||
(when (ex/error? e)
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
|
||||
(println "Error on validating configuration:")
|
||||
(println (some-> e ex-data ex/explain))
|
||||
(println (ex/explain (ex-data e)))
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))
|
||||
(throw e))))
|
||||
(def decode-config
|
||||
(sm/decoder schema:config sm/string-transformer))
|
||||
|
||||
(def validate-config
|
||||
(sm/validator schema:config))
|
||||
|
||||
(def explain-config
|
||||
(sm/explainer schema:config))
|
||||
|
||||
(defn read-config
|
||||
"Reads the configuration from enviroment variables and decodes all
|
||||
known values."
|
||||
[& {:keys [prefix default] :or {prefix "penpot"}}]
|
||||
(->> (read-env prefix)
|
||||
(merge default)
|
||||
(decode-config)))
|
||||
|
||||
(def version
|
||||
(v/parse (or (some-> (io/resource "version.txt")
|
||||
@@ -388,10 +277,28 @@
|
||||
(str/trim))
|
||||
"%version%")))
|
||||
|
||||
(defonce ^:dynamic config (read-config))
|
||||
(defonce ^:dynamic config (read-config :default default))
|
||||
(defonce ^:dynamic flags (parse-flags config))
|
||||
|
||||
(def deletion-delay
|
||||
(defn validate!
|
||||
"Validate the currently loaded configuration data."
|
||||
[& {:keys [exit-on-error?] :or {exit-on-error? true}}]
|
||||
(if (validate-config config)
|
||||
true
|
||||
(let [explain (explain-config config)]
|
||||
(println "Error on validating configuration:")
|
||||
(sm/pretty-explain explain
|
||||
:variant ::sm/schemaless-explain
|
||||
:message "Configuration Validation Error")
|
||||
(flush)
|
||||
(if exit-on-error?
|
||||
(System/exit -1)
|
||||
(ex/raise :type :validation
|
||||
:code :config-validaton
|
||||
::sm/explain explain)))))
|
||||
|
||||
(defn get-deletion-delay
|
||||
[]
|
||||
(or (c/get config :deletion-delay)
|
||||
(dt/duration {:days 7})))
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db.sql :as sql]
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.util.time :as dt]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[next.jdbc :as jdbc]
|
||||
[next.jdbc.date-time :as jdbc-dt])
|
||||
@@ -49,27 +48,17 @@
|
||||
;; Initialization
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::connection-timeout ::us/integer)
|
||||
(s/def ::max-size ::us/integer)
|
||||
(s/def ::min-size ::us/integer)
|
||||
(s/def ::name keyword?)
|
||||
(s/def ::password ::us/string)
|
||||
(s/def ::uri ::us/not-empty-string)
|
||||
(s/def ::username ::us/string)
|
||||
(s/def ::validation-timeout ::us/integer)
|
||||
(s/def ::read-only? ::us/boolean)
|
||||
|
||||
(s/def ::pool-options
|
||||
(s/keys :opt [::uri
|
||||
::name
|
||||
::min-size
|
||||
::max-size
|
||||
::connection-timeout
|
||||
::validation-timeout
|
||||
::username
|
||||
::password
|
||||
::mtx/metrics
|
||||
::read-only?]))
|
||||
(def ^:private schema:pool-options
|
||||
[:map {:title "pool-options"}
|
||||
[::connect-timeout {:optional true} ::sm/int]
|
||||
[::max-size {:optional true} ::sm/int]
|
||||
[::min-size {:optional true} ::sm/int]
|
||||
[::name {:optional true} :keyword]
|
||||
[::uri {:optional true} ::sm/uri]
|
||||
[::password {:optional true} :string]
|
||||
[::username {:optional true} :string]
|
||||
[::validation-timeout {:optional true} ::sm/int]
|
||||
[::read-only {:optional true} ::sm/boolean]])
|
||||
|
||||
(def defaults
|
||||
{::name :main
|
||||
@@ -79,27 +68,26 @@
|
||||
::validation-timeout 10000
|
||||
::idle-timeout 120000 ; 2min
|
||||
::max-lifetime 1800000 ; 30m
|
||||
::read-only? false})
|
||||
::read-only false})
|
||||
|
||||
(defmethod ig/prep-key ::pool
|
||||
[_ cfg]
|
||||
(merge defaults (d/without-nils cfg)))
|
||||
|
||||
;; Don't validate here, just validate that a map is received.
|
||||
(defmethod ig/pre-init-spec ::pool [_] ::pool-options)
|
||||
(defmethod ig/assert-key ::pool
|
||||
[_ options]
|
||||
(assert (sm/check schema:pool-options options)))
|
||||
|
||||
(defmethod ig/init-key ::pool
|
||||
[_ {:keys [::uri ::read-only?] :as cfg}]
|
||||
(when uri
|
||||
(l/info :hint "initialize connection pool"
|
||||
:name (d/name (::name cfg))
|
||||
:uri uri
|
||||
:read-only read-only?
|
||||
:with-credentials (and (contains? cfg ::username)
|
||||
(contains? cfg ::password))
|
||||
:min-size (::min-size cfg)
|
||||
:max-size (::max-size cfg))
|
||||
(create-pool cfg)))
|
||||
[_ cfg]
|
||||
(let [{:keys [::uri ::read-only] :as cfg}
|
||||
(merge defaults cfg)]
|
||||
(when uri
|
||||
(l/info :hint "initialize connection pool"
|
||||
:name (d/name (::name cfg))
|
||||
:uri (str uri)
|
||||
:read-only read-only
|
||||
:credentials (and (contains? cfg ::username)
|
||||
(contains? cfg ::password))
|
||||
:min-size (::min-size cfg)
|
||||
:max-size (::max-size cfg))
|
||||
(create-pool cfg))))
|
||||
|
||||
(defmethod ig/halt-key! ::pool
|
||||
[_ pool]
|
||||
@@ -115,13 +103,15 @@
|
||||
"SET idle_in_transaction_session_timeout = 300000;"))
|
||||
|
||||
(defn- create-datasource-config
|
||||
[{:keys [::mtx/metrics ::uri] :as cfg}]
|
||||
[{:keys [::uri] :as cfg}]
|
||||
|
||||
;; (app.common.pprint/pprint cfg)
|
||||
(let [config (HikariConfig.)]
|
||||
(doto config
|
||||
(.setJdbcUrl (str "jdbc:" uri))
|
||||
(.setPoolName (d/name (::name cfg)))
|
||||
(.setAutoCommit true)
|
||||
(.setReadOnly (::read-only? cfg))
|
||||
(.setReadOnly (::read-only cfg))
|
||||
(.setConnectionTimeout (::connection-timeout cfg))
|
||||
(.setValidationTimeout (::validation-timeout cfg))
|
||||
(.setIdleTimeout (::idle-timeout cfg))
|
||||
@@ -132,8 +122,8 @@
|
||||
(.setInitializationFailTimeout -1))
|
||||
|
||||
;; When metrics namespace is provided
|
||||
(when metrics
|
||||
(->> (::mtx/registry metrics)
|
||||
(when-let [instance (::mtx/metrics cfg)]
|
||||
(->> (mtx/get-registry instance)
|
||||
(PrometheusMetricsTrackerFactory.)
|
||||
(.setMetricsTrackerFactory config)))
|
||||
|
||||
@@ -150,10 +140,22 @@
|
||||
[conn]
|
||||
(instance? Connection conn))
|
||||
|
||||
(s/def ::conn some?)
|
||||
(s/def ::nilable-pool (s/nilable ::pool))
|
||||
(s/def ::pool pool?)
|
||||
(s/def ::pool-or-conn some?)
|
||||
(defn connectable?
|
||||
[o]
|
||||
(or (connection? o)
|
||||
(pool? o)))
|
||||
|
||||
(sm/register!
|
||||
{:type ::conn
|
||||
:pred connection?})
|
||||
|
||||
(sm/register!
|
||||
{:type ::connectable
|
||||
:pred connectable?})
|
||||
|
||||
(sm/register!
|
||||
{:type ::pool
|
||||
:pred pool?})
|
||||
|
||||
(defn closed?
|
||||
[pool]
|
||||
@@ -407,6 +409,7 @@
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:table table
|
||||
:params params
|
||||
:hint "database object not found"))
|
||||
row))
|
||||
|
||||
|
||||
@@ -7,19 +7,17 @@
|
||||
(ns app.email
|
||||
"Main api for send emails."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.email.invite-to-team :as-alias email.invite-to-team]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.template :as tmpl]
|
||||
[app.worker :as wrk]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
@@ -149,9 +147,27 @@
|
||||
"mail.smtp.timeout" timeout
|
||||
"mail.smtp.connectiontimeout" timeout}))
|
||||
|
||||
(def ^:private schema:smtp-config
|
||||
[:map
|
||||
[::username {:optional true} :string]
|
||||
[::password {:optional true} :string]
|
||||
[::tls {:optional true} ::sm/boolean]
|
||||
[::ssl {:optional true} ::sm/boolean]
|
||||
[::host {:optional true} :string]
|
||||
[::port {:optional true} ::sm/int]
|
||||
[::default-from {:optional true} :string]
|
||||
[::default-reply-to {:optional true} :string]])
|
||||
|
||||
(def valid-smtp-config?
|
||||
(sm/check-fn schema:smtp-config))
|
||||
|
||||
(defn- create-smtp-session
|
||||
^Session
|
||||
[cfg]
|
||||
(dm/assert!
|
||||
"expected valid smtp config"
|
||||
(valid-smtp-config? cfg))
|
||||
|
||||
(let [props (opts->props cfg)]
|
||||
(Session/getInstance props)))
|
||||
|
||||
@@ -201,50 +217,47 @@
|
||||
[{:type "text/html"
|
||||
:content html}]))}))
|
||||
|
||||
(s/def ::priority #{:high :low})
|
||||
(s/def ::to (s/or :single ::us/email
|
||||
:multi (s/coll-of ::us/email)))
|
||||
(s/def ::from ::us/email)
|
||||
(s/def ::reply-to ::us/email)
|
||||
(s/def ::lang string?)
|
||||
(s/def ::extra-data ::us/string)
|
||||
(def ^:private schema:context
|
||||
[:map
|
||||
[:to [:or ::sm/email [::sm/vec ::sm/email]]]
|
||||
[:reply-to {:optional true} ::sm/email]
|
||||
[:from {:optional true} ::sm/email]
|
||||
[:lang {:optional true} ::sm/text]
|
||||
[:priority {:optional true} [:enum :high :low]]
|
||||
[:extra-data {:optional true} ::sm/text]])
|
||||
|
||||
(s/def ::context
|
||||
(s/keys :req-un [::to]
|
||||
:opt-un [::reply-to ::from ::lang ::priority ::extra-data]))
|
||||
(def ^:private valid-context?
|
||||
(sm/validator schema:context))
|
||||
|
||||
(defn template-factory
|
||||
([id] (template-factory id {}))
|
||||
([id extra-context]
|
||||
(s/assert keyword? id)
|
||||
(fn [context]
|
||||
(us/verify ::context context)
|
||||
(when-let [spec (s/get-spec id)]
|
||||
(s/assert spec context))
|
||||
[& {:keys [id schema]}]
|
||||
(assert (keyword? id) "id should be provided and it should be a keyword")
|
||||
(let [check-fn (if schema
|
||||
(sm/check-fn schema)
|
||||
(constantly nil))]
|
||||
(fn [context]
|
||||
(assert (valid-context? context) "expected a valid context")
|
||||
(check-fn context)
|
||||
|
||||
(let [context (merge (if (fn? extra-context)
|
||||
(extra-context)
|
||||
extra-context)
|
||||
context)
|
||||
email (build-email-template id context)]
|
||||
(when-not email
|
||||
(ex/raise :type :internal
|
||||
:code :email-template-does-not-exists
|
||||
:hint "seems like the template is wrong or does not exists."
|
||||
:context {:id id}))
|
||||
(cond-> (assoc email :id (name id))
|
||||
(:extra-data context)
|
||||
(assoc :extra-data (:extra-data context))
|
||||
(let [email (build-email-template id context)]
|
||||
(when-not email
|
||||
(ex/raise :type :internal
|
||||
:code :email-template-does-not-exists
|
||||
:hint "seems like the template is wrong or does not exists."
|
||||
:template-id id))
|
||||
|
||||
(:from context)
|
||||
(assoc :from (:from context))
|
||||
(cond-> (assoc email :id (name id))
|
||||
(:extra-data context)
|
||||
(assoc :extra-data (:extra-data context))
|
||||
|
||||
(:reply-to context)
|
||||
(assoc :reply-to (:reply-to context))
|
||||
(:from context)
|
||||
(assoc :from (:from context))
|
||||
|
||||
(:to context)
|
||||
(assoc :to (:to context)))))))
|
||||
(:reply-to context)
|
||||
(assoc :reply-to (:reply-to context))
|
||||
|
||||
(:to context)
|
||||
(assoc :to (:to context)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC HIGH-LEVEL API
|
||||
@@ -258,7 +271,8 @@
|
||||
"Schedule an already defined email to be sent using asynchronously
|
||||
using worker task."
|
||||
[{:keys [::conn ::factory] :as context}]
|
||||
(us/verify some? conn)
|
||||
(assert (db/connection? conn) "expected a valid database connection")
|
||||
|
||||
(let [email (if factory
|
||||
(factory context)
|
||||
(dissoc context ::conn))]
|
||||
@@ -273,32 +287,8 @@
|
||||
;; SENDMAIL FN / TASK HANDLER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::username ::cf/smtp-username)
|
||||
(s/def ::password ::cf/smtp-password)
|
||||
(s/def ::tls ::cf/smtp-tls)
|
||||
(s/def ::ssl ::cf/smtp-ssl)
|
||||
(s/def ::host ::cf/smtp-host)
|
||||
(s/def ::port ::cf/smtp-port)
|
||||
(s/def ::default-reply-to ::cf/smtp-default-reply-to)
|
||||
(s/def ::default-from ::cf/smtp-default-from)
|
||||
|
||||
(s/def ::smtp-config
|
||||
(s/keys :opt [::username
|
||||
::password
|
||||
::tls
|
||||
::ssl
|
||||
::host
|
||||
::port
|
||||
::default-from
|
||||
::default-reply-to]))
|
||||
|
||||
(declare send-to-logger!)
|
||||
|
||||
(s/def ::sendmail fn?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::sendmail [_]
|
||||
(s/spec ::smtp-config))
|
||||
|
||||
(defmethod ig/init-key ::sendmail
|
||||
[_ cfg]
|
||||
(fn [params]
|
||||
@@ -315,19 +305,18 @@
|
||||
(l/dbg :hint "sendmail"
|
||||
:id (:id params)
|
||||
:to (:to params)
|
||||
:subject (str/trim (:subject params))
|
||||
:body (str/join "," (map :type (:body params))))
|
||||
:subject (str/trim (:subject params)))
|
||||
|
||||
(.sendMessage ^Transport transport
|
||||
^MimeMessage message
|
||||
(.getAllRecipients message))))))
|
||||
|
||||
(when (or (contains? cf/flags :log-emails)
|
||||
(not (contains? cf/flags :smtp)))
|
||||
(when (contains? cf/flags :log-emails)
|
||||
(send-to-logger! cfg params))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::sendmail ::mtx/metrics]))
|
||||
(defmethod ig/assert-key ::handler
|
||||
[_ params]
|
||||
(assert (fn? (::sendmail params)) "expected valid sendmail handler"))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [::sendmail]}]
|
||||
@@ -354,52 +343,113 @@
|
||||
;; EMAIL FACTORIES
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::subject ::us/string)
|
||||
(s/def ::content ::us/string)
|
||||
|
||||
(s/def ::feedback
|
||||
(s/keys :req-un [::subject ::content]))
|
||||
(def ^:private schema:feedback
|
||||
[:map
|
||||
[:subject ::sm/text]
|
||||
[:content ::sm/text]])
|
||||
|
||||
(def feedback
|
||||
"A profile feedback email."
|
||||
(template-factory ::feedback))
|
||||
(template-factory
|
||||
:id ::feedback
|
||||
:schema schema:feedback))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::register
|
||||
(s/keys :req-un [::name]))
|
||||
(def ^:private schema:register
|
||||
[:map [:name ::sm/text]])
|
||||
|
||||
(def register
|
||||
"A new profile registration welcome email."
|
||||
(template-factory ::register))
|
||||
(template-factory
|
||||
:id ::register
|
||||
:schema schema:register))
|
||||
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::password-recovery
|
||||
(s/keys :req-un [::name ::token]))
|
||||
(def ^:private schema:password-recovery
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:token ::sm/text]])
|
||||
|
||||
(def password-recovery
|
||||
"A password recovery notification email."
|
||||
(template-factory ::password-recovery))
|
||||
(template-factory
|
||||
:id ::password-recovery
|
||||
:schema schema:password-recovery))
|
||||
|
||||
(s/def ::pending-email ::us/email)
|
||||
(s/def ::change-email
|
||||
(s/keys :req-un [::name ::pending-email ::token]))
|
||||
(def ^:private schema:change-email
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:pending-email ::sm/email]
|
||||
[:token ::sm/text]])
|
||||
|
||||
(def change-email
|
||||
"Password change confirmation email"
|
||||
(template-factory ::change-email))
|
||||
(template-factory
|
||||
:id ::change-email
|
||||
:schema schema:change-email))
|
||||
|
||||
(s/def ::email.invite-to-team/invited-by ::us/string)
|
||||
(s/def ::email.invite-to-team/team ::us/string)
|
||||
(s/def ::email.invite-to-team/token ::us/string)
|
||||
|
||||
(s/def ::invite-to-team
|
||||
(s/keys :req-un [::email.invite-to-team/invited-by
|
||||
::email.invite-to-team/token
|
||||
::email.invite-to-team/team]))
|
||||
(def ^:private schema:invite-to-team
|
||||
[:map
|
||||
[:invited-by ::sm/text]
|
||||
[:team ::sm/text]
|
||||
[:token ::sm/text]])
|
||||
|
||||
(def invite-to-team
|
||||
"Teams member invitation email."
|
||||
(template-factory ::invite-to-team))
|
||||
(template-factory
|
||||
:id ::invite-to-team
|
||||
:schema schema:invite-to-team))
|
||||
|
||||
(def ^:private schema:join-team
|
||||
[:map
|
||||
[:invited-by ::sm/text]
|
||||
[:team ::sm/text]
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(def join-team
|
||||
"Teams member joined after request email."
|
||||
(template-factory
|
||||
:id ::join-team
|
||||
:schema schema:join-team))
|
||||
|
||||
(def ^:private schema:request-file-access
|
||||
[:map
|
||||
[:requested-by ::sm/text]
|
||||
[:requested-by-email ::sm/text]
|
||||
[:team-name ::sm/text]
|
||||
[:team-id ::sm/uuid]
|
||||
[:file-name ::sm/text]
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]])
|
||||
|
||||
(def request-file-access
|
||||
"File access request email."
|
||||
(template-factory
|
||||
:id ::request-file-access
|
||||
:schema schema:request-file-access))
|
||||
|
||||
(def request-file-access-yourpenpot
|
||||
"File access on Your Penpot request email."
|
||||
(template-factory
|
||||
:id ::request-file-access-yourpenpot
|
||||
:schema schema:request-file-access))
|
||||
|
||||
(def request-file-access-yourpenpot-view
|
||||
"File access on Your Penpot view mode request email."
|
||||
(template-factory
|
||||
:id ::request-file-access-yourpenpot-view
|
||||
:schema schema:request-file-access))
|
||||
|
||||
(def ^:private schema:request-team-access
|
||||
[:map
|
||||
[:requested-by ::sm/text]
|
||||
[:requested-by-email ::sm/text]
|
||||
[:team-name ::sm/text]
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(def request-team-access
|
||||
"Team access request email."
|
||||
(template-factory
|
||||
:id ::request-team-access
|
||||
:schema schema:request-team-access))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; BOUNCE/COMPLAINS HELPERS
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
[app.common.types.shape.path :as ctsp]
|
||||
[app.common.types.shape.text :as ctsx]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.features.fdata :as fdata]
|
||||
@@ -62,6 +63,7 @@
|
||||
[datoteka.io :as io]
|
||||
[promesa.util :as pu]))
|
||||
|
||||
|
||||
(def ^:dynamic *stats*
|
||||
"A dynamic var for setting up state for collect stats globally."
|
||||
nil)
|
||||
@@ -113,7 +115,7 @@
|
||||
(sm/lazy-validator ::ctc/color))
|
||||
|
||||
(def valid-fill?
|
||||
(sm/lazy-validator ::cts/fill))
|
||||
(sm/lazy-validator cts/schema:fill))
|
||||
|
||||
(def valid-stroke?
|
||||
(sm/lazy-validator ::cts/stroke))
|
||||
@@ -134,10 +136,10 @@
|
||||
(sm/lazy-validator ::ctc/rgb-color))
|
||||
|
||||
(def valid-shape-points?
|
||||
(sm/lazy-validator ::cts/points))
|
||||
(sm/lazy-validator cts/schema:points))
|
||||
|
||||
(def valid-image-attrs?
|
||||
(sm/lazy-validator ::cts/image-attrs))
|
||||
(sm/lazy-validator cts/schema:image-attrs))
|
||||
|
||||
(def valid-column-grid-params?
|
||||
(sm/lazy-validator ::ctg/column-params))
|
||||
@@ -1297,7 +1299,7 @@
|
||||
(let [[mtype data] (parse-datauri href)
|
||||
size (alength ^bytes data)
|
||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||
written (io/write-to-file! data path :size size)]
|
||||
written (io/write* path data :size size)]
|
||||
|
||||
(when (not= written size)
|
||||
(ex/raise :type :internal
|
||||
@@ -1380,7 +1382,9 @@
|
||||
(defn get-optimized-svg
|
||||
[sid]
|
||||
(let [svg-text (get-sobject-content sid)
|
||||
svg-text (svgo/optimize *system* svg-text)]
|
||||
svg-text (if (contains? cf/flags :backend-svgo)
|
||||
(svgo/optimize *system* svg-text)
|
||||
svg-text)]
|
||||
(csvg/parse svg-text)))
|
||||
|
||||
(def base-path "/data/cache")
|
||||
@@ -1483,11 +1487,6 @@
|
||||
:file-id (str (:id fdata))
|
||||
:id (str (:id mobj)))
|
||||
|
||||
(instance? org.graalvm.polyglot.PolyglotException cause)
|
||||
(l/inf :hint "skip processing media object: invalid svg found"
|
||||
:file-id (str (:id fdata))
|
||||
:id (str (:id mobj)))
|
||||
|
||||
(= (:type edata) :not-found)
|
||||
(l/inf :hint "skip processing media object: underlying object does not exist"
|
||||
:file-id (str (:id fdata))
|
||||
@@ -1742,12 +1741,12 @@
|
||||
:validate validate?
|
||||
:skip-on-graphic-error skip-on-graphic-error?)
|
||||
|
||||
(db/tx-run! (update system ::sto/storage media/configure-assets-storage)
|
||||
(db/tx-run! system
|
||||
(fn [system]
|
||||
(binding [*system* system]
|
||||
(when (string? label)
|
||||
(fsnap/take-file-snapshot! system {:file-id file-id
|
||||
:label (str "migration/" label)}))
|
||||
(fsnap/create-file-snapshot! system nil file-id (str "migration/" label)))
|
||||
|
||||
(let [file (get-file system file-id)
|
||||
file (process-file! system file :validate? validate?)]
|
||||
|
||||
|
||||
@@ -12,10 +12,19 @@
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.pointer-map :as pmap]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OFFLOAD
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn offloaded?
|
||||
[file]
|
||||
(= "objects-storage" (:data-backend file)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OBJECTS-MAP
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -55,31 +64,45 @@
|
||||
;; POINTER-MAP
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn get-file-data
|
||||
"Get file data given a file instance."
|
||||
[system file]
|
||||
(if (offloaded? file)
|
||||
(let [storage (sto/resolve system ::db/reuse-conn true)]
|
||||
(->> (sto/get-object storage (:data-ref-id file))
|
||||
(sto/get-object-bytes storage)))
|
||||
(:data file)))
|
||||
|
||||
(defn resolve-file-data
|
||||
[system file]
|
||||
(let [data (get-file-data system file)]
|
||||
(assoc file :data data)))
|
||||
|
||||
(defn load-pointer
|
||||
"A database loader pointer helper"
|
||||
[system file-id id]
|
||||
(let [{:keys [content]} (db/get system :file-data-fragment
|
||||
{:id id :file-id file-id}
|
||||
{::sql/columns [:content]
|
||||
::db/check-deleted false})]
|
||||
(let [fragment (db/get* system :file-data-fragment
|
||||
{:id id :file-id file-id}
|
||||
{::sql/columns [:data :data-backend :data-ref-id :id]})]
|
||||
|
||||
(l/trc :hint "load pointer"
|
||||
:file-id (str file-id)
|
||||
:id (str id)
|
||||
:found (some? content))
|
||||
:found (some? fragment))
|
||||
|
||||
(when-not content
|
||||
(when-not fragment
|
||||
(ex/raise :type :internal
|
||||
:code :fragment-not-found
|
||||
:hint "fragment not found"
|
||||
:file-id file-id
|
||||
:fragment-id id))
|
||||
|
||||
(blob/decode content)))
|
||||
(let [data (get-file-data system fragment)]
|
||||
;; FIXME: conditional thread scheduling for decoding big objects
|
||||
(blob/decode data))))
|
||||
|
||||
(defn persist-pointers!
|
||||
"Given a database connection and the final file-id, persist all
|
||||
pointers to the underlying storage (the database)."
|
||||
"Persist all currently tracked pointer objects"
|
||||
[system file-id]
|
||||
(let [conn (db/get-connection system)]
|
||||
(doseq [[id item] @pmap/*tracked*]
|
||||
@@ -89,7 +112,7 @@
|
||||
(db/insert! conn :file-data-fragment
|
||||
{:id id
|
||||
:file-id file-id
|
||||
:content content}))))))
|
||||
:data content}))))))
|
||||
|
||||
(defn process-pointers
|
||||
"Apply a function to all pointers on the file. Usuly used for
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.auth.oidc :as-alias oidc]
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.transit :as t]
|
||||
[app.db :as-alias db]
|
||||
[app.http.access-token :as actoken]
|
||||
@@ -24,14 +25,13 @@
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as-alias rpc.doc]
|
||||
[app.setup :as-alias setup]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[reitit.core :as r]
|
||||
[reitit.middleware :as rr]
|
||||
[ring.request :as rreq]
|
||||
[ring.response :as-alias rres]
|
||||
[yetti.adapter :as yt]))
|
||||
[yetti.adapter :as yt]
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(declare router-handler)
|
||||
|
||||
@@ -39,31 +39,28 @@
|
||||
;; HTTP SERVER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::handler fn?)
|
||||
(s/def ::router some?)
|
||||
(s/def ::port integer?)
|
||||
(s/def ::host string?)
|
||||
(s/def ::name string?)
|
||||
(def default-params
|
||||
{::port 6060
|
||||
::host "0.0.0.0"
|
||||
::max-body-size (* 1024 1024 30) ; default 30 MiB
|
||||
::max-multipart-body-size (* 1024 1024 120)}) ; default 120 MiB
|
||||
|
||||
(s/def ::max-body-size integer?)
|
||||
(s/def ::max-multipart-body-size integer?)
|
||||
(s/def ::io-threads integer?)
|
||||
(defmethod ig/expand-key ::server
|
||||
[k v]
|
||||
{k (merge default-params (d/without-nils v))})
|
||||
|
||||
(defmethod ig/prep-key ::server
|
||||
[_ cfg]
|
||||
(merge {::port 6060
|
||||
::host "0.0.0.0"
|
||||
::max-body-size (* 1024 1024 30) ; default 30 MiB
|
||||
::max-multipart-body-size (* 1024 1024 120)} ; default 120 MiB
|
||||
(d/without-nils cfg)))
|
||||
(def ^:private schema:server-params
|
||||
[:map
|
||||
[::port ::sm/int]
|
||||
[::host ::sm/text]
|
||||
[::max-body-size {:optional true} ::sm/int]
|
||||
[::max-multipart-body-size {:optional true} ::sm/int]
|
||||
[::router {:optional true} [:fn r/router?]]
|
||||
[::handler {:optional true} ::sm/fn]])
|
||||
|
||||
(defmethod ig/pre-init-spec ::server [_]
|
||||
(s/keys :req [::port ::host]
|
||||
:opt [::max-body-size
|
||||
::max-multipart-body-size
|
||||
::router
|
||||
::handler
|
||||
::io-threads]))
|
||||
(defmethod ig/assert-key ::server
|
||||
[_ params]
|
||||
(assert (sm/check schema:server-params params)))
|
||||
|
||||
(defmethod ig/init-key ::server
|
||||
[_ {:keys [::handler ::router ::host ::port] :as cfg}]
|
||||
@@ -100,12 +97,12 @@
|
||||
|
||||
(defn- not-found-handler
|
||||
[_]
|
||||
{::rres/status 404})
|
||||
{::yres/status 404})
|
||||
|
||||
(defn- router-handler
|
||||
[router]
|
||||
(letfn [(resolve-handler [request]
|
||||
(if-let [match (r/match-by-path router (rreq/path request))]
|
||||
(if-let [match (r/match-by-path router (yreq/path request))]
|
||||
(let [params (:path-params match)
|
||||
result (:result match)
|
||||
handler (or (:handler result) not-found-handler)
|
||||
@@ -114,11 +111,11 @@
|
||||
(partial not-found-handler request)))
|
||||
|
||||
(on-error [cause request]
|
||||
(let [{:keys [::rres/body] :as response} (errors/handle cause request)]
|
||||
(let [{:keys [::yres/body] :as response} (errors/handle cause request)]
|
||||
(cond-> response
|
||||
(map? body)
|
||||
(-> (update ::rres/headers assoc "content-type" "application/transit+json")
|
||||
(assoc ::rres/body (t/encode-str body {:type :json-verbose}))))))]
|
||||
(-> (update ::yres/headers assoc "content-type" "application/transit+json")
|
||||
(assoc ::yres/body (t/encode-str body {:type :json-verbose}))))))]
|
||||
|
||||
(fn [request]
|
||||
(let [handler (resolve-handler request)]
|
||||
@@ -131,18 +128,26 @@
|
||||
;; HTTP ROUTER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod ig/pre-init-spec ::router [_]
|
||||
(s/keys :req [::session/manager
|
||||
::ws/routes
|
||||
::rpc/routes
|
||||
::rpc.doc/routes
|
||||
::oidc/routes
|
||||
::setup/props
|
||||
::assets/routes
|
||||
::debug/routes
|
||||
::db/pool
|
||||
::mtx/routes
|
||||
::awsns/routes]))
|
||||
(def ^:private schema:routes
|
||||
[:vector :any])
|
||||
|
||||
(def ^:private schema:router-params
|
||||
[:map
|
||||
[::ws/routes schema:routes]
|
||||
[::rpc/routes schema:routes]
|
||||
[::rpc.doc/routes schema:routes]
|
||||
[::oidc/routes schema:routes]
|
||||
[::assets/routes schema:routes]
|
||||
[::debug/routes schema:routes]
|
||||
[::mtx/routes schema:routes]
|
||||
[::awsns/routes schema:routes]
|
||||
::session/manager
|
||||
::setup/props
|
||||
::db/pool])
|
||||
|
||||
(defmethod ig/assert-key ::router
|
||||
[_ params]
|
||||
(assert (sm/check schema:router-params params)))
|
||||
|
||||
(defmethod ig/init-key ::router
|
||||
[_ cfg]
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[ring.request :as rreq]))
|
||||
[yetti.request :as yreq]))
|
||||
|
||||
(def header-re #"^Token\s+(.*)")
|
||||
|
||||
(defn- get-token
|
||||
[request]
|
||||
(some->> (rreq/get-header request "authorization")
|
||||
(some->> (yreq/get-header request "authorization")
|
||||
(re-matches header-re)
|
||||
(second)))
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[ring.response :as-alias rres]))
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(def ^:private cache-max-age
|
||||
(dt/duration {:hours 24}))
|
||||
@@ -37,8 +35,8 @@
|
||||
(defn- serve-object-from-s3
|
||||
[{:keys [::sto/storage] :as cfg} obj]
|
||||
(let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
|
||||
{::rres/status 307
|
||||
::rres/headers {"location" (str url)
|
||||
{::yres/status 307
|
||||
::yres/headers {"location" (str url)
|
||||
"x-host" (cond-> host port (str ":" port))
|
||||
"x-mtype" (-> obj meta :content-type)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}}))
|
||||
@@ -51,17 +49,16 @@
|
||||
headers {"x-accel-redirect" (:path purl)
|
||||
"content-type" (:content-type mdata)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
|
||||
{::rres/status 204
|
||||
::rres/headers headers}))
|
||||
{::yres/status 204
|
||||
::yres/headers headers}))
|
||||
|
||||
(defn- serve-object
|
||||
"Helper function that returns the appropriate response depending on
|
||||
the storage object backend type."
|
||||
[{:keys [::sto/storage] :as cfg} {:keys [backend] :as obj}]
|
||||
(let [backend (sto/resolve-backend storage backend)]
|
||||
(case (::sto/type backend)
|
||||
:s3 (serve-object-from-s3 cfg obj)
|
||||
:fs (serve-object-from-fs cfg obj))))
|
||||
[cfg {:keys [backend] :as obj}]
|
||||
(case backend
|
||||
(:s3 :assets-s3) (serve-object-from-s3 cfg obj)
|
||||
(:fs :assets-fs) (serve-object-from-fs cfg obj)))
|
||||
|
||||
(defn objects-handler
|
||||
"Handler that servers storage objects by id."
|
||||
@@ -70,7 +67,7 @@
|
||||
obj (sto/get-object storage id)]
|
||||
(if obj
|
||||
(serve-object cfg obj)
|
||||
{::rres/status 404})))
|
||||
{::yres/status 404})))
|
||||
|
||||
(defn- generic-handler
|
||||
"A generic handler helper/common code for file-media based handlers."
|
||||
@@ -81,7 +78,7 @@
|
||||
sobj (sto/get-object storage (kf mobj))]
|
||||
(if sobj
|
||||
(serve-object cfg sobj)
|
||||
{::rres/status 404})))
|
||||
{::yres/status 404})))
|
||||
|
||||
(defn file-objects-handler
|
||||
"Handler that serves storage objects by file media id."
|
||||
@@ -96,11 +93,10 @@
|
||||
|
||||
;; --- Initialization
|
||||
|
||||
(s/def ::path ::us/string)
|
||||
(s/def ::routes vector?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::sto/storage ::path]))
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (sto/valid-storage? (::sto/storage params)) "expected valid storage instance")
|
||||
(assert (string? (::path params))))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.http.client :as http]
|
||||
@@ -18,29 +19,29 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.data.json :as j]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[ring.request :as rreq]
|
||||
[ring.response :as-alias rres]))
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(declare parse-json)
|
||||
(declare handle-request)
|
||||
(declare parse-notification)
|
||||
(declare process-report)
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::http/client
|
||||
::setup/props
|
||||
::db/pool]))
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (http/client? (::http/client params)) "expect a valid http client")
|
||||
(assert (sm/valid? ::setup/props (::setup/props params)) "expected valid setup props")
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
(letfn [(handler [request]
|
||||
(let [data (-> request rreq/body slurp)]
|
||||
(let [data (-> request yreq/body slurp)]
|
||||
(px/run! :vthread (partial handle-request cfg data)))
|
||||
{::rres/status 200})]
|
||||
{::yres/status 200})]
|
||||
["/sns" {:handler handler
|
||||
:allowed-methods #{:post}}]))
|
||||
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
(ns app.http.client
|
||||
"Http client abstraction layer."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]
|
||||
[app.common.schema :as sm]
|
||||
[integrant.core :as ig]
|
||||
[java-http-clj.core :as http]
|
||||
[promesa.core :as p])
|
||||
(:import
|
||||
java.net.http.HttpClient))
|
||||
|
||||
(s/def ::client #(instance? HttpClient %))
|
||||
(s/def ::client-holder
|
||||
(s/keys :req [::client]))
|
||||
(defn client?
|
||||
[o]
|
||||
(instance? HttpClient o))
|
||||
|
||||
(defmethod ig/pre-init-spec ::client [_]
|
||||
(s/keys :req []))
|
||||
(sm/register!
|
||||
{:type ::client
|
||||
:pred client?})
|
||||
|
||||
(defmethod ig/init-key ::client
|
||||
[_ _]
|
||||
@@ -30,7 +30,7 @@
|
||||
(defn send!
|
||||
([client req] (send! client req {}))
|
||||
([client req {:keys [response-type sync?] :or {response-type :string sync? false}}]
|
||||
(us/assert! ::client client)
|
||||
(assert (client? client) "expected valid http client")
|
||||
(if sync?
|
||||
(http/send req {:client client :as response-type})
|
||||
(try
|
||||
@@ -54,9 +54,10 @@
|
||||
"A convencience toplevel function for gradual migration to a new API
|
||||
convention."
|
||||
([cfg-or-client request]
|
||||
(let [client (resolve-client cfg-or-client)]
|
||||
(let [client (resolve-client cfg-or-client)
|
||||
request (update request :uri str)]
|
||||
(send! client request {:sync? true})))
|
||||
([cfg-or-client request options]
|
||||
(let [client (resolve-client cfg-or-client)]
|
||||
(let [client (resolve-client cfg-or-client)
|
||||
request (update request :uri str)]
|
||||
(send! client request (merge {:sync? true} options)))))
|
||||
|
||||
|
||||
@@ -26,15 +26,14 @@
|
||||
[app.util.blob :as blob]
|
||||
[app.util.template :as tmpl]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
[emoji.core :as emj]
|
||||
[integrant.core :as ig]
|
||||
[markdown.core :as md]
|
||||
[markdown.transformers :as mdt]
|
||||
[ring.request :as rreq]
|
||||
[ring.response :as rres]))
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
;; (selmer.parser/cache-off!)
|
||||
|
||||
@@ -44,10 +43,10 @@
|
||||
|
||||
(defn index-handler
|
||||
[_cfg _request]
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/html"}
|
||||
::rres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {}))})
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/html"}
|
||||
::yres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {:version (:full cf/version)}))})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FILE CHANGES
|
||||
@@ -56,17 +55,17 @@
|
||||
(defn prepare-response
|
||||
[body]
|
||||
(let [headers {"content-type" "application/transit+json"}]
|
||||
{::rres/status 200
|
||||
::rres/body body
|
||||
::rres/headers headers}))
|
||||
{::yres/status 200
|
||||
::yres/body body
|
||||
::yres/headers headers}))
|
||||
|
||||
(defn prepare-download-response
|
||||
[body filename]
|
||||
(let [headers {"content-disposition" (str "attachment; filename=" filename)
|
||||
"content-type" "application/octet-stream"}]
|
||||
{::rres/status 200
|
||||
::rres/body body
|
||||
::rres/headers headers}))
|
||||
{::yres/status 200
|
||||
::yres/body body
|
||||
::yres/headers headers}))
|
||||
|
||||
(def sql:retrieve-range-of-changes
|
||||
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn")
|
||||
@@ -108,8 +107,8 @@
|
||||
(db/update! conn :file
|
||||
{:data data}
|
||||
{:id file-id})
|
||||
{::rres/status 201
|
||||
::rres/body "OK CREATED"})))
|
||||
{::yres/status 201
|
||||
::yres/body "OK CREATED"})))
|
||||
|
||||
:else
|
||||
(prepare-response (blob/decode data))))))
|
||||
@@ -123,7 +122,7 @@
|
||||
[{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}]
|
||||
(let [profile (profile/get-profile pool profile-id)
|
||||
project-id (:default-project-id profile)
|
||||
data (some-> params :file :path io/read-as-bytes)]
|
||||
data (some-> params :file :path io/read*)]
|
||||
|
||||
(if (and data project-id)
|
||||
(let [fname (str "Imported file *: " (dt/now))
|
||||
@@ -138,8 +137,8 @@
|
||||
{:data data
|
||||
:deleted-at nil}
|
||||
{:id file-id})
|
||||
{::rres/status 200
|
||||
::rres/body "OK UPDATED"})
|
||||
{::yres/status 200
|
||||
::yres/body "OK UPDATED"})
|
||||
|
||||
(db/run! pool (fn [{:keys [::db/conn] :as cfg}]
|
||||
(create-file cfg {:id file-id
|
||||
@@ -149,15 +148,15 @@
|
||||
(db/update! conn :file
|
||||
{:data data}
|
||||
{:id file-id})
|
||||
{::rres/status 201
|
||||
::rres/body "OK CREATED"}))))
|
||||
{::yres/status 201
|
||||
::yres/body "OK CREATED"}))))
|
||||
|
||||
{::rres/status 500
|
||||
::rres/body "ERROR"})))
|
||||
{::yres/status 500
|
||||
::yres/body "ERROR"})))
|
||||
|
||||
(defn file-data-handler
|
||||
[cfg request]
|
||||
(case (rreq/method request)
|
||||
(case (yreq/method request)
|
||||
:get (retrieve-file-data cfg request)
|
||||
:post (upload-file-data cfg request)
|
||||
(ex/raise :type :http
|
||||
@@ -238,12 +237,12 @@
|
||||
1 (render-template-v1 report)
|
||||
2 (render-template-v2 report)
|
||||
3 (render-template-v3 report))]
|
||||
{::rres/status 200
|
||||
::rres/body result
|
||||
::rres/headers {"content-type" "text/html; charset=utf-8"
|
||||
{::yres/status 200
|
||||
::yres/body result
|
||||
::yres/headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}})
|
||||
{::rres/status 404
|
||||
::rres/body "not found"})))
|
||||
{::yres/status 404
|
||||
::yres/body "not found"})))
|
||||
|
||||
(def sql:error-reports
|
||||
"SELECT id, created_at,
|
||||
@@ -256,10 +255,10 @@
|
||||
[{:keys [::db/pool]} _request]
|
||||
(let [items (->> (db/exec! pool [sql:error-reports])
|
||||
(map #(update % :created-at dt/format-instant :rfc1123)))]
|
||||
{::rres/status 200
|
||||
::rres/body (-> (io/resource "app/templates/error-list.tmpl")
|
||||
{::yres/status 200
|
||||
::yres/body (-> (io/resource "app/templates/error-list.tmpl")
|
||||
(tmpl/render {:items items}))
|
||||
::rres/headers {"content-type" "text/html; charset=utf-8"
|
||||
::yres/headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -295,15 +294,16 @@
|
||||
cfg (assoc cfg
|
||||
::bf.v1/overwrite false
|
||||
::bf.v1/profile-id profile-id
|
||||
::bf.v1/project-id project-id)]
|
||||
(bf.v1/import-files! cfg path)
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body "OK CLONED"})
|
||||
::bf.v1/project-id project-id
|
||||
::bf.v1/input path)]
|
||||
(bf.v1/import-files! cfg)
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK CLONED"})
|
||||
|
||||
{::rres/status 200
|
||||
::rres/body (io/input-stream path)
|
||||
::rres/headers {"content-type" "application/octet-stream"
|
||||
{::yres/status 200
|
||||
::yres/body (io/input-stream path)
|
||||
::yres/headers {"content-type" "application/octet-stream"
|
||||
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}}))))
|
||||
|
||||
|
||||
@@ -329,11 +329,12 @@
|
||||
::bf.v1/overwrite overwrite?
|
||||
::bf.v1/migrate migrate?
|
||||
::bf.v1/profile-id profile-id
|
||||
::bf.v1/project-id project-id)]
|
||||
(bf.v1/import-files! cfg path)
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body "OK"})))
|
||||
::bf.v1/project-id project-id
|
||||
::bf.v1/input path)]
|
||||
(bf.v1/import-files! cfg)
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ACTIONS
|
||||
@@ -363,34 +364,34 @@
|
||||
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! conn :http-session {:profile-id (:id profile)})
|
||||
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))})
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/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))})
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/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"}
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/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))}))
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body (str/ffmt "RESENDED FOR '%'" (: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))}))))))
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))}))))))
|
||||
|
||||
|
||||
(defn- reset-file-version
|
||||
@@ -415,9 +416,9 @@
|
||||
|
||||
(db/tx-run! cfg srepl/process-file! file-id #(assoc % :version version))
|
||||
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/plain"}
|
||||
::rres/body "OK"}))
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/plain"}
|
||||
::yres/body "OK"}))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -429,13 +430,13 @@
|
||||
[{:keys [::db/pool]} _]
|
||||
(try
|
||||
(db/exec-one! pool ["select count(*) as count from server_prop;"])
|
||||
{::rres/status 200
|
||||
::rres/body "OK"}
|
||||
{::yres/status 200
|
||||
::yres/body "OK"}
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unable to execute query on health handler"
|
||||
:cause cause)
|
||||
{::rres/status 503
|
||||
::rres/body "KO"})))
|
||||
{::yres/status 503
|
||||
::yres/body "KO"})))
|
||||
|
||||
(defn changelog-handler
|
||||
[_ _]
|
||||
@@ -444,11 +445,11 @@
|
||||
(md->html [text]
|
||||
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
|
||||
(if-let [clog (io/resource "changelog.md")]
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "text/html; charset=utf-8"}
|
||||
::rres/body (-> clog slurp md->html)}
|
||||
{::rres/status 404
|
||||
::rres/body "NOT FOUND"})))
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/html; charset=utf-8"}
|
||||
::yres/body (-> clog slurp md->html)}
|
||||
{::yres/status 404
|
||||
::yres/body "NOT FOUND"})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INIT
|
||||
@@ -471,8 +472,10 @@
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed)))))})
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::db/pool ::session/manager]))
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool")
|
||||
(assert (session/manager? (::session/manager params)) "expected a valid session manager"))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::db/pool] :as cfg}]
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
[app.http.session :as-alias session]
|
||||
[app.util.inet :as inet]
|
||||
[clojure.spec.alpha :as s]
|
||||
[ring.request :as rreq]
|
||||
[ring.response :as rres]))
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
(defn request->context
|
||||
"Extracts error report relevant context data from request."
|
||||
@@ -29,10 +29,10 @@
|
||||
{:request/path (:path request)
|
||||
:request/method (:method request)
|
||||
:request/params (:params request)
|
||||
:request/user-agent (rreq/get-header request "user-agent")
|
||||
:request/user-agent (yreq/get-header request "user-agent")
|
||||
:request/ip-addr (inet/parse-request request)
|
||||
:request/profile-id (:uid claims)
|
||||
:version/frontend (or (rreq/get-header request "x-frontend-version") "unknown")
|
||||
:version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")
|
||||
:version/backend (:full cf/version)}))
|
||||
|
||||
|
||||
@@ -46,34 +46,34 @@
|
||||
|
||||
(defmethod handle-error :authentication
|
||||
[err _ _]
|
||||
{::rres/status 401
|
||||
::rres/body (ex-data err)})
|
||||
{::yres/status 401
|
||||
::yres/body (ex-data err)})
|
||||
|
||||
(defmethod handle-error :authorization
|
||||
[err _ _]
|
||||
{::rres/status 403
|
||||
::rres/body (ex-data err)})
|
||||
{::yres/status 403
|
||||
::yres/body (ex-data err)})
|
||||
|
||||
(defmethod handle-error :restriction
|
||||
[err _ _]
|
||||
(let [{:keys [code] :as data} (ex-data err)]
|
||||
(if (= code :method-not-allowed)
|
||||
{::rres/status 405
|
||||
::rres/body data}
|
||||
{::rres/status 400
|
||||
::rres/body data})))
|
||||
{::yres/status 405
|
||||
::yres/body data}
|
||||
{::yres/status 400
|
||||
::yres/body data})))
|
||||
|
||||
(defmethod handle-error :rate-limit
|
||||
[err _ _]
|
||||
(let [headers (-> err ex-data ::http/headers)]
|
||||
{::rres/status 429
|
||||
::rres/headers headers}))
|
||||
{::yres/status 429
|
||||
::yres/headers headers}))
|
||||
|
||||
(defmethod handle-error :concurrency-limit
|
||||
[err _ _]
|
||||
(let [headers (-> err ex-data ::http/headers)]
|
||||
{::rres/status 429
|
||||
::rres/headers headers}))
|
||||
{::yres/status 429
|
||||
::yres/headers headers}))
|
||||
|
||||
(defmethod handle-error :validation
|
||||
[err request parent-cause]
|
||||
@@ -84,22 +84,26 @@
|
||||
(= code :schema-validation)
|
||||
(= code :data-validation))
|
||||
(let [explain (ex/explain data)]
|
||||
{::rres/status 400
|
||||
::rres/body (-> data
|
||||
{::yres/status 400
|
||||
::yres/body (-> data
|
||||
(dissoc ::s/problems ::s/value ::s/spec ::sm/explain)
|
||||
(cond-> explain (assoc :explain explain)))})
|
||||
|
||||
(= code :vern-conflict)
|
||||
{::yres/status 409 ;; 409 - Conflict
|
||||
::yres/body data}
|
||||
|
||||
(= code :request-body-too-large)
|
||||
{::rres/status 413 ::rres/body data}
|
||||
{::yres/status 413 ::yres/body data}
|
||||
|
||||
(= code :invalid-image)
|
||||
(binding [l/*context* (request->context request)]
|
||||
(let [cause (or parent-cause err)]
|
||||
(l/warn :hint "unexpected error on processing image" :cause cause)
|
||||
{::rres/status 400 ::rres/body data}))
|
||||
{::yres/status 400 ::yres/body data}))
|
||||
|
||||
:else
|
||||
{::rres/status 400 ::rres/body data})))
|
||||
{::yres/status 400 ::yres/body data})))
|
||||
|
||||
(defmethod handle-error :assertion
|
||||
[error request parent-cause]
|
||||
@@ -110,46 +114,47 @@
|
||||
(= code :data-validation)
|
||||
(let [explain (ex/explain data)]
|
||||
(l/error :hint "data assertion error" :cause cause)
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
:code :assertion
|
||||
:data (-> data
|
||||
(dissoc ::sm/explain)
|
||||
(cond-> explain (assoc :explain explain)))}})
|
||||
{::yres/status 500
|
||||
::yres/body (-> data
|
||||
(dissoc ::sm/explain)
|
||||
(cond-> explain (assoc :explain explain))
|
||||
(assoc :type :server-error)
|
||||
(assoc :code :assertion))})
|
||||
|
||||
(= code :spec-validation)
|
||||
(let [explain (ex/explain data)]
|
||||
(l/error :hint "spec assertion error" :cause cause)
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
:code :assertion
|
||||
:data (-> data
|
||||
(dissoc ::s/problems ::s/value ::s/spec)
|
||||
(cond-> explain (assoc :explain explain)))}})
|
||||
{::yres/status 500
|
||||
::yres/body (-> data
|
||||
(dissoc ::s/problems ::s/value ::s/spec)
|
||||
(cond-> explain (assoc :explain explain))
|
||||
(assoc :type :server-error)
|
||||
(assoc :code :assertion))})
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/error :hint "assertion error" :cause cause)
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
:code :assertion
|
||||
:data data}})))))
|
||||
{::yres/status 500
|
||||
::yres/body (-> data
|
||||
(assoc :type :server-error)
|
||||
(assoc :code :assertion))})))))
|
||||
|
||||
(defmethod handle-error :not-found
|
||||
[err _ _]
|
||||
{::rres/status 404
|
||||
::rres/body (ex-data err)})
|
||||
{::yres/status 404
|
||||
::yres/body (ex-data err)})
|
||||
|
||||
(defmethod handle-error :internal
|
||||
[error request parent-cause]
|
||||
(binding [l/*context* (request->context request)]
|
||||
(let [cause (or parent-cause error)]
|
||||
(let [cause (or parent-cause error)
|
||||
data (ex-data error)]
|
||||
(l/error :hint "internal error" :cause cause)
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
:code :unhandled
|
||||
:hint (ex-message error)
|
||||
:data (ex-data error)}})))
|
||||
{::yres/status 500
|
||||
::yres/body (-> data
|
||||
(assoc :type :server-error)
|
||||
(update :code #(or % :unhandled))
|
||||
(assoc :hint (ex-message error)))})))
|
||||
|
||||
(defmethod handle-error :default
|
||||
[error request parent-cause]
|
||||
@@ -173,20 +178,20 @@
|
||||
:cause cause)
|
||||
(cond
|
||||
(= state "57014")
|
||||
{::rres/status 504
|
||||
::rres/body {:type :server-error
|
||||
{::yres/status 504
|
||||
::yres/body {:type :server-error
|
||||
:code :statement-timeout
|
||||
:hint (ex-message error)}}
|
||||
|
||||
(= state "25P03")
|
||||
{::rres/status 504
|
||||
::rres/body {:type :server-error
|
||||
{::yres/status 504
|
||||
::yres/body {:type :server-error
|
||||
:code :idle-in-transaction-timeout
|
||||
:hint (ex-message error)}}
|
||||
|
||||
:else
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
{::yres/status 500
|
||||
::yres/body {:type :server-error
|
||||
:code :unexpected
|
||||
:hint (ex-message error)
|
||||
:state state}}))))
|
||||
@@ -200,25 +205,25 @@
|
||||
(nil? edata)
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "unexpected error" :cause cause)
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
{::yres/status 500
|
||||
::yres/body {:type :server-error
|
||||
:code :unexpected
|
||||
:hint (ex-message error)}})
|
||||
|
||||
:else
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "unhandled error" :cause cause)
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
:code :unhandled
|
||||
:hint (ex-message error)
|
||||
:data edata}}))))
|
||||
{::yres/status 500
|
||||
::yres/body (-> edata
|
||||
(assoc :type :server-error)
|
||||
(update :code #(or % :unhandled))
|
||||
(assoc :hint (ex-message error)))}))))
|
||||
|
||||
(defmethod handle-exception java.io.IOException
|
||||
[cause _ _]
|
||||
(l/wrn :hint "io exception" :cause cause)
|
||||
{::rres/status 500
|
||||
::rres/body {:type :server-error
|
||||
{::yres/status 500
|
||||
::yres/body {:type :server-error
|
||||
:code :io-exception
|
||||
:hint (ex-message cause)}})
|
||||
|
||||
@@ -244,4 +249,4 @@
|
||||
|
||||
(defn handle'
|
||||
[cause request]
|
||||
(::rres/body (handle cause request)))
|
||||
(::yres/body (handle cause request)))
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
(ns app.http.middleware
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as-alias sm]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.http.errors :as errors]
|
||||
[clojure.data.json :as json]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[cuerdas.core :as str]
|
||||
[ring.request :as rreq]
|
||||
[ring.response :as rres]
|
||||
[yetti.adapter :as yt]
|
||||
[yetti.middleware :as ymw])
|
||||
[yetti.middleware :as ymw]
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as yres])
|
||||
(:import
|
||||
io.undertow.server.RequestTooBigException
|
||||
java.io.InputStream
|
||||
@@ -35,27 +37,17 @@
|
||||
(defn- get-reader
|
||||
^java.io.BufferedReader
|
||||
[request]
|
||||
(let [^InputStream body (rreq/body request)]
|
||||
(let [^InputStream body (yreq/body request)]
|
||||
(java.io.BufferedReader.
|
||||
(java.io.InputStreamReader. body))))
|
||||
|
||||
(defn- read-json-key
|
||||
[k]
|
||||
(-> k str/kebab keyword))
|
||||
|
||||
(defn- write-json-key
|
||||
[k]
|
||||
(if (or (keyword? k) (symbol? k))
|
||||
(str/camel k)
|
||||
(str k)))
|
||||
|
||||
(defn wrap-parse-request
|
||||
[handler]
|
||||
(letfn [(process-request [request]
|
||||
(let [header (rreq/get-header request "content-type")]
|
||||
(let [header (yreq/get-header request "content-type")]
|
||||
(cond
|
||||
(str/starts-with? header "application/transit+json")
|
||||
(with-open [^InputStream is (rreq/body request)]
|
||||
(with-open [^InputStream is (yreq/body request)]
|
||||
(let [params (t/read! (t/reader is))]
|
||||
(-> request
|
||||
(assoc :body-params params)
|
||||
@@ -63,7 +55,7 @@
|
||||
|
||||
(str/starts-with? header "application/json")
|
||||
(with-open [reader (get-reader request)]
|
||||
(let [params (json/read reader :key-fn read-json-key)]
|
||||
(let [params (json/read reader :key-fn json/read-kebab-key)]
|
||||
(-> request
|
||||
(assoc :body-params params)
|
||||
(update :params merge params))))
|
||||
@@ -93,7 +85,7 @@
|
||||
(errors/handle cause request)))]
|
||||
|
||||
(fn [request]
|
||||
(if (= (rreq/method request) :post)
|
||||
(if (= (yreq/method request) :post)
|
||||
(try
|
||||
(-> request process-request handler)
|
||||
(catch Throwable cause
|
||||
@@ -113,58 +105,61 @@
|
||||
|
||||
(def ^:const buffer-size (:xnio/buffer-size yt/defaults))
|
||||
|
||||
(defn- write-json-value
|
||||
[_ val]
|
||||
(if (pmap/pointer-map? val)
|
||||
[(pmap/get-id val) (meta val)]
|
||||
val))
|
||||
|
||||
(defn wrap-format-response
|
||||
[handler]
|
||||
(letfn [(transit-streamable-body [data opts]
|
||||
(reify rres/StreamableResponseBody
|
||||
(-write-body-to-stream [_ _ output-stream]
|
||||
(try
|
||||
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||
(let [tw (t/writer bos opts)]
|
||||
(t/write! tw data)))
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
(letfn [(transit-streamable-body [data opts _ output-stream]
|
||||
(try
|
||||
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||
(let [tw (t/writer bos opts)]
|
||||
(t/write! tw data)))
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))
|
||||
|
||||
(json-streamable-body [data]
|
||||
(reify rres/StreamableResponseBody
|
||||
(-write-body-to-stream [_ _ output-stream]
|
||||
(try
|
||||
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||
(with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)]
|
||||
(json/write data writer :key-fn write-json-key)))
|
||||
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
(json-streamable-body [data _ output-stream]
|
||||
(try
|
||||
(let [encode (or (-> data meta :encode/json) identity)
|
||||
data (encode data)]
|
||||
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
|
||||
(with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)]
|
||||
(json/write writer data :key-fn json/write-camel-key :value-fn write-json-value))))
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))
|
||||
|
||||
(format-response-with-json [response _]
|
||||
(let [body (::rres/body response)]
|
||||
(let [body (::yres/body response)]
|
||||
(if (or (boolean? body) (coll? body))
|
||||
(-> response
|
||||
(update ::rres/headers assoc "content-type" "application/json")
|
||||
(assoc ::rres/body (json-streamable-body body)))
|
||||
(update ::yres/headers assoc "content-type" "application/json")
|
||||
(assoc ::yres/body (yres/stream-body (partial json-streamable-body body))))
|
||||
response)))
|
||||
|
||||
(format-response-with-transit [response request]
|
||||
(let [body (::rres/body response)]
|
||||
(let [body (::yres/body response)]
|
||||
(if (or (boolean? body) (coll? body))
|
||||
(let [qs (rreq/query request)
|
||||
(let [qs (yreq/query request)
|
||||
opts (if (or (contains? cf/flags :transit-readable-response)
|
||||
(str/includes? qs "transit_verbose"))
|
||||
{:type :json-verbose}
|
||||
{:type :json})]
|
||||
(-> response
|
||||
(update ::rres/headers assoc "content-type" "application/transit+json")
|
||||
(assoc ::rres/body (transit-streamable-body body opts))))
|
||||
(update ::yres/headers assoc "content-type" "application/transit+json")
|
||||
(assoc ::yres/body (yres/stream-body (partial transit-streamable-body body opts)))))
|
||||
response)))
|
||||
|
||||
(format-from-params [{:keys [query-params] :as request}]
|
||||
@@ -173,7 +168,7 @@
|
||||
|
||||
(format-response [response request]
|
||||
(let [accept (or (format-from-params request)
|
||||
(rreq/get-header request "accept"))]
|
||||
(yreq/get-header request "accept"))]
|
||||
(cond
|
||||
(or (= accept "application/transit+json")
|
||||
(str/includes? accept "application/transit+json"))
|
||||
@@ -222,11 +217,11 @@
|
||||
(defn wrap-cors
|
||||
[handler]
|
||||
(fn [request]
|
||||
(let [response (if (= (rreq/method request) :options)
|
||||
{::rres/status 200}
|
||||
(let [response (if (= (yreq/method request) :options)
|
||||
{::yres/status 200}
|
||||
(handler request))
|
||||
origin (rreq/get-header request "origin")]
|
||||
(update response ::rres/headers with-cors-headers origin))))
|
||||
origin (yreq/get-header request "origin")]
|
||||
(update response ::yres/headers with-cors-headers origin))))
|
||||
|
||||
(def cors
|
||||
{:name ::cors
|
||||
@@ -241,7 +236,7 @@
|
||||
(when-let [allowed (:allowed-methods data)]
|
||||
(fn [handler]
|
||||
(fn [request]
|
||||
(let [method (rreq/method request)]
|
||||
(let [method (yreq/method request)]
|
||||
(if (contains? allowed method)
|
||||
(handler request)
|
||||
{::rres/status 405}))))))})
|
||||
{::yres/status 405}))))))})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -19,11 +19,9 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[ring.request :as rreq]
|
||||
[yetti.request :as yrq]))
|
||||
[yetti.request :as yreq]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DEFAULTS
|
||||
@@ -52,21 +50,32 @@
|
||||
(update! [_ data])
|
||||
(delete! [_ key]))
|
||||
|
||||
(s/def ::manager #(satisfies? ISessionManager %))
|
||||
(defn manager?
|
||||
[o]
|
||||
(satisfies? ISessionManager o))
|
||||
|
||||
(sm/register!
|
||||
{:type ::manager
|
||||
:pred manager?})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; STORAGE IMPL
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::session-params
|
||||
(s/keys :req-un [::user-agent
|
||||
::profile-id
|
||||
::created-at]))
|
||||
(def ^:private schema:params
|
||||
[:map {:title "session-params"}
|
||||
[:user-agent ::sm/text]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:created-at ::sm/inst]])
|
||||
|
||||
(def ^:private valid-params?
|
||||
(sm/validator schema:params))
|
||||
|
||||
(defn- prepare-session-params
|
||||
[key params]
|
||||
(us/assert! ::us/not-empty-string key)
|
||||
(us/assert! ::session-params params)
|
||||
(assert (string? key) "expected key to be a string")
|
||||
(assert (not (str/blank? key)) "expected key to be not empty")
|
||||
(assert (valid-params? params) "expected valid params")
|
||||
|
||||
{:user-agent (:user-agent params)
|
||||
:profile-id (:profile-id params)
|
||||
@@ -117,8 +126,9 @@
|
||||
(swap! cache dissoc token)
|
||||
nil))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::manager [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
(defmethod ig/assert-key ::manager
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||
|
||||
(defmethod ig/init-key ::manager
|
||||
[_ {:keys [::db/pool]}]
|
||||
@@ -141,11 +151,11 @@
|
||||
|
||||
(defn create-fn
|
||||
[{:keys [::manager ::setup/props]} profile-id]
|
||||
(us/assert! ::manager manager)
|
||||
(us/assert! ::us/uuid profile-id)
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(assert (uuid? profile-id) "expected valid uuid for profile-id")
|
||||
|
||||
(fn [request response]
|
||||
(let [uagent (rreq/get-header request "user-agent")
|
||||
(let [uagent (yreq/get-header request "user-agent")
|
||||
params {:profile-id profile-id
|
||||
:user-agent uagent
|
||||
:created-at (dt/now)}
|
||||
@@ -158,10 +168,10 @@
|
||||
|
||||
(defn delete-fn
|
||||
[{:keys [::manager]}]
|
||||
(us/assert! ::manager manager)
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(fn [request response]
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
cookie (yrq/get-cookie request cname)]
|
||||
cookie (yreq/get-cookie request cname)]
|
||||
(l/trace :hint "delete" :profile-id (:profile-id request))
|
||||
(some->> (:value cookie) (delete! manager))
|
||||
(-> response
|
||||
@@ -183,7 +193,7 @@
|
||||
(defn- get-token
|
||||
[request]
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
cookie (some-> (yrq/get-cookie request cname) :value)]
|
||||
cookie (some-> (yreq/get-cookie request cname) :value)]
|
||||
(when-not (str/empty? cookie)
|
||||
cookie)))
|
||||
|
||||
@@ -199,7 +209,7 @@
|
||||
|
||||
(defn- wrap-soft-auth
|
||||
[handler {:keys [::manager ::setup/props]}]
|
||||
(us/assert! ::manager manager)
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(letfn [(handle-request [request]
|
||||
(try
|
||||
(let [token (get-token request)
|
||||
@@ -217,7 +227,7 @@
|
||||
|
||||
(defn- wrap-authz
|
||||
[handler {:keys [::manager]}]
|
||||
(us/assert! ::manager manager)
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(fn [request]
|
||||
(let [session (get-session manager (::token request))
|
||||
request (cond-> request
|
||||
@@ -308,16 +318,17 @@
|
||||
;; TASK: SESSION GC
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::tasks/max-age ::dt/duration)
|
||||
;; FIXME: MOVE
|
||||
|
||||
(defmethod ig/pre-init-spec ::tasks/gc [_]
|
||||
(s/keys :req [::db/pool]
|
||||
:opt [::tasks/max-age]))
|
||||
(defmethod ig/assert-key ::tasks/gc
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected valid database pool")
|
||||
(assert (dt/duration? (::tasks/max-age params))))
|
||||
|
||||
(defmethod ig/prep-key ::tasks/gc
|
||||
[_ cfg]
|
||||
(defmethod ig/expand-key ::tasks/gc
|
||||
[k v]
|
||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)]
|
||||
(merge {::tasks/max-age max-age} (d/without-nils cfg))))
|
||||
{k (merge {::tasks/max-age max-age} (d/without-nils v))}))
|
||||
|
||||
(def ^:private
|
||||
sql:delete-expired
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:refer-clojure :exclude [tap])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.transit :as t]
|
||||
[app.http.errors :as errors]
|
||||
@@ -16,7 +17,7 @@
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.csp :as sp]
|
||||
[promesa.util :as pu]
|
||||
[ring.response :as rres])
|
||||
[yetti.response :as yres])
|
||||
(:import
|
||||
java.io.OutputStream))
|
||||
|
||||
@@ -49,21 +50,21 @@
|
||||
(defn response
|
||||
[handler & {:keys [buf] :or {buf 32} :as opts}]
|
||||
(fn [request]
|
||||
{::rres/headers default-headers
|
||||
::rres/status 200
|
||||
::rres/body (reify rres/StreamableResponseBody
|
||||
(-write-body-to-stream [_ _ output]
|
||||
(binding [events/*channel* (sp/chan :buf buf :xf (keep encode))]
|
||||
(let [listener (events/start-listener
|
||||
(partial write! output)
|
||||
(partial pu/close! output))]
|
||||
(try
|
||||
(let [result (handler)]
|
||||
(events/tap :end result))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unexpected error on processing sse response"
|
||||
:cause cause)
|
||||
(events/tap :error (errors/handle' cause request)))
|
||||
(finally
|
||||
(sp/close! events/*channel*)
|
||||
(px/await! listener)))))))}))
|
||||
{::yres/headers default-headers
|
||||
::yres/status 200
|
||||
::yres/body (yres/stream-body
|
||||
(fn [_ output]
|
||||
(binding [events/*channel* (sp/chan :buf buf :xf (keep encode))]
|
||||
(let [listener (events/start-listener
|
||||
(partial write! output)
|
||||
(partial pu/close! output))]
|
||||
(try
|
||||
(let [result (handler)]
|
||||
(events/tap :end result))
|
||||
(catch Throwable cause
|
||||
(events/tap :error (errors/handle' cause request))
|
||||
(when-not (ex/instance? java.io.EOFException cause)
|
||||
(l/err :hint "unexpected error on processing sse response" :cause cause)))
|
||||
(finally
|
||||
(sp/close! events/*channel*)
|
||||
(px/await! listener)))))))}))
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
[app.msgbus :as mbus]
|
||||
[app.util.time :as dt]
|
||||
[app.util.websocket :as ws]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec.csp :as sp]
|
||||
[ring.websocket :as rws]
|
||||
[yetti.websocket :as yws]))
|
||||
|
||||
(def recv-labels
|
||||
@@ -113,7 +111,6 @@
|
||||
fsub (::file-subscription @state)
|
||||
tsub (::team-subscription @state)
|
||||
msg {:type :disconnect
|
||||
:subs-id profile-id
|
||||
:profile-id profile-id
|
||||
:session-id session-id}]
|
||||
|
||||
@@ -138,9 +135,7 @@
|
||||
(l/trace :fn "handle-message" :event "subscribe-team" :team-id team-id :conn-id id)
|
||||
(let [prev-subs (get @state ::team-subscription)
|
||||
channel (sp/chan :buf (sp/dropping-buffer 64)
|
||||
:xf (comp
|
||||
(remove #(= (:session-id %) session-id))
|
||||
(map #(assoc % :subs-id team-id))))]
|
||||
:xf (remove #(= (:session-id %) session-id)))]
|
||||
|
||||
(sp/pipe channel output-ch false)
|
||||
(mbus/sub! msgbus :topic team-id :chan channel)
|
||||
@@ -159,8 +154,7 @@
|
||||
(l/trace :fn "handle-message" :event "subscribe-file" :file-id file-id :conn-id id)
|
||||
(let [psub (::file-subscription @state)
|
||||
fch (sp/chan :buf (sp/dropping-buffer 64)
|
||||
:xf (comp (remove #(= (:session-id %) session-id))
|
||||
(map #(assoc % :subs-id file-id))))]
|
||||
:xf (remove #(= (:session-id %) session-id)))]
|
||||
|
||||
(let [subs {:file-id file-id :channel fch :topic file-id}]
|
||||
(swap! state assoc ::file-subscription subs))
|
||||
@@ -191,7 +185,6 @@
|
||||
;; Notifify the rest of participants of the new connection.
|
||||
(let [message {:type :join-file
|
||||
:file-id file-id
|
||||
:subs-id file-id
|
||||
:session-id session-id
|
||||
:profile-id profile-id}]
|
||||
(mbus/pub! msgbus :topic file-id :message message))))
|
||||
@@ -278,18 +271,18 @@
|
||||
:inc 1)
|
||||
message)
|
||||
|
||||
(def ^:private schema:params
|
||||
(sm/define
|
||||
[:map {:title "params"}
|
||||
[:session-id ::sm/uuid]]))
|
||||
|
||||
(defn- http-handler
|
||||
[cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [{:keys [session-id]} (sm/conform! schema:params params)]
|
||||
(let [session-id (some-> params :session-id sm/parse-uuid)]
|
||||
(when-not (uuid? session-id)
|
||||
(ex/raise :type :validation
|
||||
:code :missing-session-id
|
||||
:hint "missing or invalid session-id found"))
|
||||
|
||||
(cond
|
||||
(not profile-id)
|
||||
(ex/raise :type :authentication
|
||||
:hint "Authentication required.")
|
||||
:hint "authentication required")
|
||||
|
||||
;; WORKAROUND: we use the adapter specific predicate for
|
||||
;; performance reasons; for now, the ring default impl for
|
||||
@@ -303,7 +296,7 @@
|
||||
:else
|
||||
(do
|
||||
(l/trace :hint "websocket request" :profile-id profile-id :session-id session-id)
|
||||
{::rws/listener (ws/listener request
|
||||
{::yws/listener (ws/listener request
|
||||
::ws/on-rcv-message (partial on-rcv-message cfg)
|
||||
::ws/on-snd-message (partial on-snd-message cfg)
|
||||
::ws/on-connect (partial on-connect cfg)
|
||||
@@ -311,13 +304,17 @@
|
||||
::profile-id profile-id
|
||||
::session-id session-id)}))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::mbus/msgbus
|
||||
::mtx/metrics
|
||||
::db/pool
|
||||
::session/manager]))
|
||||
|
||||
(s/def ::routes vector?)
|
||||
(def ^:private schema:routes-params
|
||||
[:map
|
||||
::mbus/msgbus
|
||||
::mtx/metrics
|
||||
::db/pool
|
||||
::session/manager])
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (sm/valid? schema:routes-params params)))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -25,9 +25,7 @@
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
@@ -95,46 +93,28 @@
|
||||
;; --- SPECS
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; COLLECTOR
|
||||
;; COLLECTOR API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Defines a service that collects the audit/activity log using
|
||||
;; internal database. Later this audit log can be transferred to
|
||||
;; an external storage and data cleared.
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::type ::us/string)
|
||||
(s/def ::props (s/map-of ::us/keyword any?))
|
||||
(s/def ::ip-addr ::us/string)
|
||||
(def ^:private schema:event
|
||||
[:map {:title "event"}
|
||||
[::type ::sm/text]
|
||||
[::name ::sm/text]
|
||||
[::profile-id ::sm/uuid]
|
||||
[::ip-addr {:optional true} ::sm/text]
|
||||
[::props {:optional true} [:map-of :keyword :any]]
|
||||
[::context {:optional true} [:map-of :keyword :any]]
|
||||
[::webhooks/event? {:optional true} ::sm/boolean]
|
||||
[::webhooks/batch-timeout {:optional true} ::dt/duration]
|
||||
[::webhooks/batch-key {:optional true}
|
||||
[:or ::sm/fn ::sm/text :keyword]]])
|
||||
|
||||
(s/def ::webhooks/event? ::us/boolean)
|
||||
(s/def ::webhooks/batch-timeout ::dt/duration)
|
||||
(s/def ::webhooks/batch-key
|
||||
(s/or :fn fn? :str string? :kw keyword?))
|
||||
|
||||
(s/def ::event
|
||||
(s/keys :req [::type ::name ::profile-id]
|
||||
:opt [::ip-addr
|
||||
::props
|
||||
::webhooks/event?
|
||||
::webhooks/batch-timeout
|
||||
::webhooks/batch-key]))
|
||||
|
||||
(s/def ::collector
|
||||
(s/keys :req [::wrk/executor ::db/pool]))
|
||||
|
||||
(defmethod ig/pre-init-spec ::collector [_]
|
||||
(s/keys :req [::db/pool ::wrk/executor]))
|
||||
|
||||
(defmethod ig/init-key ::collector
|
||||
[_ {:keys [::db/pool] :as cfg}]
|
||||
(cond
|
||||
(db/read-only? pool)
|
||||
(l/warn :hint "audit disabled (db is read-only)")
|
||||
|
||||
:else
|
||||
cfg))
|
||||
(def ^:private check-event
|
||||
(sm/check-fn schema:event))
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
@@ -263,6 +243,8 @@
|
||||
(assoc ::wrk/dedupe dedupe?)
|
||||
(assoc ::wrk/label label)
|
||||
(assoc ::wrk/params (-> params
|
||||
(dissoc :source)
|
||||
(dissoc :context)
|
||||
(dissoc :ip-addr)
|
||||
(dissoc :type)))))))
|
||||
params))
|
||||
@@ -271,12 +253,12 @@
|
||||
"Submit audit event to the collector."
|
||||
[cfg event]
|
||||
(try
|
||||
(let [event (d/without-nils event)
|
||||
(let [event (-> (d/without-nils event)
|
||||
(check-event))
|
||||
cfg (-> cfg
|
||||
(assoc ::rtry/when rtry/conflict-exception?)
|
||||
(assoc ::rtry/max-retries 6)
|
||||
(assoc ::rtry/label "persist-audit-log"))]
|
||||
(us/verify! ::event event)
|
||||
(rtry/invoke! cfg db/tx-run! handle-event! event))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error processing event" :cause cause))))
|
||||
@@ -287,8 +269,8 @@
|
||||
logic."
|
||||
[cfg event]
|
||||
(when (contains? cf/flags :audit-log)
|
||||
(let [event (d/without-nils event)]
|
||||
(us/verify! ::event event)
|
||||
(let [event (-> (d/without-nils event)
|
||||
(check-event))]
|
||||
(db/run! cfg (fn [cfg]
|
||||
(let [tnow (dt/now)
|
||||
params (-> (event->params event)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -16,7 +17,6 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.exec :as px]))
|
||||
@@ -108,8 +108,15 @@
|
||||
(mark-archived! cfg rows)
|
||||
(count events)))))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool ::setup/props ::http/client]))
|
||||
(def ^:private schema:handler-params
|
||||
[:map
|
||||
::db/pool
|
||||
::setup/props
|
||||
::http/client])
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
[_ params]
|
||||
(assert (sm/valid? schema:handler-params params) "valid params expected for handler"))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private sql:clean-archived
|
||||
@@ -22,8 +21,9 @@
|
||||
(l/debug :hint "delete archived audit log entries" :deleted result)
|
||||
result))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
(defmethod ig/assert-key ::handler
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "valid database pool expected"))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -38,7 +37,7 @@
|
||||
|
||||
(defn record->report
|
||||
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
|
||||
(us/assert! ::l/record record)
|
||||
(assert (l/valid-record? record) "expectd valid log record")
|
||||
(if (or (instance? java.util.concurrent.CompletionException cause)
|
||||
(instance? java.util.concurrent.ExecutionException cause))
|
||||
(-> record
|
||||
@@ -63,7 +62,7 @@
|
||||
(ex/format-throwable cause :data? false :explain? false :header? false :summary? false))}
|
||||
|
||||
(when-let [params (or (:request/params context) (:params context))]
|
||||
{:params (pp/pprint-str params :length 30 :level 12)})
|
||||
{:params (pp/pprint-str params :length 30 :level 13)})
|
||||
|
||||
(when-let [value (:value context)]
|
||||
{:value (pp/pprint-str value :length 30 :level 12)})
|
||||
@@ -91,8 +90,9 @@
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
(defmethod ig/assert-key ::reporter
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ cfg]
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.database :as ldb]
|
||||
[app.util.json :as json]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.csp :as sp]))
|
||||
@@ -54,7 +52,7 @@
|
||||
|
||||
(defn record->report
|
||||
[{:keys [::l/context ::l/id ::l/cause] :as record}]
|
||||
(us/assert! ::l/record record)
|
||||
(assert (l/valid-record? record) "expectd valid log record")
|
||||
{:id id
|
||||
:tenant (cf/get :tenant)
|
||||
:host (cf/get :host)
|
||||
@@ -75,8 +73,9 @@
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unhandled error" :cause cause)))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req [::http/client]))
|
||||
(defmethod ig/assert-key ::reporter
|
||||
[_ params]
|
||||
(assert (http/client? (::http/client params)) "expect valid http client"))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ cfg]
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -60,27 +59,26 @@
|
||||
(some->> (:project-id props) (lookup-webhooks-by-project pool))
|
||||
(some->> (:file-id props) (lookup-webhooks-by-file pool))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::process-event-handler [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
(defmethod ig/assert-key ::process-event-handler
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
||||
(assert (http/client? (::http/client params)) "expect valid http client"))
|
||||
|
||||
(defmethod ig/init-key ::process-event-handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [event (:event props)]
|
||||
(l/dbg :hint "process webhook event" :name (:name event))
|
||||
|
||||
(when-let [items (lookup-webhooks cfg event)]
|
||||
(l/trc :hint "webhooks found for event" :total (count items))
|
||||
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(doseq [item items]
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :run-webhook)
|
||||
(assoc ::wrk/queue :webhooks)
|
||||
(assoc ::wrk/max-retries 3)
|
||||
(assoc ::wrk/params {:event event
|
||||
:config item}))))))))))
|
||||
(l/dbg :hint "process webhook event" :name (:name props))
|
||||
|
||||
(when-let [items (lookup-webhooks cfg props)]
|
||||
(l/trc :hint "webhooks found for event" :total (count items))
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(doseq [item items]
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :run-webhook)
|
||||
(assoc ::wrk/queue :webhooks)
|
||||
(assoc ::wrk/max-retries 3)
|
||||
(assoc ::wrk/params {:event props
|
||||
:config item})))))))))
|
||||
;; --- RUN
|
||||
|
||||
(declare interpret-exception)
|
||||
@@ -90,12 +88,14 @@
|
||||
{:key-fn str/camel
|
||||
:indent true})
|
||||
|
||||
(defmethod ig/pre-init-spec ::run-webhook-handler [_]
|
||||
(s/keys :req [::http/client ::db/pool]))
|
||||
(defmethod ig/assert-key ::run-webhook-handler
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
||||
(assert (http/client? (::http/client params)) "expect valid http client"))
|
||||
|
||||
(defmethod ig/prep-key ::run-webhook-handler
|
||||
[_ cfg]
|
||||
(merge {::max-errors 3} (d/without-nils cfg)))
|
||||
(defmethod ig/expand-key ::run-webhook-handler
|
||||
[k v]
|
||||
{k (merge {::max-errors 3} (d/without-nils v))})
|
||||
|
||||
(defmethod ig/init-key ::run-webhook-handler
|
||||
[_ {:keys [::db/pool ::max-errors] :as cfg}]
|
||||
@@ -138,7 +138,7 @@
|
||||
|
||||
(l/dbg :hint "run webhook"
|
||||
:event-name (:name event)
|
||||
:webhook-id (:id whook)
|
||||
:webhook-id (str (:id whook))
|
||||
:webhook-uri (:uri whook)
|
||||
:webhook-mtype (:mtype whook))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.auth.ldap :as-alias ldap]
|
||||
[app.auth.oidc :as-alias oidc]
|
||||
[app.auth.oidc.providers :as-alias oidc.providers]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
@@ -28,6 +29,7 @@
|
||||
[app.msgbus :as-alias mbus]
|
||||
[app.redis :as-alias rds]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.doc :as-alias rpc.doc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.srepl :as-alias srepl]
|
||||
@@ -169,7 +171,7 @@
|
||||
{::db/uri (cf/get :database-uri)
|
||||
::db/username (cf/get :database-username)
|
||||
::db/password (cf/get :database-password)
|
||||
::db/read-only? (cf/get :database-readonly false)
|
||||
::db/read-only (cf/get :database-readonly false)
|
||||
::db/min-size (cf/get :database-min-pool-size 0)
|
||||
::db/max-size (cf/get :database-max-pool-size 60)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
@@ -245,7 +247,7 @@
|
||||
:base-dn (cf/get :ldap-base-dn)
|
||||
:bind-dn (cf/get :ldap-bind-dn)
|
||||
:bind-password (cf/get :ldap-bind-password)
|
||||
:enabled? (contains? cf/flags :login-with-ldap)}
|
||||
:enabled (contains? cf/flags :login-with-ldap)}
|
||||
|
||||
::oidc.providers/google
|
||||
{}
|
||||
@@ -302,9 +304,11 @@
|
||||
::http.assets/cache-max-agesignature-max-age (dt/duration {:hours 24 :minutes 5})
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
:app.rpc/climit
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
::rpc/climit
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::wrk/executor (ig/ref ::wrk/executor)
|
||||
::climit/config (cf/get :rpc-climit-config)
|
||||
::climit/enabled (contains? cf/flags :rpc-climit)}
|
||||
|
||||
:app.rpc/rlimit
|
||||
{::wrk/executor (ig/ref ::wrk/executor)}
|
||||
@@ -319,7 +323,6 @@
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||
::rds/redis (ig/ref ::rds/redis)
|
||||
::svgo/optimizer (ig/ref ::svgo/optimizer)
|
||||
|
||||
::rpc/climit (ig/ref ::rpc/climit)
|
||||
::rpc/rlimit (ig/ref ::rpc/rlimit)
|
||||
@@ -330,7 +333,7 @@
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
:app.rpc.doc/routes
|
||||
{:methods (ig/ref :app.rpc/methods)}
|
||||
{:app.rpc/methods (ig/ref :app.rpc/methods)}
|
||||
|
||||
:app.rpc/routes
|
||||
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||
@@ -344,6 +347,8 @@
|
||||
{:sendmail (ig/ref ::email/handler)
|
||||
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
|
||||
:file-gc (ig/ref :app.tasks.file-gc/handler)
|
||||
:file-gc-scheduler (ig/ref :app.tasks.file-gc-scheduler/handler)
|
||||
:offload-file-data (ig/ref :app.tasks.offload-file-data/handler)
|
||||
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
|
||||
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
||||
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
||||
@@ -377,8 +382,7 @@
|
||||
::email/default-from (cf/get :smtp-default-from)}
|
||||
|
||||
::email/handler
|
||||
{::email/sendmail (ig/ref ::email/sendmail)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||
{::email/sendmail (ig/ref ::email/sendmail)}
|
||||
|
||||
:app.tasks.tasks-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
@@ -394,9 +398,17 @@
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
:app.tasks.file-xlog-gc/handler
|
||||
:app.tasks.file-gc-scheduler/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.tasks.offload-file-data/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
:app.tasks.file-xlog-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
:app.tasks.telemetry/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::http.client/client (ig/ref ::http.client/client)
|
||||
@@ -420,9 +432,6 @@
|
||||
;; module requires the migrations to run before initialize.
|
||||
::migrations (ig/ref :app.migrations/migrations)}
|
||||
|
||||
::svgo/optimizer
|
||||
{}
|
||||
|
||||
:app.loggers.audit.archive-task/handler
|
||||
{::setup/props (ig/ref ::setup/props)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
@@ -448,17 +457,29 @@
|
||||
::sto/storage
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::sto/backends
|
||||
{:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
|
||||
:assets-fs (ig/ref [::assets :app.storage.fs/backend])}}
|
||||
{:s3 (ig/ref :app.storage.s3/backend)
|
||||
:fs (ig/ref :app.storage.fs/backend)
|
||||
|
||||
[::assets :app.storage.s3/backend]
|
||||
{::sto.s3/region (cf/get :storage-assets-s3-region)
|
||||
::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint)
|
||||
::sto.s3/bucket (cf/get :storage-assets-s3-bucket)
|
||||
::sto.s3/io-threads (cf/get :storage-assets-s3-io-threads)}
|
||||
;; LEGACY (should not be removed, can only be removed after an
|
||||
;; explicit migration because the database objects/rows will
|
||||
;; still reference the old names).
|
||||
:assets-s3 (ig/ref :app.storage.s3/backend)
|
||||
:assets-fs (ig/ref :app.storage.fs/backend)}}
|
||||
|
||||
[::assets :app.storage.fs/backend]
|
||||
{::sto.fs/directory (cf/get :storage-assets-fs-directory)}})
|
||||
:app.storage.s3/backend
|
||||
{::sto.s3/region (or (cf/get :storage-assets-s3-region)
|
||||
(cf/get :objects-storage-s3-region))
|
||||
::sto.s3/endpoint (or (cf/get :storage-assets-s3-endpoint)
|
||||
(cf/get :objects-storage-s3-endpoint))
|
||||
::sto.s3/bucket (or (cf/get :storage-assets-s3-bucket)
|
||||
(cf/get :objects-storage-s3-bucket))
|
||||
::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads)
|
||||
(cf/get :objects-storage-s3-io-threads))
|
||||
::wrk/executor (ig/ref ::wrk/executor)}
|
||||
|
||||
:app.storage.fs/backend
|
||||
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
|
||||
(cf/get :objects-storage-fs-directory))}})
|
||||
|
||||
|
||||
(def worker-config
|
||||
@@ -466,10 +487,7 @@
|
||||
{::wrk/registry (ig/ref ::wrk/registry)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/entries
|
||||
[{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||
:task :file-xlog-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
[{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :session-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
@@ -485,7 +503,7 @@
|
||||
:task :tasks-gc}
|
||||
|
||||
{:cron #app/cron "0 0 2 * * ?" ;; daily
|
||||
:task :file-gc}
|
||||
:task :file-gc-scheduler}
|
||||
|
||||
{:cron #app/cron "0 30 */3,23 * * ?"
|
||||
:task :telemetry}
|
||||
@@ -501,11 +519,13 @@
|
||||
::wrk/dispatcher
|
||||
{::rds/redis (ig/ref ::rds/redis)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::db/pool (ig/ref ::db/pool)}
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::wrk/tenant (cf/get :tenant)}
|
||||
|
||||
[::default ::wrk/runner]
|
||||
{::wrk/parallelism (cf/get ::worker-default-parallelism 1)
|
||||
::wrk/queue :default
|
||||
::wrk/tenant (cf/get :tenant)
|
||||
::rds/redis (ig/ref ::rds/redis)
|
||||
::wrk/registry (ig/ref ::wrk/registry)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
@@ -514,6 +534,7 @@
|
||||
[::webhook ::wrk/runner]
|
||||
{::wrk/parallelism (cf/get ::worker-webhook-parallelism 1)
|
||||
::wrk/queue :webhooks
|
||||
::wrk/tenant (cf/get :tenant)
|
||||
::rds/redis (ig/ref ::rds/redis)
|
||||
::wrk/registry (ig/ref ::wrk/registry)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
@@ -524,13 +545,14 @@
|
||||
|
||||
(defn start
|
||||
[]
|
||||
(cf/validate!)
|
||||
(ig/load-namespaces (merge system-config worker-config))
|
||||
(alter-var-root #'system (fn [sys]
|
||||
(when sys (ig/halt! sys))
|
||||
(-> system-config
|
||||
(cond-> (contains? cf/flags :backend-worker)
|
||||
(merge worker-config))
|
||||
(ig/prep)
|
||||
(ig/expand)
|
||||
(ig/init))))
|
||||
(l/inf :hint "welcome to penpot"
|
||||
:flags (str/join "," (map name cf/flags))
|
||||
@@ -543,7 +565,7 @@
|
||||
(alter-var-root #'system (fn [sys]
|
||||
(when sys (ig/halt! sys))
|
||||
(-> config
|
||||
(ig/prep)
|
||||
(ig/expand)
|
||||
(ig/init)))))
|
||||
|
||||
(defn stop
|
||||
@@ -599,12 +621,6 @@
|
||||
|
||||
(deref p))
|
||||
(catch Throwable cause
|
||||
(binding [*out* *err*]
|
||||
(println "==== ERROR ===="))
|
||||
(.printStackTrace cause)
|
||||
(when-let [cause' (ex-cause cause)]
|
||||
(binding [*out* *err*]
|
||||
(println "==== CAUSE ===="))
|
||||
(.printStackTrace cause'))
|
||||
(ex/print-throwable cause)
|
||||
(px/sleep 500)
|
||||
(System/exit -1))))
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
[app.common.spec :as us]
|
||||
[app.common.svg :as csvg]
|
||||
@@ -47,26 +46,15 @@
|
||||
(s/keys :req-un [::path]
|
||||
:opt-un [::mtype]))
|
||||
|
||||
(sm/def! ::fs/path
|
||||
{:type ::fs/path
|
||||
:pred fs/path?
|
||||
:type-properties
|
||||
{:title "path"
|
||||
:description "filesystem path"
|
||||
:error/message "expected a valid fs path instance"
|
||||
:gen/gen (sg/generator :string)
|
||||
::oapi/type "string"
|
||||
::oapi/format "unix-path"
|
||||
::oapi/decode fs/path}})
|
||||
|
||||
(sm/def! ::upload
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size :int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]])
|
||||
(sm/register!
|
||||
^{::sm/type ::upload}
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]])
|
||||
|
||||
(defn validate-media-type!
|
||||
([upload] (validate-media-type! upload cm/valid-image-types))
|
||||
@@ -238,7 +226,7 @@
|
||||
(letfn [(ttf->otf [data]
|
||||
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix "")
|
||||
foutput (fs/path (str finput ".otf"))
|
||||
_ (io/write-to-file! data finput)
|
||||
_ (io/write* finput data)
|
||||
res (sh/sh "fontforge" "-lang=ff" "-c"
|
||||
(str/fmt "Open('%s'); Generate('%s')"
|
||||
(str finput)
|
||||
@@ -249,7 +237,7 @@
|
||||
(otf->ttf [data]
|
||||
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix "")
|
||||
foutput (fs/path (str finput ".ttf"))
|
||||
_ (io/write-to-file! data finput)
|
||||
_ (io/write* finput data)
|
||||
res (sh/sh "fontforge" "-lang=ff" "-c"
|
||||
(str/fmt "Open('%s'); Generate('%s')"
|
||||
(str finput)
|
||||
@@ -263,14 +251,14 @@
|
||||
;; command.
|
||||
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix "")
|
||||
foutput (fs/path (str finput ".woff"))
|
||||
_ (io/write-to-file! data finput)
|
||||
_ (io/write* finput data)
|
||||
res (sh/sh "sfnt2woff" (str finput))]
|
||||
(when (zero? (:exit res))
|
||||
foutput)))
|
||||
|
||||
(woff->sfnt [data]
|
||||
(let [finput (tmp/tempfile :prefix "penpot" :suffix "")
|
||||
_ (io/write-to-file! data finput)
|
||||
_ (io/write* finput data)
|
||||
res (sh/sh "woff2sfnt" (str finput)
|
||||
:out-enc :bytes)]
|
||||
(when (zero? (:exit res))
|
||||
@@ -326,17 +314,3 @@
|
||||
(= stype :ttf)
|
||||
(-> (assoc "font/otf" (ttf->otf sfnt))
|
||||
(assoc "font/ttf" sfnt)))))))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Utility functions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn configure-assets-storage
|
||||
"Given storage map, returns a storage configured with the appropriate
|
||||
backend for assets and optional connection attached."
|
||||
([storage]
|
||||
(assoc storage ::sto/backend (cf/get :assets-storage-backend :assets-fs)))
|
||||
([storage pool-or-conn]
|
||||
(-> (configure-assets-storage storage)
|
||||
(assoc ::db/pool-or-conn pool-or-conn))))
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
(:refer-clojure :exclude [run!])
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.metrics.definition :as-alias mdef]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
io.prometheus.client.CollectorRegistry
|
||||
@@ -34,41 +33,52 @@
|
||||
(declare create-collector)
|
||||
(declare handler)
|
||||
|
||||
(defprotocol IMetrics
|
||||
(get-registry [_])
|
||||
(get-collector [_ id])
|
||||
(get-handler [_]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; METRICS SERVICE PROVIDER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::mdef/name string?)
|
||||
(s/def ::mdef/help string?)
|
||||
(s/def ::mdef/labels (s/every string? :kind vector?))
|
||||
(s/def ::mdef/type #{:gauge :counter :summary :histogram})
|
||||
(sm/register!
|
||||
{:type ::collector
|
||||
:pred #(instance? SimpleCollector %)
|
||||
:type-properties
|
||||
{:title "collector"
|
||||
:description "An instance of SimpleCollector"}})
|
||||
|
||||
(s/def ::mdef/instance
|
||||
#(instance? SimpleCollector %))
|
||||
(sm/register!
|
||||
{:type ::registry
|
||||
:pred #(instance? CollectorRegistry %)
|
||||
:type-properties
|
||||
{:title "Metrics Registry"
|
||||
:description "Instance of CollectorRegistry"}})
|
||||
|
||||
(s/def ::mdef/definition
|
||||
(s/keys :req [::mdef/name
|
||||
::mdef/help
|
||||
::mdef/type]
|
||||
:opt [::mdef/labels
|
||||
::mdef/instance]))
|
||||
(def ^:private schema:definitions
|
||||
[:map-of :keyword
|
||||
[:map {:title "definition"}
|
||||
[::mdef/name :string]
|
||||
[::mdef/help :string]
|
||||
[::mdef/type [:enum :gauge :counter :summary :histogram]]
|
||||
[::mdef/labels {:optional true} [::sm/vec :string]]
|
||||
[::mdef/instance {:optional true} ::collector]]])
|
||||
|
||||
(s/def ::definitions
|
||||
(s/map-of keyword? ::mdef/definition))
|
||||
(defn metrics?
|
||||
[o]
|
||||
(satisfies? IMetrics o))
|
||||
|
||||
(s/def ::registry
|
||||
#(instance? CollectorRegistry %))
|
||||
(sm/register!
|
||||
{:type ::metrics
|
||||
:pred metrics?})
|
||||
|
||||
(s/def ::handler fn?)
|
||||
(s/def ::metrics
|
||||
(s/keys :req [::registry
|
||||
::handler
|
||||
::definitions]))
|
||||
(def ^:private valid-definitions?
|
||||
(sm/validator schema:definitions))
|
||||
|
||||
(s/def ::default ::definitions)
|
||||
|
||||
(defmethod ig/pre-init-spec ::metrics [_]
|
||||
(s/keys :req-un [::default]))
|
||||
(defmethod ig/assert-key ::metrics
|
||||
[_ {:keys [default]}]
|
||||
(assert (valid-definitions? default) "expected valid definitions"))
|
||||
|
||||
(defmethod ig/init-key ::metrics
|
||||
[_ cfg]
|
||||
@@ -81,12 +91,14 @@
|
||||
{}
|
||||
(:default cfg))]
|
||||
|
||||
(us/verify! ::definitions definitions)
|
||||
|
||||
{::handler (partial handler registry)
|
||||
::definitions definitions
|
||||
::registry registry}))
|
||||
|
||||
(reify
|
||||
IMetrics
|
||||
(get-handler [_]
|
||||
(partial handler registry))
|
||||
(get-collector [_ id]
|
||||
(get definitions id))
|
||||
(get-registry [_]
|
||||
registry))))
|
||||
|
||||
(defn- handler
|
||||
[registry _]
|
||||
@@ -96,17 +108,14 @@
|
||||
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
|
||||
:body (.toString writer)}))
|
||||
|
||||
|
||||
|
||||
(s/def ::routes vector?)
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::metrics]))
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ {:keys [::metrics]}]
|
||||
(assert (metrics? metrics) "expected a valid instance for metrics"))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::metrics]}]
|
||||
(let [registry (::registry metrics)]
|
||||
["/metrics" {:handler (partial handler registry)
|
||||
:allowed-methods #{:get}}]))
|
||||
["/metrics" {:handler (get-handler metrics)
|
||||
:allowed-methods #{:get}}])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Implementation
|
||||
@@ -126,8 +135,9 @@
|
||||
(defmulti create-collector ::mdef/type)
|
||||
|
||||
(defn run!
|
||||
[{:keys [::definitions]} & {:keys [id] :as params}]
|
||||
(when-let [mobj (get definitions id)]
|
||||
[instance & {:keys [id] :as params}]
|
||||
(assert (metrics? instance) "expected valid metrics instance")
|
||||
(when-let [mobj (get-collector instance id)]
|
||||
(run-collector! mobj params)
|
||||
true))
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.db :as db]
|
||||
[app.migrations.clj.migration-0023 :as mg0023]
|
||||
[app.util.migrations :as mg]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def migrations
|
||||
@@ -379,7 +378,55 @@
|
||||
:fn (mg/resource "app/migrations/sql/0119-mod-file-table.sql")}
|
||||
|
||||
{:name "0120-mod-audit-log-table"
|
||||
:fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")}
|
||||
|
||||
{:name "0121-mod-file-data-fragment-table"
|
||||
:fn (mg/resource "app/migrations/sql/0121-mod-file-data-fragment-table.sql")}
|
||||
|
||||
{:name "0122-mod-file-table"
|
||||
:fn (mg/resource "app/migrations/sql/0122-mod-file-table.sql")}
|
||||
|
||||
{:name "0122-mod-file-data-fragment-table"
|
||||
:fn (mg/resource "app/migrations/sql/0122-mod-file-data-fragment-table.sql")}
|
||||
|
||||
{:name "0123-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0123-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0124-mod-profile-table"
|
||||
:fn (mg/resource "app/migrations/sql/0124-mod-profile-table.sql")}
|
||||
|
||||
{:name "0125-mod-file-table"
|
||||
:fn (mg/resource "app/migrations/sql/0125-mod-file-table.sql")}
|
||||
|
||||
{:name "0126-add-team-access-request-table"
|
||||
:fn (mg/resource "app/migrations/sql/0126-add-team-access-request-table.sql")}
|
||||
|
||||
{:name "0127-mod-storage-object-table"
|
||||
:fn (mg/resource "app/migrations/sql/0127-mod-storage-object-table.sql")}
|
||||
|
||||
{:name "0128-mod-task-table"
|
||||
:fn (mg/resource "app/migrations/sql/0128-mod-task-table.sql")}
|
||||
|
||||
{:name "0129-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0130-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0130-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0131-mod-webhook-table"
|
||||
:fn (mg/resource "app/migrations/sql/0131-mod-webhook-table.sql")}
|
||||
|
||||
{:name "0132-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0132-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0133-mod-file-table"
|
||||
:fn (mg/resource "app/migrations/sql/0133-mod-file-table.sql")}
|
||||
|
||||
{:name "0134-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0134-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0135-mod-team-invitation-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0135-mod-team-invitation-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
@@ -387,9 +434,9 @@
|
||||
(mg/setup! conn)
|
||||
(mg/migrate! conn {:name name :steps migrations})))
|
||||
|
||||
(defmethod ig/pre-init-spec ::migrations
|
||||
[_]
|
||||
(s/keys :req [::db/pool]))
|
||||
(defmethod ig/assert-key ::migrations
|
||||
[_ {:keys [::db/pool]}]
|
||||
(assert (db/pool? pool) "expected valid pool"))
|
||||
|
||||
(defmethod ig/init-key ::migrations
|
||||
[module {:keys [::db/pool]}]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE file_data_fragment
|
||||
ADD COLUMN data bytea NULL;
|
||||
|
||||
UPDATE file_data_fragment
|
||||
SET data = content;
|
||||
|
||||
ALTER TABLE file_data_fragment
|
||||
DROP COLUMN content;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE file_data_fragment
|
||||
ADD COLUMN data_backend text NULL,
|
||||
ADD COLUMN data_ref_id uuid NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS file_data_fragment__data_ref_id__idx
|
||||
ON file_data_fragment (data_ref_id);
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE file_data_fragment
|
||||
ADD COLUMN data_backend text NULL,
|
||||
ADD COLUMN data_ref_id uuid NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS file_data_fragment__data_ref_id__idx
|
||||
ON file_data_fragment (data_ref_id);
|
||||
4
backend/src/app/migrations/sql/0122-mod-file-table.sql
Normal file
4
backend/src/app/migrations/sql/0122-mod-file-table.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE file ADD COLUMN data_ref_id uuid NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS file__data_ref_id__idx
|
||||
ON file (data_ref_id);
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX IF NOT EXISTS file_change__created_at__label__idx
|
||||
ON file_change (created_at, label);
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX profile__props__newsletter1__idx ON profile (email) WHERE props->>'~:newsletter-news' = 'true';
|
||||
CREATE INDEX profile__props__newsletter2__idx ON profile (email) WHERE props->>'~:newsletter-updates' = 'true';
|
||||
3
backend/src/app/migrations/sql/0125-mod-file-table.sql
Normal file
3
backend/src/app/migrations/sql/0125-mod-file-table.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
--- This setting allow to optimize the table for heavy write workload
|
||||
--- leaving space on the page for HOT updates
|
||||
ALTER TABLE file SET (FILLFACTOR=50);
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE team_access_request (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE,
|
||||
requester_id uuid NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE,
|
||||
valid_until timestamptz NOT NULL,
|
||||
auto_join_until timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (team_id, requester_id)
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
--- This setting allow to optimize the table for heavy write workload
|
||||
--- leaving space on the page for HOT updates
|
||||
ALTER TABLE storage_object SET (FILLFACTOR=60);
|
||||
3
backend/src/app/migrations/sql/0128-mod-task-table.sql
Normal file
3
backend/src/app/migrations/sql/0128-mod-task-table.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
--- This setting allow to optimize the table for heavy write workload
|
||||
--- leaving space on the page for HOT updates
|
||||
ALTER TABLE task SET (FILLFACTOR=60);
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN data_backend text NULL,
|
||||
ADD COLUMN data_ref_id uuid NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS file_change__data_ref_id__idx
|
||||
ON file_change (data_ref_id);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN version integer NULL;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE webhook
|
||||
ADD COLUMN profile_id uuid NULL REFERENCES profile (id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX webhook__profile_id__idx
|
||||
ON webhook (profile_id)
|
||||
WHERE profile_id IS NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN created_by text NOT NULL DEFAULT 'system';
|
||||
2
backend/src/app/migrations/sql/0133-mod-file-table.sql
Normal file
2
backend/src/app/migrations/sql/0133-mod-file-table.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file
|
||||
ADD COLUMN vern int NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,18 @@
|
||||
ALTER TABLE file_change
|
||||
ADD COLUMN updated_at timestamptz DEFAULT now(),
|
||||
ADD COLUMN deleted_at timestamptz DEFAULT NULL,
|
||||
ALTER COLUMN created_at SET DEFAULT now();
|
||||
|
||||
DROP INDEX file_change__created_at__idx;
|
||||
DROP INDEX file_change__created_at__label__idx;
|
||||
DROP INDEX file_change__label__idx;
|
||||
|
||||
CREATE INDEX file_change__deleted_at__idx
|
||||
ON file_change (deleted_at, id)
|
||||
WHERE deleted_at IS NOT NULL;
|
||||
|
||||
CREATE INDEX file_change__system_snapshots__idx
|
||||
ON file_change (file_id, created_at)
|
||||
WHERE data IS NOT NULL
|
||||
AND created_by = 'system'
|
||||
AND deleted_at IS NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE team_invitation
|
||||
ADD COLUMN created_by uuid NULL REFERENCES profile(id) ON DELETE SET NULL;
|
||||
@@ -9,22 +9,27 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cfg]
|
||||
[app.redis :as rds]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[promesa.exec.csp :as sp]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(def ^:private prefix (cfg/get :tenant))
|
||||
|
||||
(defprotocol IMsgBus
|
||||
(-sub [_ topics chan])
|
||||
(-pub [_ topic message])
|
||||
(-purge [_ chans]))
|
||||
|
||||
|
||||
|
||||
(defn- prefix-topic
|
||||
[topic]
|
||||
(str prefix "." topic))
|
||||
@@ -32,30 +37,33 @@
|
||||
(def ^:private xform-prefix-topic
|
||||
(map (fn [obj] (update obj :topic prefix-topic))))
|
||||
|
||||
(declare ^:private redis-pub!)
|
||||
(declare ^:private redis-sub!)
|
||||
(declare ^:private redis-unsub!)
|
||||
(declare ^:private start-io-loop!)
|
||||
(declare ^:private redis-pub)
|
||||
(declare ^:private redis-sub)
|
||||
(declare ^:private redis-unsub)
|
||||
(declare ^:private start-io-loop)
|
||||
(declare ^:private subscribe-to-topics)
|
||||
(declare ^:private unsubscribe-channels)
|
||||
|
||||
(s/def ::cmd-ch sp/chan?)
|
||||
(s/def ::rcv-ch sp/chan?)
|
||||
(s/def ::pub-ch sp/chan?)
|
||||
(s/def ::state ::us/agent)
|
||||
(s/def ::pconn ::rds/connection-holder)
|
||||
(s/def ::sconn ::rds/connection-holder)
|
||||
(s/def ::msgbus
|
||||
(s/keys :req [::cmd-ch ::rcv-ch ::pub-ch ::state ::pconn ::sconn ::wrk/executor]))
|
||||
(defn msgbus?
|
||||
[o]
|
||||
(satisfies? IMsgBus o))
|
||||
|
||||
(defmethod ig/pre-init-spec ::msgbus [_]
|
||||
(s/keys :req [::rds/redis ::wrk/executor]))
|
||||
(sm/register!
|
||||
{:type ::msgbus
|
||||
:pred msgbus?})
|
||||
|
||||
(defmethod ig/prep-key ::msgbus
|
||||
[_ cfg]
|
||||
(-> cfg
|
||||
(assoc ::buffer-size 128)
|
||||
(assoc ::timeout (dt/duration {:seconds 30}))))
|
||||
(defmethod ig/expand-key ::msgbus
|
||||
[k v]
|
||||
{k (-> (d/without-nils v)
|
||||
(assoc ::buffer-size 128)
|
||||
(assoc ::timeout (dt/duration {:seconds 30})))})
|
||||
|
||||
(def ^:private schema:params
|
||||
[:map ::rds/redis ::wrk/executor])
|
||||
|
||||
(defmethod ig/assert-key ::msgbus
|
||||
[_ params]
|
||||
(assert (sm/check schema:params params)))
|
||||
|
||||
(defmethod ig/init-key ::msgbus
|
||||
[_ {:keys [::buffer-size ::wrk/executor ::timeout ::rds/redis] :as cfg}]
|
||||
@@ -66,46 +74,66 @@
|
||||
:xf xform-prefix-topic)
|
||||
state (agent {})
|
||||
|
||||
pconn (rds/connect redis :timeout timeout)
|
||||
pconn (rds/connect redis :type :default :timeout timeout)
|
||||
sconn (rds/connect redis :type :pubsub :timeout timeout)
|
||||
msgbus (-> cfg
|
||||
|
||||
_ (set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
|
||||
_ (set-error-mode! state :continue)
|
||||
|
||||
cfg (-> cfg
|
||||
(assoc ::pconn pconn)
|
||||
(assoc ::sconn sconn)
|
||||
(assoc ::cmd-ch cmd-ch)
|
||||
(assoc ::rcv-ch rcv-ch)
|
||||
(assoc ::pub-ch pub-ch)
|
||||
(assoc ::state state)
|
||||
(assoc ::wrk/executor executor))]
|
||||
(assoc ::state state))
|
||||
|
||||
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
|
||||
(set-error-mode! state :continue)
|
||||
io-thr (start-io-loop cfg)]
|
||||
|
||||
(assoc msgbus ::io-thr (start-io-loop! msgbus))))
|
||||
(reify
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(px/interrupt! io-thr)
|
||||
(sp/close! cmd-ch)
|
||||
(sp/close! rcv-ch)
|
||||
(sp/close! pub-ch)
|
||||
(d/close! pconn)
|
||||
(d/close! sconn))
|
||||
|
||||
IMsgBus
|
||||
(-sub [_ topics chan]
|
||||
(l/debug :hint "subscribe" :topics topics :chan (hash chan))
|
||||
(send-via executor state subscribe-to-topics cfg topics chan))
|
||||
|
||||
(-pub [_ topic message]
|
||||
(let [message (assoc message :topic topic)]
|
||||
(sp/put! pub-ch {:topic topic :message message})))
|
||||
|
||||
(-purge [_ chans]
|
||||
(l/debug :hint "purge" :chans (count chans))
|
||||
(send-via executor state unsubscribe-channels cfg chans)))))
|
||||
|
||||
(defmethod ig/halt-key! ::msgbus
|
||||
[_ msgbus]
|
||||
(px/interrupt! (::io-thr msgbus))
|
||||
(sp/close! (::cmd-ch msgbus))
|
||||
(sp/close! (::rcv-ch msgbus))
|
||||
(sp/close! (::pub-ch msgbus))
|
||||
(d/close! (::pconn msgbus))
|
||||
(d/close! (::sconn msgbus)))
|
||||
[_ instance]
|
||||
(d/close! instance))
|
||||
|
||||
(defn sub!
|
||||
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
|
||||
[instance & {:keys [topic topics chan]}]
|
||||
(assert (satisfies? IMsgBus instance) "expected valid msgbus instance")
|
||||
(let [topics (into [] (map prefix-topic) (if topic [topic] topics))]
|
||||
(l/debug :hint "subscribe" :topics topics :chan (hash chan))
|
||||
(send-via executor state subscribe-to-topics cfg topics chan)
|
||||
(-sub instance topics chan)
|
||||
nil))
|
||||
|
||||
(defn pub!
|
||||
[{::keys [pub-ch]} & {:as params}]
|
||||
(sp/put! pub-ch params))
|
||||
[instance & {:keys [topic message]}]
|
||||
(assert (satisfies? IMsgBus instance) "expected valid msgbus instance")
|
||||
(-pub instance topic message))
|
||||
|
||||
(defn purge!
|
||||
[{:keys [::state ::wrk/executor] :as msgbus} chans]
|
||||
(l/debug :hint "purge" :chans (count chans))
|
||||
(send-via executor state unsubscribe-channels msgbus chans)
|
||||
[instance chans]
|
||||
(assert (satisfies? IMsgBus instance) "expected valid msgbus instance")
|
||||
(assert (every? sp/chan? chans) "expected a seq of chans")
|
||||
(-purge instance chans)
|
||||
nil)
|
||||
|
||||
;; --- IMPL
|
||||
@@ -118,7 +146,7 @@
|
||||
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
|
||||
(when (= 1 (count nsubs))
|
||||
(l/trace :hint "open subscription" :topic topic ::l/sync? true)
|
||||
(redis-sub! cfg topic))
|
||||
(redis-sub cfg topic))
|
||||
nsubs))
|
||||
|
||||
(defn- disj-subscription
|
||||
@@ -129,7 +157,7 @@
|
||||
(let [nsubs (disj nsubs chan)]
|
||||
(when (empty? nsubs)
|
||||
(l/trace :hint "close subscription" :topic topic ::l/sync? true)
|
||||
(redis-unsub! cfg topic))
|
||||
(redis-unsub cfg topic))
|
||||
nsubs))
|
||||
|
||||
(defn- subscribe-to-topics
|
||||
@@ -170,7 +198,7 @@
|
||||
(when-not (sp/offer! rcv-ch val)
|
||||
(l/warn :msg "dropping message on subscription loop"))))))
|
||||
|
||||
(defn- process-input!
|
||||
(defn- process-input
|
||||
[{:keys [::state ::wrk/executor] :as cfg} topic message]
|
||||
(let [chans (get-in @state [:topics topic])]
|
||||
(when-let [closed (loop [chans (seq chans)
|
||||
@@ -183,9 +211,9 @@
|
||||
(send-via executor state unsubscribe-channels cfg closed))))
|
||||
|
||||
|
||||
(defn start-io-loop!
|
||||
(defn start-io-loop
|
||||
[{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}]
|
||||
(rds/add-listener! sconn (create-listener rcv-ch))
|
||||
(rds/add-listener sconn (create-listener rcv-ch))
|
||||
|
||||
(px/thread
|
||||
{:name "penpot/msgbus/io-loop"
|
||||
@@ -209,12 +237,12 @@
|
||||
|
||||
(identical? port rcv-ch)
|
||||
(let [{:keys [topic message]} val]
|
||||
(process-input! cfg topic message)
|
||||
(process-input cfg topic message)
|
||||
(recur))
|
||||
|
||||
(identical? port pub-ch)
|
||||
(do
|
||||
(redis-pub! cfg val)
|
||||
(redis-pub cfg val)
|
||||
(recur)))))
|
||||
|
||||
(catch InterruptedException _
|
||||
@@ -230,13 +258,12 @@
|
||||
|
||||
(l/debug :hint "io-loop thread terminated")))))
|
||||
|
||||
|
||||
(defn- redis-pub!
|
||||
(defn- redis-pub
|
||||
"Publish a message to the redis server. Asynchronous operation,
|
||||
intended to be used in core.async go blocks."
|
||||
[{:keys [::pconn] :as cfg} {:keys [topic message]}]
|
||||
(try
|
||||
(p/await! (rds/publish! pconn topic (t/encode message)))
|
||||
(p/await! (rds/publish pconn topic (t/encode message)))
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Throwable cause
|
||||
@@ -244,23 +271,23 @@
|
||||
:message message
|
||||
:cause cause))))
|
||||
|
||||
(defn- redis-sub!
|
||||
(defn- redis-sub
|
||||
"Create redis subscription. Blocking operation, intended to be used
|
||||
inside an agent."
|
||||
[{:keys [::sconn] :as cfg} topic]
|
||||
(try
|
||||
(rds/subscribe! sconn topic)
|
||||
(rds/subscribe sconn [topic])
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Throwable cause
|
||||
(l/trace :hint "exception on subscribing" :topic topic :cause cause))))
|
||||
|
||||
(defn- redis-unsub!
|
||||
(defn- redis-unsub
|
||||
"Removes redis subscription. Blocking operation, intended to be used
|
||||
inside an agent."
|
||||
[{:keys [::sconn] :as cfg} topic]
|
||||
(try
|
||||
(rds/unsubscribe! sconn topic)
|
||||
(rds/unsubscribe sconn [topic])
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
(catch Throwable cause
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
(ns app.redis
|
||||
"The msgbus abstraction implemented using redis as underlying backend."
|
||||
(:refer-clojure :exclude [eval])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.metrics :as mtx]
|
||||
[app.redis.script :as-alias rscript]
|
||||
[app.util.cache :as cache]
|
||||
@@ -18,13 +19,11 @@
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.core :as c]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px])
|
||||
(:import
|
||||
clojure.lang.IDeref
|
||||
clojure.lang.MapEntry
|
||||
io.lettuce.core.KeyValue
|
||||
io.lettuce.core.RedisClient
|
||||
@@ -53,79 +52,24 @@
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(declare initialize-resources)
|
||||
(declare shutdown-resources)
|
||||
(declare connect*)
|
||||
(declare ^:private initialize-resources)
|
||||
(declare ^:private shutdown-resources)
|
||||
(declare ^:private impl-eval)
|
||||
|
||||
(s/def ::timer
|
||||
#(instance? Timer %))
|
||||
(defprotocol IRedis
|
||||
(-connect [_ options])
|
||||
(-get-or-connect [_ key options]))
|
||||
|
||||
(s/def ::default-connection
|
||||
#(or (instance? StatefulRedisConnection %)
|
||||
(and (instance? IDeref %)
|
||||
(instance? StatefulRedisConnection (deref %)))))
|
||||
(defprotocol IConnection
|
||||
(publish [_ topic message])
|
||||
(rpush [_ key payload])
|
||||
(blpop [_ timeout keys])
|
||||
(eval [_ script]))
|
||||
|
||||
(s/def ::pubsub-connection
|
||||
#(or (instance? StatefulRedisPubSubConnection %)
|
||||
(and (instance? IDeref %)
|
||||
(instance? StatefulRedisPubSubConnection (deref %)))))
|
||||
|
||||
(s/def ::connection
|
||||
(s/or :default ::default-connection
|
||||
:pubsub ::pubsub-connection))
|
||||
|
||||
(s/def ::connection-holder
|
||||
(s/keys :req [::connection]))
|
||||
|
||||
(s/def ::redis-uri
|
||||
#(instance? RedisURI %))
|
||||
|
||||
(s/def ::resources
|
||||
#(instance? ClientResources %))
|
||||
|
||||
(s/def ::pubsub-listener
|
||||
#(instance? RedisPubSubListener %))
|
||||
|
||||
(s/def ::uri ::us/not-empty-string)
|
||||
(s/def ::timeout ::dt/duration)
|
||||
(s/def ::connect? ::us/boolean)
|
||||
(s/def ::io-threads ::us/integer)
|
||||
(s/def ::worker-threads ::us/integer)
|
||||
(s/def ::cache cache/cache?)
|
||||
|
||||
(s/def ::redis
|
||||
(s/keys :req [::resources
|
||||
::redis-uri
|
||||
::timer
|
||||
::mtx/metrics]
|
||||
:opt [::connection
|
||||
::cache]))
|
||||
|
||||
(defmethod ig/prep-key ::redis
|
||||
[_ cfg]
|
||||
(let [cpus (px/get-available-processors)
|
||||
threads (max 1 (int (* cpus 0.2)))]
|
||||
(merge {::timeout (dt/duration "10s")
|
||||
::io-threads (max 3 threads)
|
||||
::worker-threads (max 3 threads)}
|
||||
(d/without-nils cfg))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::redis [_]
|
||||
(s/keys :req [::uri ::mtx/metrics]
|
||||
:opt [::timeout
|
||||
::connect?
|
||||
::io-threads
|
||||
::worker-threads]))
|
||||
|
||||
(defmethod ig/init-key ::redis
|
||||
[_ {:keys [::connect?] :as cfg}]
|
||||
(let [state (initialize-resources cfg)]
|
||||
(cond-> state
|
||||
connect? (assoc ::connection (connect* cfg {})))))
|
||||
|
||||
(defmethod ig/halt-key! ::redis
|
||||
[_ state]
|
||||
(shutdown-resources state))
|
||||
(defprotocol IPubSubConnection
|
||||
(add-listener [_ listener])
|
||||
(subscribe [_ topics])
|
||||
(unsubscribe [_ topics]))
|
||||
|
||||
(def default-codec
|
||||
(RedisCodec/of StringCodec/UTF8 ByteArrayCodec/INSTANCE))
|
||||
@@ -133,23 +77,76 @@
|
||||
(def string-codec
|
||||
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
|
||||
|
||||
(defn- create-cache
|
||||
[{:keys [::wrk/executor] :as cfg}]
|
||||
(letfn [(on-remove [key val cause]
|
||||
(l/trace :hint "evict connection (cache)" :key key :reason cause)
|
||||
(some-> val d/close!))]
|
||||
(cache/create :executor executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")))
|
||||
(sm/register!
|
||||
{:type ::connection
|
||||
:pred #(satisfies? IConnection %)
|
||||
:type-properties
|
||||
{:title "connection"
|
||||
:description "redis connection instance"}})
|
||||
|
||||
(sm/register!
|
||||
{:type ::pubsub-connection
|
||||
:pred #(satisfies? IPubSubConnection %)
|
||||
:type-properties
|
||||
{:title "connection"
|
||||
:description "redis connection instance"}})
|
||||
|
||||
(defn redis?
|
||||
[o]
|
||||
(satisfies? IRedis o))
|
||||
|
||||
(sm/register!
|
||||
{:type ::redis
|
||||
:pred redis?})
|
||||
|
||||
(def ^:private schema:script
|
||||
[:map {:title "script"}
|
||||
[::rscript/name qualified-keyword?]
|
||||
[::rscript/path ::sm/text]
|
||||
[::rscript/keys {:optional true} [:vector :any]]
|
||||
[::rscript/vals {:optional true} [:vector :any]]])
|
||||
|
||||
(def valid-script?
|
||||
(sm/lazy-validator schema:script))
|
||||
|
||||
(defmethod ig/expand-key ::redis
|
||||
[k v]
|
||||
(let [cpus (px/get-available-processors)
|
||||
threads (max 1 (int (* cpus 0.2)))]
|
||||
{k (-> (d/without-nils v)
|
||||
(assoc ::timeout (dt/duration "10s"))
|
||||
(assoc ::io-threads (max 3 threads))
|
||||
(assoc ::worker-threads (max 3 threads)))}))
|
||||
|
||||
(def ^:private schema:redis-params
|
||||
[:map {:title "redis-params"}
|
||||
::wrk/executor
|
||||
::mtx/metrics
|
||||
[::uri ::sm/uri]
|
||||
[::worker-threads ::sm/int]
|
||||
[::io-threads ::sm/int]
|
||||
[::timeout ::dt/duration]])
|
||||
|
||||
(defmethod ig/assert-key ::redis
|
||||
[_ params]
|
||||
(assert (sm/check schema:redis-params params)))
|
||||
|
||||
(defmethod ig/init-key ::redis
|
||||
[_ params]
|
||||
(initialize-resources params))
|
||||
|
||||
(defmethod ig/halt-key! ::redis
|
||||
[_ instance]
|
||||
(d/close! instance))
|
||||
|
||||
(defn- initialize-resources
|
||||
"Initialize redis connection resources"
|
||||
[{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}]
|
||||
(l/info :hint "initialize redis resources"
|
||||
:uri uri
|
||||
:io-threads io-threads
|
||||
:worker-threads worker-threads
|
||||
:connect? connect?)
|
||||
[{:keys [::uri ::io-threads ::worker-threads ::wrk/executor ::mtx/metrics] :as params}]
|
||||
|
||||
(l/inf :hint "initialize redis resources"
|
||||
:uri (str uri)
|
||||
:io-threads io-threads
|
||||
:worker-threads worker-threads)
|
||||
|
||||
(let [timer (HashedWheelTimer.)
|
||||
resources (.. (DefaultClientResources/builder)
|
||||
@@ -158,147 +155,134 @@
|
||||
(timer ^Timer timer)
|
||||
(build))
|
||||
|
||||
redis-uri (RedisURI/create ^String uri)
|
||||
cfg (-> cfg
|
||||
(assoc ::resources resources)
|
||||
(assoc ::timer timer)
|
||||
(assoc ::redis-uri redis-uri))]
|
||||
redis-uri (RedisURI/create ^String (str uri))
|
||||
|
||||
(assoc cfg ::cache (create-cache cfg))))
|
||||
shutdown (fn [client conn]
|
||||
(ex/ignoring (.close ^StatefulConnection conn))
|
||||
(ex/ignoring (.close ^RedisClient client))
|
||||
(l/trc :hint "disconnect" :hid (hash client)))
|
||||
|
||||
(defn- shutdown-resources
|
||||
[{:keys [::resources ::cache ::timer]}]
|
||||
(cache/invalidate! cache)
|
||||
on-remove (fn [key val cause]
|
||||
(l/trace :hint "evict connection (cache)" :key key :reason cause)
|
||||
(some-> val d/close!))
|
||||
|
||||
(when resources
|
||||
(.shutdown ^ClientResources resources))
|
||||
|
||||
(when timer
|
||||
(.stop ^Timer timer)))
|
||||
|
||||
(defn connect*
|
||||
[{:keys [::resources ::redis-uri] :as state}
|
||||
{:keys [timeout codec type]
|
||||
:or {codec default-codec type :default}}]
|
||||
|
||||
(us/assert! ::resources resources)
|
||||
(let [client (RedisClient/create ^ClientResources resources ^RedisURI redis-uri)
|
||||
timeout (or timeout (::timeout state))
|
||||
conn (case type
|
||||
:default (.connect ^RedisClient client ^RedisCodec codec)
|
||||
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
|
||||
|
||||
(l/trc :hint "connect" :hid (hash client))
|
||||
(.setTimeout ^StatefulConnection conn ^Duration timeout)
|
||||
cache (cache/create :executor executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")]
|
||||
(reify
|
||||
IDeref
|
||||
(deref [_] conn)
|
||||
|
||||
AutoCloseable
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(ex/ignoring (.close ^StatefulConnection conn))
|
||||
(ex/ignoring (.shutdown ^RedisClient client))
|
||||
(l/trc :hint "disconnect" :hid (hash client))))))
|
||||
(ex/ignoring (cache/invalidate! cache))
|
||||
(ex/ignoring (.shutdown ^ClientResources resources))
|
||||
(ex/ignoring (.stop ^Timer timer)))
|
||||
|
||||
IRedis
|
||||
(-get-or-connect [this key options]
|
||||
(let [create (fn [_] (-connect this options))]
|
||||
(cache/get cache key create)))
|
||||
|
||||
(-connect [_ options]
|
||||
(let [timeout (or (:timeout options) (::timeout params))
|
||||
codec (get options :codec default-codec)
|
||||
type (get options :type :default)
|
||||
client (RedisClient/create ^ClientResources resources
|
||||
^RedisURI redis-uri)]
|
||||
|
||||
(l/trc :hint "connect" :hid (hash client))
|
||||
(if (= type :pubsub)
|
||||
(let [conn (.connectPubSub ^RedisClient client
|
||||
^RedisCodec codec)]
|
||||
(.setTimeout ^StatefulConnection conn
|
||||
^Duration timeout)
|
||||
(reify
|
||||
IPubSubConnection
|
||||
(add-listener [_ listener]
|
||||
(assert (instance? RedisPubSubListener listener) "expected listener instance")
|
||||
(.addListener ^StatefulRedisPubSubConnection conn
|
||||
^RedisPubSubListener listener))
|
||||
|
||||
(subscribe [_ topics]
|
||||
(try
|
||||
(let [topics (into-array String (map str topics))
|
||||
cmd (.sync ^StatefulRedisPubSubConnection conn)]
|
||||
(.subscribe ^RedisPubSubCommands cmd topics))
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
(unsubscribe [_ topics]
|
||||
(try
|
||||
(let [topics (into-array String (map str topics))
|
||||
cmd (.sync ^StatefulRedisPubSubConnection conn)]
|
||||
(.unsubscribe ^RedisPubSubCommands cmd topics))
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
|
||||
AutoCloseable
|
||||
(close [_] (shutdown client conn))))
|
||||
|
||||
(let [conn (.connect ^RedisClient client ^RedisCodec codec)]
|
||||
(.setTimeout ^StatefulConnection conn ^Duration timeout)
|
||||
(reify
|
||||
IConnection
|
||||
(publish [_ topic message]
|
||||
(assert (string? topic) "expected topic to be string")
|
||||
(assert (bytes? message) "expected message to be a byte array")
|
||||
|
||||
(let [pcomm (.async ^StatefulRedisConnection conn)]
|
||||
(.publish ^RedisAsyncCommands pcomm ^String topic ^bytes message)))
|
||||
|
||||
(rpush [_ key payload]
|
||||
(assert (or (and (vector? payload)
|
||||
(every? bytes? payload))
|
||||
(bytes? payload)))
|
||||
(try
|
||||
(let [cmd (.sync ^StatefulRedisConnection conn)
|
||||
data (if (vector? payload) payload [payload])
|
||||
vals (make-array (. Class (forName "[B")) (count data))]
|
||||
|
||||
(loop [i 0 xs (seq data)]
|
||||
(when xs
|
||||
(aset ^"[[B" vals i ^bytes (first xs))
|
||||
(recur (inc i) (next xs))))
|
||||
|
||||
(.rpush ^RedisCommands cmd
|
||||
^String key
|
||||
^"[[B" vals))
|
||||
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
(blpop [_ timeout keys]
|
||||
(try
|
||||
(let [keys (into-array Object (map str keys))
|
||||
cmd (.sync ^StatefulRedisConnection conn)
|
||||
timeout (/ (double (inst-ms timeout)) 1000.0)]
|
||||
(when-let [res (.blpop ^RedisCommands cmd
|
||||
^double timeout
|
||||
^"[Ljava.lang.String;" keys)]
|
||||
(MapEntry/create
|
||||
(.getKey ^KeyValue res)
|
||||
(.getValue ^KeyValue res))))
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
(eval [_ script]
|
||||
(assert (valid-script? script) "expected valid script")
|
||||
(impl-eval conn metrics script))
|
||||
|
||||
AutoCloseable
|
||||
(close [_] (shutdown client conn))))))))))
|
||||
|
||||
(defn connect
|
||||
[state & {:as opts}]
|
||||
(let [connection (connect* state opts)]
|
||||
(-> state
|
||||
(assoc ::connection connection)
|
||||
(dissoc ::cache)
|
||||
(vary-meta assoc `d/close! (fn [_] (d/close! connection))))))
|
||||
[instance & {:as opts}]
|
||||
(assert (satisfies? IRedis instance) "expected valid redis instance")
|
||||
(-connect instance opts))
|
||||
|
||||
(defn get-or-connect
|
||||
[{:keys [::cache] :as state} key options]
|
||||
(us/assert! ::redis state)
|
||||
(let [create (fn [_] (connect* state options))
|
||||
connection (cache/get cache key create)]
|
||||
(-> state
|
||||
(dissoc ::cache)
|
||||
(assoc ::connection connection))))
|
||||
|
||||
(defn add-listener!
|
||||
[{:keys [::connection] :as conn} listener]
|
||||
(us/assert! ::pubsub-connection connection)
|
||||
(us/assert! ::pubsub-listener listener)
|
||||
(.addListener ^StatefulRedisPubSubConnection @connection
|
||||
^RedisPubSubListener listener)
|
||||
conn)
|
||||
|
||||
(defn publish!
|
||||
[{:keys [::connection]} topic message]
|
||||
(us/assert! ::us/string topic)
|
||||
(us/assert! ::us/bytes message)
|
||||
(us/assert! ::default-connection connection)
|
||||
|
||||
(let [pcomm (.async ^StatefulRedisConnection @connection)]
|
||||
(.publish ^RedisAsyncCommands pcomm ^String topic ^bytes message)))
|
||||
|
||||
(defn subscribe!
|
||||
"Blocking operation, intended to be used on a thread/agent thread."
|
||||
[{:keys [::connection]} & topics]
|
||||
(us/assert! ::pubsub-connection connection)
|
||||
(try
|
||||
(let [topics (into-array String (map str topics))
|
||||
cmd (.sync ^StatefulRedisPubSubConnection @connection)]
|
||||
(.subscribe ^RedisPubSubCommands cmd topics))
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
(defn unsubscribe!
|
||||
"Blocking operation, intended to be used on a thread/agent thread."
|
||||
[{:keys [::connection]} & topics]
|
||||
(us/assert! ::pubsub-connection connection)
|
||||
(try
|
||||
(let [topics (into-array String (map str topics))
|
||||
cmd (.sync ^StatefulRedisPubSubConnection @connection)]
|
||||
(.unsubscribe ^RedisPubSubCommands cmd topics))
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
(defn rpush!
|
||||
[{:keys [::connection]} key payload]
|
||||
(us/assert! ::default-connection connection)
|
||||
(us/assert! (or (and (vector? payload)
|
||||
(every? bytes? payload))
|
||||
(bytes? payload)))
|
||||
(try
|
||||
(let [cmd (.sync ^StatefulRedisConnection @connection)
|
||||
data (if (vector? payload) payload [payload])
|
||||
vals (make-array (. Class (forName "[B")) (count data))]
|
||||
|
||||
(loop [i 0 xs (seq data)]
|
||||
(when xs
|
||||
(aset ^"[[B" vals i ^bytes (first xs))
|
||||
(recur (inc i) (next xs))))
|
||||
|
||||
(.rpush ^RedisCommands cmd
|
||||
^String key
|
||||
^"[[B" vals))
|
||||
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
(defn blpop!
|
||||
[{:keys [::connection]} timeout & keys]
|
||||
(us/assert! ::default-connection connection)
|
||||
(try
|
||||
(let [keys (into-array Object (map str keys))
|
||||
cmd (.sync ^StatefulRedisConnection @connection)
|
||||
timeout (/ (double (inst-ms timeout)) 1000.0)]
|
||||
(when-let [res (.blpop ^RedisCommands cmd
|
||||
^double timeout
|
||||
^"[Ljava.lang.String;" keys)]
|
||||
(MapEntry/create
|
||||
(.getKey ^KeyValue res)
|
||||
(.getValue ^KeyValue res))))
|
||||
(catch RedisCommandInterruptedException cause
|
||||
(throw (InterruptedException. (ex-message cause))))))
|
||||
|
||||
(defn open?
|
||||
[{:keys [::connection]}]
|
||||
(us/assert! ::pubsub-connection connection)
|
||||
(.isOpen ^StatefulConnection @connection))
|
||||
[instance key & {:as opts}]
|
||||
(assert (satisfies? IRedis instance) "expected valid redis instance")
|
||||
(-get-or-connect instance key opts))
|
||||
|
||||
(defn pubsub-listener
|
||||
[& {:keys [on-message on-subscribe on-unsubscribe]}]
|
||||
@@ -328,26 +312,10 @@
|
||||
(on-unsubscribe nil topic count)))))
|
||||
|
||||
(def ^:private scripts-cache (atom {}))
|
||||
(def noop-fn (constantly nil))
|
||||
|
||||
(s/def ::rscript/name qualified-keyword?)
|
||||
(s/def ::rscript/path ::us/not-empty-string)
|
||||
(s/def ::rscript/keys (s/every any? :kind vector?))
|
||||
(s/def ::rscript/vals (s/every any? :kind vector?))
|
||||
|
||||
(s/def ::rscript/script
|
||||
(s/keys :req [::rscript/name
|
||||
::rscript/path]
|
||||
:opt [::rscript/keys
|
||||
::rscript/vals]))
|
||||
|
||||
(defn eval!
|
||||
[{:keys [::mtx/metrics ::connection] :as state} script]
|
||||
(us/assert! ::redis state)
|
||||
(us/assert! ::default-connection connection)
|
||||
(us/assert! ::rscript/script script)
|
||||
|
||||
(let [cmd (.async ^StatefulRedisConnection @connection)
|
||||
(defn- impl-eval
|
||||
[^StatefulRedisConnection connection metrics script]
|
||||
(let [cmd (.async ^StatefulRedisConnection connection)
|
||||
keys (into-array String (map str (::rscript/keys script)))
|
||||
vals (into-array String (map str (::rscript/vals script)))
|
||||
sname (::rscript/name script)]
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[ring.request :as rreq]
|
||||
[ring.response :as rres]))
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
|
||||
@@ -64,16 +64,16 @@
|
||||
response (if (fn? result)
|
||||
(result request)
|
||||
(let [result (rph/unwrap result)]
|
||||
{::rres/status (::http/status mdata 200)
|
||||
::rres/headers (::http/headers mdata {})
|
||||
::rres/body result}))]
|
||||
{::yres/status (::http/status mdata 200)
|
||||
::yres/headers (::http/headers mdata {})
|
||||
::yres/body result}))]
|
||||
(-> response
|
||||
(handle-response-transformation request mdata)
|
||||
(handle-before-comple-hook mdata))))
|
||||
|
||||
(defn get-external-session-id
|
||||
[request]
|
||||
(when-let [session-id (rreq/get-header request "x-external-session-id")]
|
||||
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
(when-not (or (> (count session-id) 256)
|
||||
(= session-id "null")
|
||||
(str/blank? session-id))
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
(defn- get-external-event-origin
|
||||
[request]
|
||||
(when-let [origin (rreq/get-header request "x-event-origin")]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(str/blank? origin))
|
||||
@@ -92,7 +92,7 @@
|
||||
internal async flow into ring async flow."
|
||||
[methods {:keys [params path-params method] :as request}]
|
||||
(let [handler-name (:type path-params)
|
||||
etag (rreq/get-header request "if-none-match")
|
||||
etag (yreq/get-header request "if-none-match")
|
||||
profile-id (or (::session/profile-id request)
|
||||
(::actoken/profile-id request))
|
||||
|
||||
@@ -149,6 +149,13 @@
|
||||
:hint "authentication required for this endpoint")
|
||||
(f cfg params)))))
|
||||
|
||||
(defn- wrap-db-transaction
|
||||
[_ f mdata]
|
||||
(if (::db/transaction mdata)
|
||||
(fn [cfg params]
|
||||
(db/tx-run! cfg f params))
|
||||
f))
|
||||
|
||||
(defn- wrap-audit
|
||||
[_ f mdata]
|
||||
(if (or (contains? cf/flags :webhooks)
|
||||
@@ -178,41 +185,25 @@
|
||||
(if-let [schema (::sm/params mdata)]
|
||||
(let [validate (sm/validator schema)
|
||||
explain (sm/explainer schema)
|
||||
decode (sm/decoder schema)]
|
||||
decode (sm/decoder schema sm/json-transformer)
|
||||
encode (sm/encoder schema sm/json-transformer)]
|
||||
(fn [cfg params]
|
||||
(let [params (decode params)]
|
||||
(if (validate params)
|
||||
(f cfg params)
|
||||
|
||||
(let [result (f cfg params)]
|
||||
(if (instance? clojure.lang.IObj result)
|
||||
(vary-meta result assoc :encode/json encode)
|
||||
result))
|
||||
(let [params (d/without-qualified params)]
|
||||
(ex/raise :type :validation
|
||||
:code :params-validation
|
||||
::sm/explain (explain params)))))))
|
||||
f))
|
||||
|
||||
(defn- wrap-output-validation
|
||||
[_ f mdata]
|
||||
(if (contains? cf/flags :rpc-output-validation)
|
||||
(or (when-let [schema (::sm/result mdata)]
|
||||
(let [schema (if (sm/lazy-schema? schema)
|
||||
schema
|
||||
(sm/define schema))
|
||||
validate (sm/validator schema)
|
||||
explain (sm/explainer schema)]
|
||||
(fn [cfg params]
|
||||
(let [response (f cfg params)]
|
||||
(when (map? response)
|
||||
(when-not (validate response)
|
||||
(ex/raise :type :validation
|
||||
:code :data-validation
|
||||
::sm/explain (explain response))))
|
||||
response))))
|
||||
f)
|
||||
f))
|
||||
|
||||
(defn- wrap-all
|
||||
[cfg f mdata]
|
||||
(as-> f $
|
||||
(wrap-db-transaction cfg $ mdata)
|
||||
(cond/wrap cfg $ mdata)
|
||||
(retry/wrap-retry cfg $ mdata)
|
||||
(climit/wrap cfg $ mdata)
|
||||
@@ -220,7 +211,6 @@
|
||||
(rlimit/wrap cfg $ mdata)
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-output-validation cfg $ mdata)
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
|
||||
@@ -260,39 +250,49 @@
|
||||
'app.rpc.commands.projects
|
||||
'app.rpc.commands.search
|
||||
'app.rpc.commands.teams
|
||||
'app.rpc.commands.teams-invitations
|
||||
'app.rpc.commands.verify-token
|
||||
'app.rpc.commands.viewer
|
||||
'app.rpc.commands.webhooks)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::methods [_]
|
||||
(s/keys :req [::session/manager
|
||||
::http.client/client
|
||||
::db/pool
|
||||
::mbus/msgbus
|
||||
::ldap/provider
|
||||
::sto/storage
|
||||
::mtx/metrics
|
||||
::setup/props]
|
||||
:opt [::climit
|
||||
::rlimit]))
|
||||
(def ^:private schema:methods-params
|
||||
[:map {:title "methods-params"}
|
||||
::session/manager
|
||||
::http.client/client
|
||||
::db/pool
|
||||
::mbus/msgbus
|
||||
::sto/storage
|
||||
::mtx/metrics
|
||||
[::ldap/provider [:maybe ::ldap/provider]]
|
||||
[::climit [:maybe ::climit]]
|
||||
[::rlimit [:maybe ::rlimit]]
|
||||
::setup/props])
|
||||
|
||||
(defmethod ig/assert-key ::methods
|
||||
[_ params]
|
||||
(assert (sm/check schema:methods-params params)))
|
||||
|
||||
(defmethod ig/init-key ::methods
|
||||
[_ cfg]
|
||||
(let [cfg (d/without-nils cfg)]
|
||||
(resolve-command-methods cfg)))
|
||||
|
||||
(s/def ::methods
|
||||
(s/map-of keyword? (s/tuple map? fn?)))
|
||||
(def ^:private schema:methods
|
||||
[:map-of :keyword [:tuple :map ::sm/fn]])
|
||||
|
||||
(s/def ::routes vector?)
|
||||
(sm/register! ::methods schema:methods)
|
||||
|
||||
(defmethod ig/pre-init-spec ::routes [_]
|
||||
(s/keys :req [::methods
|
||||
::db/pool
|
||||
::setup/props
|
||||
::session/manager]))
|
||||
(def ^:private valid-methods?
|
||||
(sm/validator schema:methods))
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
||||
(assert (some? (::setup/props params)))
|
||||
(assert (session/manager? (::session/manager params)) "expect valid session manager")
|
||||
(assert (valid-methods? (::methods params)) "expect valid methods map"))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::methods] :as cfg}]
|
||||
|
||||
@@ -10,18 +10,15 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.common.schema :as sm]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit.config :as-alias config]
|
||||
[app.util.cache :as cache]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
@@ -32,6 +29,62 @@
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(declare ^:private impl-invoke)
|
||||
(declare ^:private id->str)
|
||||
(declare ^:private create-cache)
|
||||
|
||||
(defprotocol IConcurrencyLimiter
|
||||
(^:private get-config [_ limit-id] "get a config for a key")
|
||||
(^:private invoke [_ config handler] "invoke a handler for a config"))
|
||||
|
||||
(sm/register!
|
||||
{:type ::rpc/climit
|
||||
:pred #(satisfies? IConcurrencyLimiter %)})
|
||||
|
||||
(def ^:private schema:config
|
||||
[:map-of :keyword
|
||||
[:map
|
||||
[::id {:optional true} :keyword]
|
||||
[::key {:optional true} :any]
|
||||
[::label {:optional true} ::sm/text]
|
||||
[::params {:optional true} :map]
|
||||
[::permits {:optional true} ::sm/int]
|
||||
[::queue {:optional true} ::sm/int]
|
||||
[::timeout {:optional true} ::sm/int]]])
|
||||
|
||||
(def ^:private check-config
|
||||
(sm/check-fn schema:config))
|
||||
|
||||
(def ^:private schema:climit-params
|
||||
[:map
|
||||
::mtx/metrics
|
||||
::wrk/executor
|
||||
[::enabled {:optional true} ::sm/boolean]
|
||||
[::config {:optional true} ::fs/path]])
|
||||
|
||||
(defmethod ig/assert-key ::rpc/climit
|
||||
[_ params]
|
||||
(assert (sm/valid? schema:climit-params params)))
|
||||
|
||||
(defmethod ig/init-key ::rpc/climit
|
||||
[_ {:keys [::config ::enabled ::mtx/metrics] :as cfg}]
|
||||
(when enabled
|
||||
(when-let [params (some->> config slurp edn/read-string check-config)]
|
||||
(l/inf :hint "initializing concurrency limit" :config (str config))
|
||||
(let [params (reduce-kv (fn [result k v]
|
||||
(assoc result k (assoc v ::id k)))
|
||||
params
|
||||
params)
|
||||
cache (create-cache cfg)]
|
||||
|
||||
(reify
|
||||
IConcurrencyLimiter
|
||||
(get-config [_ id]
|
||||
(get params id))
|
||||
|
||||
(invoke [_ config handler]
|
||||
(impl-invoke metrics cache config handler)))))))
|
||||
|
||||
(defn- id->str
|
||||
([id]
|
||||
(-> (str id)
|
||||
@@ -41,59 +94,23 @@
|
||||
(str (-> (str id) (subs 1)) "/" key)
|
||||
(id->str id))))
|
||||
|
||||
(defn- create-cache
|
||||
[{:keys [::wrk/executor]}]
|
||||
(letfn [(on-remove [key _ cause]
|
||||
(let [[id skey] key]
|
||||
(l/trc :hint "disposed" :id (id->str id skey) :reason (str cause))))]
|
||||
(cache/create :executor executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")))
|
||||
|
||||
(s/def ::config/permits ::us/integer)
|
||||
(s/def ::config/queue ::us/integer)
|
||||
(s/def ::config/timeout ::us/integer)
|
||||
(s/def ::config
|
||||
(s/map-of keyword?
|
||||
(s/keys :opt-un [::config/permits
|
||||
::config/queue
|
||||
::config/timeout])))
|
||||
|
||||
(defmethod ig/prep-key ::rpc/climit
|
||||
[_ cfg]
|
||||
(assoc cfg ::path (cf/get :rpc-climit-config)))
|
||||
|
||||
(s/def ::path ::fs/path)
|
||||
(defmethod ig/pre-init-spec ::rpc/climit [_]
|
||||
(s/keys :req [::mtx/metrics ::wrk/executor ::path]))
|
||||
|
||||
(defmethod ig/init-key ::rpc/climit
|
||||
[_ {:keys [::path ::mtx/metrics] :as cfg}]
|
||||
(when (contains? cf/flags :rpc-climit)
|
||||
(when-let [params (some->> path slurp edn/read-string)]
|
||||
(l/inf :hint "initializing concurrency limit" :config (str path))
|
||||
(us/verify! ::config params)
|
||||
{::cache (create-cache cfg)
|
||||
::config params
|
||||
::mtx/metrics metrics})))
|
||||
|
||||
(s/def ::cache cache/cache?)
|
||||
(s/def ::instance
|
||||
(s/keys :req [::cache ::config]))
|
||||
|
||||
(s/def ::rpc/climit
|
||||
(s/nilable ::instance))
|
||||
|
||||
(defn- create-limiter
|
||||
[config [id skey]]
|
||||
(l/trc :hint "created" :id (id->str id skey))
|
||||
[config id]
|
||||
(l/trc :hint "created" :id id)
|
||||
(pbh/create :permits (or (:permits config) (:concurrency config))
|
||||
:queue (or (:queue config) (:queue-size config))
|
||||
:timeout (:timeout config)
|
||||
:type :semaphore))
|
||||
|
||||
(defn- create-cache
|
||||
[{:keys [::wrk/executor]}]
|
||||
(letfn [(on-remove [id _ cause]
|
||||
(l/trc :hint "disposed" :id id :reason (str cause)))]
|
||||
(cache/create :executor executor
|
||||
:on-remove on-remove
|
||||
:keepalive "5m")))
|
||||
|
||||
(defn measure!
|
||||
(defn- measure
|
||||
[metrics mlabels stats elapsed]
|
||||
(let [mpermits (:max-permits stats)
|
||||
permits (:permits stats)
|
||||
@@ -117,8 +134,14 @@
|
||||
:val (inst-ms elapsed)
|
||||
:labels mlabels))))
|
||||
|
||||
(defn log!
|
||||
[action req-id stats limit-id limit-label params elapsed]
|
||||
(defn- prepare-params-for-debug
|
||||
[params]
|
||||
(-> (select-keys params [::rpc/profile-id :file-id :profile-id])
|
||||
(set/rename-keys {::rpc/profile-id :profile-id})
|
||||
(update-vals str)))
|
||||
|
||||
(defn- log
|
||||
[action req-id stats limit-id limit-label limit-params elapsed]
|
||||
(let [mpermits (:max-permits stats)
|
||||
queue (:queue stats)
|
||||
queue (- queue mpermits)
|
||||
@@ -132,37 +155,42 @@
|
||||
:label limit-label
|
||||
:queue queue
|
||||
:elapsed (some-> elapsed dt/format-duration)
|
||||
:params (-> (select-keys params [::rpc/profile-id :file-id :profile-id])
|
||||
(set/rename-keys {::rpc/profile-id :profile-id})
|
||||
(update-vals str)))))
|
||||
:params @limit-params)))
|
||||
|
||||
(def ^:private idseq (AtomicLong. 0))
|
||||
|
||||
(defn- invoke
|
||||
[limiter metrics limit-id limit-key limit-label handler params]
|
||||
(let [tpoint (dt/tpoint)
|
||||
mlabels (into-array String [(id->str limit-id)])
|
||||
limit-id (id->str limit-id limit-key)
|
||||
stats (pbh/get-stats limiter)
|
||||
req-id (.incrementAndGet ^AtomicLong idseq)]
|
||||
(defn- impl-invoke
|
||||
[metrics cache config handler]
|
||||
(let [limit-id (::id config)
|
||||
limit-key (::key config)
|
||||
limit-label (::label config)
|
||||
limit-params (delay
|
||||
(prepare-params-for-debug
|
||||
(::params config)))
|
||||
|
||||
mlabels (into-array String [(id->str limit-id)])
|
||||
limit-id (id->str limit-id limit-key)
|
||||
limiter (cache/get cache limit-id (partial create-limiter config))
|
||||
tpoint (dt/tpoint)
|
||||
req-id (.incrementAndGet ^AtomicLong idseq)]
|
||||
(try
|
||||
(measure! metrics mlabels stats nil)
|
||||
(log! "enqueued" req-id stats limit-id limit-label params nil)
|
||||
(let [stats (pbh/get-stats limiter)]
|
||||
(measure metrics mlabels stats nil)
|
||||
(log "enqueued" req-id stats limit-id limit-label limit-params nil))
|
||||
|
||||
(px/invoke! limiter (fn []
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
|
||||
(measure! metrics mlabels stats elapsed)
|
||||
(log! "acquired" req-id stats limit-id limit-label params elapsed)
|
||||
|
||||
(handler params))))
|
||||
(measure metrics mlabels stats elapsed)
|
||||
(log "acquired" req-id stats limit-id limit-label limit-params elapsed)
|
||||
(handler))))
|
||||
|
||||
(catch ExceptionInfo cause
|
||||
(let [{:keys [type code]} (ex-data cause)]
|
||||
(if (= :bulkhead-error type)
|
||||
(let [elapsed (tpoint)]
|
||||
(log! "rejected" req-id stats limit-id limit-label params elapsed)
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
(log "rejected" req-id stats limit-id limit-label limit-params elapsed)
|
||||
(ex/raise :type :concurrency-limit
|
||||
:code code
|
||||
:hint "concurrency limit reached"
|
||||
@@ -173,8 +201,8 @@
|
||||
(let [elapsed (tpoint)
|
||||
stats (pbh/get-stats limiter)]
|
||||
|
||||
(measure! metrics mlabels stats nil)
|
||||
(log! "finished" req-id stats limit-id limit-label params elapsed))))))
|
||||
(measure metrics mlabels stats nil)
|
||||
(log "finished" req-id stats limit-id limit-label limit-params elapsed))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MIDDLEWARE
|
||||
@@ -204,71 +232,70 @@
|
||||
(throw (IllegalArgumentException. "unable to normalize limit")))))
|
||||
|
||||
(defn wrap
|
||||
[{:keys [::rpc/climit ::mtx/metrics]} handler mdata]
|
||||
(let [cache (::cache climit)
|
||||
config (::config climit)
|
||||
label (::sv/name mdata)]
|
||||
[cfg handler {label ::sv/name :as mdata}]
|
||||
(if-let [climit (::rpc/climit cfg)]
|
||||
(reduce (fn [handler [limit-id key-fn]]
|
||||
(if-let [config (get-config climit limit-id)]
|
||||
(let [key-fn (or key-fn noop-fn)]
|
||||
(l/trc :hint "instrumenting method"
|
||||
:method label
|
||||
:limit (id->str limit-id)
|
||||
:timeout (:timeout config)
|
||||
:permits (:permits config)
|
||||
:queue (:queue config)
|
||||
:keyed (not= key-fn nil))
|
||||
|
||||
(if climit
|
||||
(reduce (fn [handler [limit-id key-fn]]
|
||||
(if-let [config (get config limit-id)]
|
||||
(let [key-fn (or key-fn noop-fn)]
|
||||
(l/trc :hint "instrumenting method"
|
||||
:method label
|
||||
:limit (id->str limit-id)
|
||||
:timeout (:timeout config)
|
||||
:permits (:permits config)
|
||||
:queue (:queue config)
|
||||
:keyed (not= key-fn noop-fn))
|
||||
(if (and (= key-fn ::rpc/profile-id)
|
||||
(false? (::rpc/auth mdata true)))
|
||||
|
||||
(if (and (= key-fn ::rpc/profile-id)
|
||||
(false? (::rpc/auth mdata true)))
|
||||
;; We don't enforce by-profile limit on methods that does
|
||||
;; not require authentication
|
||||
handler
|
||||
|
||||
;; We don't enforce by-profile limit on methods that does
|
||||
;; not require authentication
|
||||
handler
|
||||
(fn [cfg params]
|
||||
(let [config (-> config
|
||||
(assoc ::key (key-fn params))
|
||||
(assoc ::label label)
|
||||
;; NOTE: only used for debugging output
|
||||
(assoc ::params params))]
|
||||
(invoke climit config (partial handler cfg params))))))
|
||||
|
||||
(fn [cfg params]
|
||||
(let [limit-key (key-fn params)
|
||||
cache-key [limit-id limit-key]
|
||||
limiter (cache/get cache cache-key (partial create-limiter config))
|
||||
handler (partial handler cfg)]
|
||||
(invoke limiter metrics limit-id limit-key label handler params)))))
|
||||
(do
|
||||
(l/wrn :hint "no config found for specified queue" :id (id->str limit-id))
|
||||
handler)))
|
||||
handler
|
||||
(concat global-limits (get-limits mdata)))
|
||||
|
||||
(do
|
||||
(l/wrn :hint "no config found for specified queue" :id (id->str limit-id))
|
||||
handler)))
|
||||
|
||||
handler
|
||||
(concat global-limits (get-limits mdata)))
|
||||
handler)))
|
||||
handler))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; PUBLIC API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- build-exec-chain
|
||||
[{:keys [::label ::rpc/climit ::mtx/metrics] :as cfg} f]
|
||||
(let [config (get climit ::config)
|
||||
cache (get climit ::cache)]
|
||||
(reduce (fn [handler [limit-id limit-key :as ckey]]
|
||||
(if-let [config (get config limit-id)]
|
||||
[{:keys [::label ::rpc/climit] :as cfg} f]
|
||||
(reduce (fn [handler [limit-id limit-key]]
|
||||
(if-let [config (get-config climit limit-id)]
|
||||
(let [config (-> config
|
||||
(assoc ::key limit-key)
|
||||
(assoc ::label label))]
|
||||
(fn [cfg params]
|
||||
(let [limiter (cache/get cache ckey (partial create-limiter config))
|
||||
handler (partial handler cfg)]
|
||||
(invoke limiter metrics limit-id limit-key label handler params)))
|
||||
(do
|
||||
(l/wrn :hint "config not found" :label label :id limit-id)
|
||||
f)))
|
||||
f
|
||||
(get-limits cfg))))
|
||||
(let [config (assoc config ::params params)]
|
||||
(invoke climit config (partial handler cfg params)))))
|
||||
(do
|
||||
(l/wrn :hint "config not found" :label label :id limit-id)
|
||||
f)))
|
||||
f
|
||||
(get-limits cfg)))
|
||||
|
||||
(defn invoke!
|
||||
"Run a function in context of climit.
|
||||
Intended to be used in virtual threads."
|
||||
[{:keys [::executor] :as cfg} f params]
|
||||
(let [f (if (some? executor)
|
||||
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
|
||||
f)
|
||||
f (build-exec-chain cfg f)]
|
||||
[{:keys [::executor ::rpc/climit] :as cfg} f params]
|
||||
(let [f (if climit
|
||||
(let [f (if (some? executor)
|
||||
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
|
||||
f)]
|
||||
(build-exec-chain cfg f))
|
||||
f)]
|
||||
(f cfg params)))
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.rpc.commands.access-token
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.main :as-alias main]
|
||||
@@ -16,8 +16,7 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.util.time :as dt]))
|
||||
|
||||
(defn- decode-row
|
||||
[row]
|
||||
@@ -31,20 +30,19 @@
|
||||
:tid token-id
|
||||
:iat created-at})
|
||||
|
||||
expires-at (some-> expiration dt/in-future)]
|
||||
expires-at (some-> expiration dt/in-future)
|
||||
token (db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})]
|
||||
(decode-row token)))
|
||||
|
||||
(db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})))
|
||||
|
||||
|
||||
(defn repl-create-access-token
|
||||
(defn repl:create-access-token
|
||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [props (:app.setup/props system)]
|
||||
@@ -53,40 +51,38 @@
|
||||
name
|
||||
expiration))))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::expiration ::dt/duration)
|
||||
|
||||
(s/def ::create-access-token
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::name]
|
||||
:opt-un [::expiration]))
|
||||
(def ^:private schema:create-access-token
|
||||
[:map {:title "create-access-token"}
|
||||
[:name [:string {:max 250 :min 1}]]
|
||||
[:expiration {:optional true} ::dt/duration]])
|
||||
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::db/conn conn)]
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
(-> (create-access-token cfg profile-id name expiration)
|
||||
(decode-row)))))
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:create-access-token}
|
||||
[cfg {:keys [::rpc/profile-id name expiration]}]
|
||||
|
||||
(s/def ::delete-access-token
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::us/id]))
|
||||
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration))
|
||||
|
||||
(def ^:private schema:delete-access-token
|
||||
[:map {:title "delete-access-token"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::delete-access-token
|
||||
{::doc/added "1.18"}
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:delete-access-token}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||
(db/delete! pool :access-token {:id id :profile-id profile-id})
|
||||
nil)
|
||||
|
||||
(s/def ::get-access-tokens
|
||||
(s/keys :req [::rpc/profile-id]))
|
||||
(def ^:private schema:get-access-tokens
|
||||
[:map {:title "get-access-tokens"}])
|
||||
|
||||
(sv/defmethod ::get-access-tokens
|
||||
{::doc/added "1.18"}
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:get-access-tokens}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
||||
(->> (db/query pool :access-token
|
||||
{:profile-id profile-id}
|
||||
|
||||
@@ -27,9 +27,11 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.welcome-file :refer [create-welcome-file]]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def schema:password
|
||||
@@ -180,10 +182,11 @@
|
||||
(defn- validate-register-attempt!
|
||||
[cfg params]
|
||||
|
||||
(when-not (contains? cf/flags :registration)
|
||||
(when-not (contains? params :invitation-token)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled)))
|
||||
(when (or
|
||||
(not (contains? cf/flags :registration))
|
||||
(not (contains? cf/flags :login-with-password)))
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
|
||||
(when (contains? params :invitation-token)
|
||||
(let [invitation (tokens/verify (::setup/props cfg)
|
||||
@@ -240,6 +243,7 @@
|
||||
|
||||
params (d/without-nils params)
|
||||
token (tokens/generate (::setup/props cfg) params)]
|
||||
|
||||
(with-meta {:token token}
|
||||
{::audit/profile-id uuid/zero})))
|
||||
|
||||
@@ -282,6 +286,7 @@
|
||||
is-demo (:is-demo params false)
|
||||
is-muted (:is-muted params false)
|
||||
is-active (:is-active params false)
|
||||
theme (:theme params nil)
|
||||
email (str/lower email)
|
||||
|
||||
params {:id id
|
||||
@@ -292,6 +297,7 @@
|
||||
:password password
|
||||
:deleted-at (:deleted-at params)
|
||||
:props props
|
||||
:theme theme
|
||||
:is-active is-active
|
||||
:is-muted is-muted
|
||||
:is-demo is-demo}]
|
||||
@@ -347,30 +353,45 @@
|
||||
:extra-data ptoken})))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}]
|
||||
(let [claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}]
|
||||
(let [theme (when (= theme "light") theme)
|
||||
claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register})
|
||||
params (-> claims
|
||||
(into params)
|
||||
(assoc :fullname fullname))
|
||||
(assoc :fullname fullname)
|
||||
(assoc :theme theme))
|
||||
|
||||
profile (if-let [profile-id (:profile-id claims)]
|
||||
(profile/get-profile conn profile-id)
|
||||
(let [is-active (or (boolean (:is-active claims))
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password #(profile/derive-password cfg %)))]
|
||||
(->> (create-profile! conn params)
|
||||
(create-profile-rels! conn))))
|
||||
;; NOTE: we first try to match existing profile
|
||||
;; by email, that in normal circumstances will
|
||||
;; not return anything, but when a user tries to
|
||||
;; reuse the same token multiple times, we need
|
||||
;; to detect if the profile is already registered
|
||||
(or (profile/get-profile-by-email conn (:email claims))
|
||||
(let [is-active (or (boolean (:is-active claims))
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password #(profile/derive-password cfg %)))
|
||||
profile (->> (create-profile! conn params)
|
||||
(create-profile-rels! conn))]
|
||||
(vary-meta profile assoc :created true))))
|
||||
|
||||
;; When no profile-id comes on claims means a new register
|
||||
created? (not (:profile-id claims))
|
||||
created? (-> profile meta :created true?)
|
||||
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation}))
|
||||
|
||||
props (audit/profile->props profile)]
|
||||
props (-> (audit/profile->props profile)
|
||||
(assoc :from-invitation (some? invitation)))
|
||||
|
||||
|
||||
create-welcome-file-when-needed
|
||||
(fn []
|
||||
(when (:create-welcome-file params)
|
||||
(let [cfg (dissoc cfg ::db/conn)]
|
||||
(wrk/submit! executor (create-welcome-file cfg profile)))))]
|
||||
(cond
|
||||
;; When profile is blocked, we just ignore it and return plain data
|
||||
(:is-blocked profile)
|
||||
@@ -407,6 +428,7 @@
|
||||
(if (:is-active profile)
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-defer create-welcome-file-when-needed)
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "login"}
|
||||
@@ -416,19 +438,21 @@
|
||||
(when-not (eml/has-reports? conn (:email profile))
|
||||
(send-email-verification! cfg profile))
|
||||
|
||||
(rph/with-meta {:email (:email profile)}
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "email-verification"}
|
||||
::audit/profile-id (:id profile)})))
|
||||
(-> {:email (:email profile)}
|
||||
(rph/with-defer create-welcome-file-when-needed)
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
::audit/context {:action "email-verification"}
|
||||
::audit/profile-id (:id profile)}))))
|
||||
|
||||
:else
|
||||
(let [elapsed? (elapsed-verify-threshold? profile)
|
||||
complaints? (eml/has-reports? conn (:email profile))
|
||||
action (if complaints?
|
||||
"ignore-because-complaints"
|
||||
(if elapsed?
|
||||
"resend-email-verification"
|
||||
"ignore"))]
|
||||
(let [elapsed? (elapsed-verify-threshold? profile)
|
||||
reports? (eml/has-reports? conn (:email profile))
|
||||
action (if reports?
|
||||
"ignore-because-complaints"
|
||||
(if elapsed?
|
||||
"resend-email-verification"
|
||||
"ignore"))]
|
||||
|
||||
(l/wrn :hint "repeated registry detected"
|
||||
:profile-id (str (:id profile))
|
||||
@@ -450,7 +474,9 @@
|
||||
(def schema:register-profile
|
||||
[:map {:title "register-profile"}
|
||||
[:token schema:token]
|
||||
[:fullname [::sm/word-string {:max 100}]]])
|
||||
[:fullname [::sm/word-string {:max 100}]]
|
||||
[:theme {:optional true} [:string {:max 10}]]
|
||||
[:create-welcome-file {:optional true} :boolean]])
|
||||
|
||||
(sv/defmethod ::register-profile
|
||||
{::rpc/auth false
|
||||
@@ -522,7 +548,6 @@
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn)))))))
|
||||
|
||||
|
||||
(def schema:request-profile-recovery
|
||||
[:map {:title "request-profile-recovery"}
|
||||
[:email ::sm/email]])
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:refer-clojure :exclude [assert])
|
||||
(:require
|
||||
[app.binfile.v1 :as bf.v1]
|
||||
[app.binfile.v3 :as bf.v3]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
@@ -24,51 +25,103 @@
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[promesa.exec :as px]
|
||||
[ring.response :as rres]))
|
||||
[yetti.response :as yres]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
;; --- Command: export-binfile
|
||||
|
||||
(def ^:private schema:export-binfile
|
||||
(def ^:private
|
||||
schema:export-binfile
|
||||
[:map {:title "export-binfile"}
|
||||
[:name :string]
|
||||
[:name [:string {:max 250}]]
|
||||
[:file-id ::sm/uuid]
|
||||
[:include-libraries :boolean]
|
||||
[:embed-assets :boolean]])
|
||||
[:version {:optional true} ::sm/int]
|
||||
[:include-libraries ::sm/boolean]
|
||||
[:embed-assets ::sm/boolean]])
|
||||
|
||||
(defn stream-export-v1
|
||||
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
|
||||
(yres/stream-body
|
||||
(fn [_ output-stream]
|
||||
(try
|
||||
(-> cfg
|
||||
(assoc ::bf.v1/ids #{file-id})
|
||||
(assoc ::bf.v1/embed-assets embed-assets)
|
||||
(assoc ::bf.v1/include-libraries include-libraries)
|
||||
(bf.v1/export-files! output-stream))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "exception on exporting file"
|
||||
:file-id (str file-id)
|
||||
:cause cause))))))
|
||||
|
||||
(defn stream-export-v3
|
||||
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
|
||||
(yres/stream-body
|
||||
(fn [_ output-stream]
|
||||
(try
|
||||
(-> cfg
|
||||
(assoc ::bf.v3/ids #{file-id})
|
||||
(assoc ::bf.v3/embed-assets embed-assets)
|
||||
(assoc ::bf.v3/include-libraries include-libraries)
|
||||
(bf.v3/export-files! output-stream))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "exception on exporting file"
|
||||
:file-id (str file-id)
|
||||
:cause cause))))))
|
||||
|
||||
(sv/defmethod ::export-binfile
|
||||
"Export a penpot file in a binary format."
|
||||
{::doc/added "1.15"
|
||||
::webhooks/event? true
|
||||
::sm/result schema:export-binfile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries embed-assets] :as params}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id version file-id] :as params}]
|
||||
(files/check-read-permissions! pool profile-id file-id)
|
||||
(fn [_]
|
||||
{::rres/status 200
|
||||
::rres/headers {"content-type" "application/octet-stream"}
|
||||
::rres/body (reify rres/StreamableResponseBody
|
||||
(-write-body-to-stream [_ _ output-stream]
|
||||
(try
|
||||
(-> cfg
|
||||
(assoc ::bf.v1/ids #{file-id})
|
||||
(assoc ::bf.v1/embed-assets embed-assets)
|
||||
(assoc ::bf.v1/include-libraries include-libraries)
|
||||
(bf.v1/export-files! output-stream))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "exception on exporting file"
|
||||
:file-id (str file-id)
|
||||
:cause cause)))))}))
|
||||
(let [version (or version 1)
|
||||
body (case (int version)
|
||||
1 (stream-export-v1 cfg params)
|
||||
2 (throw (ex-info "not-implemented" {}))
|
||||
3 (stream-export-v3 cfg params))]
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/octet-stream"}
|
||||
::yres/body body})))
|
||||
|
||||
;; --- Command: import-binfile
|
||||
|
||||
(defn- import-binfile-v1
|
||||
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
|
||||
(let [cfg (-> cfg
|
||||
(assoc ::bf.v1/project-id project-id)
|
||||
(assoc ::bf.v1/profile-id profile-id)
|
||||
(assoc ::bf.v1/name name)
|
||||
(assoc ::bf.v1/input (:path file)))]
|
||||
|
||||
;; NOTE: the importation process performs some operations that are
|
||||
;; not very friendly with virtual threads, and for avoid
|
||||
;; unexpected blocking of other concurrent operations we dispatch
|
||||
;; that operation to a dedicated executor.
|
||||
(px/invoke! executor (partial bf.v1/import-files! cfg))))
|
||||
|
||||
(defn- import-binfile-v3
|
||||
[{:keys [::wrk/executor] :as cfg} {:keys [project-id profile-id name file]}]
|
||||
(let [cfg (-> cfg
|
||||
(assoc ::bf.v3/project-id project-id)
|
||||
(assoc ::bf.v3/profile-id profile-id)
|
||||
(assoc ::bf.v3/name name)
|
||||
(assoc ::bf.v3/input (:path file)))]
|
||||
;; NOTE: the importation process performs some operations that are
|
||||
;; not very friendly with virtual threads, and for avoid
|
||||
;; unexpected blocking of other concurrent operations we dispatch
|
||||
;; that operation to a dedicated executor.
|
||||
(px/invoke! executor (partial bf.v3/import-files! cfg))))
|
||||
|
||||
(defn- import-binfile
|
||||
[{:keys [::wrk/executor ::bf.v1/project-id ::db/pool] :as cfg} input]
|
||||
;; NOTE: the importation process performs some operations that
|
||||
;; are not very friendly with virtual threads, and for avoid
|
||||
;; unexpected blocking of other concurrent operations we
|
||||
;; dispatch that operation to a dedicated executor.
|
||||
(let [result (px/invoke! executor (partial bf.v1/import-files! cfg input))]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [project-id version] :as params}]
|
||||
(let [result (case (int version)
|
||||
1 (import-binfile-v1 cfg params)
|
||||
3 (import-binfile-v3 cfg params))]
|
||||
(db/update! pool :project
|
||||
{:modified-at (dt/now)}
|
||||
{:id project-id})
|
||||
@@ -76,8 +129,10 @@
|
||||
|
||||
(def ^:private schema:import-binfile
|
||||
[:map {:title "import-binfile"}
|
||||
[:name :string]
|
||||
[:name [:or [:string {:max 250}]
|
||||
[:map-of ::sm/uuid [:string {:max 250}]]]]
|
||||
[:project-id ::sm/uuid]
|
||||
[:version {:optional true} ::sm/int]
|
||||
[:file ::media/upload]])
|
||||
|
||||
(sv/defmethod ::import-binfile
|
||||
@@ -86,12 +141,11 @@
|
||||
::webhooks/event? true
|
||||
::sse/stream? true
|
||||
::sm/params schema:import-binfile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name project-id file] :as params}]
|
||||
(projects/check-read-permissions! pool profile-id project-id)
|
||||
(let [cfg (-> cfg
|
||||
(assoc ::bf.v1/project-id project-id)
|
||||
(assoc ::bf.v1/profile-id profile-id)
|
||||
(assoc ::bf.v1/name name))]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id version] :as params}]
|
||||
(projects/check-edition-permissions! pool profile-id project-id)
|
||||
(let [params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :version (or version 1)))]
|
||||
(with-meta
|
||||
(sse/response #(import-binfile cfg (:path file)))
|
||||
(sse/response (partial import-binfile cfg params))
|
||||
{::audit/props {:file nil}})))
|
||||
|
||||
@@ -71,10 +71,15 @@
|
||||
[conn comment-id & {:as opts}]
|
||||
(db/get-by-id conn :comment comment-id opts))
|
||||
|
||||
(def ^:private sql:get-next-seqn
|
||||
"SELECT (f.comment_thread_seqn + 1) AS next_seqn
|
||||
FROM file AS f
|
||||
WHERE f.id = ?
|
||||
FOR UPDATE")
|
||||
|
||||
(defn- get-next-seqn
|
||||
[conn file-id]
|
||||
(let [sql "select (f.comment_thread_seqn + 1) as next_seqn from file as f where f.id = ?"
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(let [res (db/exec-one! conn [sql:get-next-seqn file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(def sql:upsert-comment-thread-status
|
||||
@@ -292,7 +297,7 @@
|
||||
[:map {:title "create-comment-thread"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:position ::gpt/point]
|
||||
[:content :string]
|
||||
[:content [:string {:max 750}]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
@@ -304,38 +309,43 @@
|
||||
::rtry/when rtry/conflict-exception?
|
||||
::sm/params schema:create-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-comment-permissions! cfg profile-id file-id share-id)
|
||||
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
|
||||
(let [{:keys [team-id project-id page-name]} (get-file cfg file-id page-id)]
|
||||
|
||||
(run! (partial quotes/check-quote! cfg)
|
||||
(list {::quotes/id ::quotes/comment-threads-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id}))
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(assoc ::quotes/project-id project-id)
|
||||
(assoc ::quotes/file-id file-id)
|
||||
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
|
||||
{::quotes/id ::quotes/comments-per-file}))
|
||||
|
||||
(create-comment-thread conn {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id})))))
|
||||
(let [params {:created-at request-at
|
||||
:profile-id profile-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id}
|
||||
thread (db/tx-run! cfg create-comment-thread params)]
|
||||
|
||||
(vary-meta thread assoc ::audit/props thread))))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query
|
||||
;; because we need to lock the file for avoid race conditions
|
||||
|
||||
;; FIXME: this method touches and locks the file table,which
|
||||
;; is already heavy-update tablel; we need to think on move
|
||||
;; the sequence state management to a different table or
|
||||
;; different storage (example: redis) for alivate the update
|
||||
;; pression on the file table
|
||||
|
||||
(let [;; NOTE: we take the next seq number from a separate query because the whole
|
||||
;; operation can be retried on conflict, and in this case the new seq shold be
|
||||
;; retrieved from the database.
|
||||
seqn (get-next-seqn conn file-id)
|
||||
thread-id (uuid/next)
|
||||
thread (db/insert! conn :comment-thread
|
||||
@@ -364,7 +374,8 @@
|
||||
;; Optimistic update of current seq number on file.
|
||||
(db/update! conn :file
|
||||
{:comment-thread-seqn seqn}
|
||||
{:id file-id})
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(-> thread
|
||||
(select-keys [:id :file-id :page-id])
|
||||
@@ -387,7 +398,6 @@
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(upsert-comment-thread-status! conn profile-id id)))))
|
||||
|
||||
|
||||
;; --- COMMAND: Update Comment Thread
|
||||
|
||||
(def ^:private
|
||||
@@ -418,7 +428,7 @@
|
||||
schema:create-comment
|
||||
[:map {:title "create-comment"}
|
||||
[:thread-id ::sm/uuid]
|
||||
[:content :string]
|
||||
[:content [:string {:max 250}]]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::create-comment
|
||||
@@ -432,12 +442,11 @@
|
||||
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
|
||||
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
(quotes/check! cfg {::quotes/id ::quotes/comments-per-file
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id
|
||||
::quotes/project-id project-id
|
||||
::quotes/file-id file-id})
|
||||
|
||||
;; Update the page-name cached attribute on comment thread table.
|
||||
(when (not= page-name (:page-name thread))
|
||||
@@ -477,7 +486,7 @@
|
||||
schema:update-comment
|
||||
[:map {:title "update-comment"}
|
||||
[:id ::sm/uuid]
|
||||
[:content :string]
|
||||
[:content [:string {:max 250}]]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::update-comment
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::create-demo-profile any?)
|
||||
[buddy.core.nonce :as bn]))
|
||||
|
||||
(sv/defmethod ::create-demo-profile
|
||||
"A command that is responsible of creating a demo purpose
|
||||
@@ -48,7 +45,7 @@
|
||||
params {:email email
|
||||
:fullname fullname
|
||||
:is-active true
|
||||
:deleted-at (dt/in-future cf/deletion-delay)
|
||||
:deleted-at (dt/in-future (cf/get-deletion-delay))
|
||||
:password (profile/derive-password cfg password)
|
||||
:props {}}]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user