mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
598 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae7e28b71b | ||
|
|
be30174a49 | ||
|
|
8373654f80 | ||
|
|
471c636580 | ||
|
|
635c6efe42 | ||
|
|
0b39318b33 | ||
|
|
47cecb2ac4 | ||
|
|
5d6ceec803 | ||
|
|
16cf16c422 | ||
|
|
4e1eee197e | ||
|
|
91c8af9e38 | ||
|
|
58593a9428 | ||
|
|
17cf57f7ca | ||
|
|
f7cfe36f37 | ||
|
|
c26f909565 | ||
|
|
6db7fe5f7b | ||
|
|
a207114d95 | ||
|
|
b8299a5ea5 | ||
|
|
1fa461e996 | ||
|
|
2e3745099b | ||
|
|
6892cffe54 | ||
|
|
e0034dc205 | ||
|
|
bd9eab08b7 | ||
|
|
b5121657ee | ||
|
|
ca257d1caf | ||
|
|
e164692391 | ||
|
|
b58edea544 | ||
|
|
9a587c91a8 | ||
|
|
aae1571a5c | ||
|
|
ebaf30727c | ||
|
|
f5f255e2d5 | ||
|
|
e65c0d9f48 | ||
|
|
86c5ca4213 | ||
|
|
179d534237 | ||
|
|
162507264c | ||
|
|
7e0a8b6227 | ||
|
|
475d14edf4 | ||
|
|
979828ffe3 | ||
|
|
65bb795199 | ||
|
|
a0546b2e63 | ||
|
|
f291125377 | ||
|
|
0ce981a68c | ||
|
|
a8814dcaba | ||
|
|
229eeae6db | ||
|
|
d03788af93 | ||
|
|
017aad6454 | ||
|
|
767ec37b83 | ||
|
|
89f64e0c49 | ||
|
|
d108ad904e | ||
|
|
6564736d3e | ||
|
|
d01cd70c6b | ||
|
|
ea7768117c | ||
|
|
5bfb39cdf6 | ||
|
|
29f1c2bdad | ||
|
|
e79f9ba40f | ||
|
|
452aabdec6 | ||
|
|
860e32d965 | ||
|
|
495f9dfa84 | ||
|
|
133ca33cb5 | ||
|
|
1c69a9fd8a | ||
|
|
15faa57e01 | ||
|
|
f5510234cf | ||
|
|
5b0331611d | ||
|
|
f8f1c58f61 | ||
|
|
3f34aa92fa | ||
|
|
c99102e49b | ||
|
|
d583661e58 | ||
|
|
ffa326e08f | ||
|
|
03040ed40b | ||
|
|
5e89cd1cb3 | ||
|
|
bf202473e9 | ||
|
|
886c0c596f | ||
|
|
b15b394c65 | ||
|
|
caf78a6b4d | ||
|
|
6a161267ba | ||
|
|
a180c33a32 | ||
|
|
ea8febdb7d | ||
|
|
f765cc8dbc | ||
|
|
81b7972347 | ||
|
|
1281670c61 | ||
|
|
b8c6103858 | ||
|
|
b2c0bed84c | ||
|
|
9619fcbc1f | ||
|
|
e9c55e9eb4 | ||
|
|
488d034a58 | ||
|
|
8d66275187 | ||
|
|
59063e861c | ||
|
|
9da891e9b0 | ||
|
|
a6de12323e | ||
|
|
52d099c80e | ||
|
|
b2010e5fd8 | ||
|
|
5808bd3743 | ||
|
|
d5f5c440dd | ||
|
|
729f679c0f | ||
|
|
9f52709a42 | ||
|
|
85444f5a47 | ||
|
|
50df2279a7 | ||
|
|
71ba0242c7 | ||
|
|
9fb91b3052 | ||
|
|
689aab32c9 | ||
|
|
c642f4afa2 | ||
|
|
a62a083294 | ||
|
|
2a13c2ec00 | ||
|
|
f3525b9ff2 | ||
|
|
d2509f4b97 | ||
|
|
6d8c424710 | ||
|
|
93e8657f73 | ||
|
|
91f6c001c0 | ||
|
|
0dd8300a80 | ||
|
|
c2b97b13a1 | ||
|
|
8feb5dabb0 | ||
|
|
8aaa04b1f8 | ||
|
|
a28117b301 | ||
|
|
0117a4767d | ||
|
|
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 | ||
|
|
8dea5d5158 | ||
|
|
7ea5c79393 | ||
|
|
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 | ||
|
|
c0cd980f5f | ||
|
|
2f99d17885 | ||
|
|
63ffa704f5 | ||
|
|
129b7afda9 | ||
|
|
1c09670e3c | ||
|
|
0db1eed87f | ||
|
|
db52d98595 | ||
|
|
e46b5b3f57 | ||
|
|
d1d3b4353a | ||
|
|
3c0944ebfc | ||
|
|
a2725ed8fe | ||
|
|
84bfef9938 | ||
|
|
6169f5c2e8 | ||
|
|
fc14871d01 | ||
|
|
bcfa1d4394 | ||
|
|
2361b2b63a | ||
|
|
d9a941cb32 | ||
|
|
f43ca1869b | ||
|
|
a34b06cfb4 | ||
|
|
f5b910d391 | ||
|
|
b57e68f4b6 | ||
|
|
d2311f066a | ||
|
|
aa39de4ea8 | ||
|
|
215f6fc0ab | ||
|
|
fc333ae098 | ||
|
|
d3e891eec3 | ||
|
|
66d30e35c7 | ||
|
|
b8693c3f85 | ||
|
|
4b2742efca | ||
|
|
c1435fba95 | ||
|
|
b13148265e | ||
|
|
c0174ab501 | ||
|
|
77c45ed109 | ||
|
|
7df68bb8bd | ||
|
|
ab461ba560 | ||
|
|
280252d40e | ||
|
|
09b6989491 | ||
|
|
f96bbb38b0 | ||
|
|
f0e3dc2f0e | ||
|
|
d2937a76d9 | ||
|
|
3219c150d4 | ||
|
|
ba167f256b | ||
|
|
0e92bcc0de | ||
|
|
f6bfe3931c | ||
|
|
253b9e5bd8 | ||
|
|
604f80de20 | ||
|
|
0aa8f7bfc6 | ||
|
|
4cc2c736e9 | ||
|
|
9455b56c07 | ||
|
|
d6f528acd2 | ||
|
|
9344fb958a | ||
|
|
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 |
@@ -22,10 +22,10 @@ jobs:
|
||||
|
||||
# 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-
|
||||
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
|
||||
@@ -58,6 +58,7 @@ jobs:
|
||||
command: |
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
yarn run fmt:js:check
|
||||
|
||||
- run:
|
||||
name: "common linter check"
|
||||
@@ -107,7 +108,7 @@ jobs:
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run compile
|
||||
yarn run build:app:assets
|
||||
clojure -M:dev:shadow-cljs release main
|
||||
yarn playwright install --with-deps chromium
|
||||
yarn e2e:test
|
||||
@@ -125,7 +126,6 @@ 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" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:remove-consecutive-blank-lines? false
|
||||
:extra-indents {rumext.v2/fnc [[:inner 0]]
|
||||
cljs.test/async [[:inner 0]]
|
||||
app.common.schema/register! [[:inner 0] [:inner 1]]
|
||||
promesa.exec/thread [[:inner 0]]
|
||||
specify! [[:inner 0] [:inner 1]]}
|
||||
}
|
||||
|
||||
3
.gitignore
vendored
3
.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,7 +70,6 @@
|
||||
/web
|
||||
clj-profiler/
|
||||
node_modules
|
||||
frontend/.storybook/preview-body.html
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
|
||||
115
CHANGES.md
115
CHANGES.md
@@ -1,10 +1,121 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.1.2
|
||||
## 2.2.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Don't allow registry with email and password, if password login is disabled (invitation workflow) [Github #4975](https://github.com/penpot/penpot/issues/4975)
|
||||
- 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)
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -50,6 +50,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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
518
backend/resources/app/email/join-team/en.html
Normal file
518
backend/resources/app/email/join-team/en.html
Normal file
@@ -0,0 +1,518 @@
|
||||
<!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>
|
||||
<!--[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://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>
|
||||
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.
|
||||
@@ -0,0 +1,528 @@
|
||||
<!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>
|
||||
<!--[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://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>
|
||||
@@ -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,551 @@
|
||||
<!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>
|
||||
<!--[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://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>
|
||||
@@ -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.
|
||||
568
backend/resources/app/email/request-file-access/en.html
Normal file
568
backend/resources/app/email/request-file-access/en.html
Normal file
@@ -0,0 +1,568 @@
|
||||
<!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>
|
||||
<!--[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://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>
|
||||
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.
|
||||
526
backend/resources/app/email/request-team-access/en.html
Normal file
526
backend/resources/app/email/request-team-access/en.html
Normal file
@@ -0,0 +1,526 @@
|
||||
<!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>
|
||||
<!--[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://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>
|
||||
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/main/wireframing-kit.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/main/prototype-examples.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/main/Penpot-Design-system.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/main/Flex%20Layout%20Playground.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>
|
||||
|
||||
@@ -24,8 +24,10 @@ export PENPOT_FLAGS="\
|
||||
enable-rpc-climit \
|
||||
enable-rpc-rlimit \
|
||||
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,10 @@ 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 OPTIONS="
|
||||
-A:jmx-remote -A:dev \
|
||||
|
||||
@@ -17,7 +17,9 @@ export PENPOT_FLAGS="\
|
||||
disable-secure-session-cookies \
|
||||
enable-rpc-climit \
|
||||
enable-smtp \
|
||||
enable-file-snapshot \
|
||||
enable-access-tokens \
|
||||
enable-tiered-file-data-storage \
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation";
|
||||
|
||||
@@ -55,9 +57,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};
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[clj-ldap.client :as ldap]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string]
|
||||
@@ -104,17 +103,17 @@
|
||||
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)
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::port ::us/integer)
|
||||
(s/def ::ssl ::us/boolean)
|
||||
(s/def ::tls ::us/boolean)
|
||||
(s/def ::query ::us/string)
|
||||
(s/def ::base-dn ::us/string)
|
||||
(s/def ::bind-dn ::us/string)
|
||||
(s/def ::bind-password ::us/string)
|
||||
(s/def ::attrs-email ::us/string)
|
||||
(s/def ::attrs-fullname ::us/string)
|
||||
(s/def ::attrs-username ::us/string)
|
||||
|
||||
(s/def ::provider-params
|
||||
(s/keys :opt-un [::host ::port
|
||||
@@ -126,6 +125,7 @@
|
||||
::attrs-email
|
||||
::attrs-username
|
||||
::attrs-fullname]))
|
||||
|
||||
(s/def ::provider
|
||||
(s/nilable ::provider-params))
|
||||
|
||||
|
||||
@@ -567,7 +567,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)})]
|
||||
@@ -656,17 +655,17 @@
|
||||
: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)
|
||||
(s/def ::client-id ::us/string)
|
||||
(s/def ::client-secret ::us/string)
|
||||
(s/def ::base-uri ::us/string)
|
||||
(s/def ::token-uri ::us/string)
|
||||
(s/def ::auth-uri ::us/string)
|
||||
(s/def ::user-uri ::us/string)
|
||||
(s/def ::scopes ::us/set-of-strings)
|
||||
(s/def ::roles ::us/set-of-strings)
|
||||
(s/def ::roles-attr ::us/string)
|
||||
(s/def ::email-attr ::us/string)
|
||||
(s/def ::name-attr ::us/string)
|
||||
|
||||
(s/def ::provider
|
||||
(s/keys :req-un [::client-id
|
||||
|
||||
@@ -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]
|
||||
@@ -403,9 +402,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)
|
||||
@@ -620,8 +619,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*))]
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -347,9 +346,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"
|
||||
@@ -390,7 +387,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"
|
||||
|
||||
@@ -11,30 +11,17 @@
|
||||
[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))
|
||||
@@ -45,18 +32,19 @@
|
||||
(d/without-nils data)
|
||||
data))
|
||||
|
||||
(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,151 @@
|
||||
;; 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} :string]
|
||||
[: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]
|
||||
|
||||
(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} :string]
|
||||
|
||||
::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} :string]
|
||||
[: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} :string]
|
||||
[:objects-storage-s3-io-threads {:optional true} ::sm/int]]))
|
||||
|
||||
(def default-flags
|
||||
[:enable-backend-api-doc
|
||||
@@ -367,20 +252,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 +275,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})))
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
(s/def ::conn some?)
|
||||
(s/def ::nilable-pool (s/nilable ::pool))
|
||||
(s/def ::pool pool?)
|
||||
(s/def ::pool-or-conn some?)
|
||||
(s/def ::connectable some?)
|
||||
|
||||
(defn closed?
|
||||
[pool]
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
(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.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[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.email.join-team :as-alias email.join-team]
|
||||
[app.email.request-team-access :as-alias email.request-team-access]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.template :as tmpl]
|
||||
[app.worker :as wrk]
|
||||
@@ -149,9 +153,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)))
|
||||
|
||||
@@ -273,32 +295,10 @@
|
||||
;; 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]
|
||||
@@ -401,6 +401,79 @@
|
||||
"Teams member invitation email."
|
||||
(template-factory ::invite-to-team))
|
||||
|
||||
|
||||
(s/def ::email.join-team/invited-by ::us/string)
|
||||
(s/def ::email.join-team/team ::us/string)
|
||||
(s/def ::email.join-team/team-id ::us/uuid)
|
||||
|
||||
(s/def ::join-team
|
||||
(s/keys :req-un [::email.join-team/invited-by
|
||||
::email.join-team/team-id
|
||||
::email.join-team/team]))
|
||||
|
||||
(def join-team
|
||||
"Teams member joined after request email."
|
||||
(template-factory ::join-team))
|
||||
|
||||
(s/def ::email.request-team-access/requested-by ::us/string)
|
||||
(s/def ::email.request-team-access/requested-by-email ::us/string)
|
||||
(s/def ::email.request-team-access/team-name ::us/string)
|
||||
(s/def ::email.request-team-access/team-id ::us/uuid)
|
||||
(s/def ::email.request-team-access/file-name ::us/string)
|
||||
(s/def ::email.request-team-access/file-id ::us/uuid)
|
||||
(s/def ::email.request-team-access/page-id ::us/uuid)
|
||||
|
||||
(s/def ::request-file-access
|
||||
(s/keys :req-un [::email.request-team-access/requested-by
|
||||
::email.request-team-access/requested-by-email
|
||||
::email.request-team-access/team-name
|
||||
::email.request-team-access/team-id
|
||||
::email.request-team-access/file-name
|
||||
::email.request-team-access/file-id
|
||||
::email.request-team-access/page-id]))
|
||||
|
||||
(def request-file-access
|
||||
"File access request email."
|
||||
(template-factory ::request-file-access))
|
||||
|
||||
|
||||
(s/def ::request-file-access-yourpenpot
|
||||
(s/keys :req-un [::email.request-team-access/requested-by
|
||||
::email.request-team-access/requested-by-email
|
||||
::email.request-team-access/team-name
|
||||
::email.request-team-access/team-id
|
||||
::email.request-team-access/file-name
|
||||
::email.request-team-access/file-id
|
||||
::email.request-team-access/page-id]))
|
||||
|
||||
(def request-file-access-yourpenpot
|
||||
"File access on Your Penpot request email."
|
||||
(template-factory ::request-file-access-yourpenpot))
|
||||
|
||||
(s/def ::request-file-access-yourpenpot-view
|
||||
(s/keys :req-un [::email.request-team-access/requested-by
|
||||
::email.request-team-access/requested-by-email
|
||||
::email.request-team-access/team-name
|
||||
::email.request-team-access/team-id
|
||||
::email.request-team-access/file-name
|
||||
::email.request-team-access/file-id
|
||||
::email.request-team-access/page-id]))
|
||||
|
||||
(def request-file-access-yourpenpot-view
|
||||
"File access on Your Penpot view mode request email."
|
||||
(template-factory ::request-file-access-yourpenpot-view))
|
||||
|
||||
(s/def ::request-team-access
|
||||
(s/keys :req-un [::email.request-team-access/requested-by
|
||||
::email.request-team-access/requested-by-email
|
||||
::email.request-team-access/team-name
|
||||
::email.request-team-access/team-id]))
|
||||
|
||||
(def request-team-access
|
||||
"Team access request email."
|
||||
(template-factory ::request-team-access))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; BOUNCE/COMPLAINS HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -62,6 +62,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 +114,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 +135,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))
|
||||
@@ -1742,7 +1743,7 @@
|
||||
: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)
|
||||
|
||||
@@ -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,12 +64,26 @@
|
||||
;; 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 [fragment (db/get* system :file-data-fragment
|
||||
{:id id :file-id file-id}
|
||||
{::sql/columns [:data]})]
|
||||
{::sql/columns [:data :data-backend :data-ref-id :id]})]
|
||||
|
||||
(l/trc :hint "load pointer"
|
||||
:file-id (str file-id)
|
||||
@@ -74,7 +97,9 @@
|
||||
:file-id file-id
|
||||
:fragment-id id))
|
||||
|
||||
(blob/decode (:data fragment))))
|
||||
(let [data (get-file-data system fragment)]
|
||||
;; FIXME: conditional thread scheduling for decoding big objects
|
||||
(blob/decode data))))
|
||||
|
||||
(defn persist-pointers!
|
||||
"Persist all currently tracked pointer objects"
|
||||
|
||||
@@ -57,11 +57,10 @@
|
||||
(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."
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
(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]
|
||||
@@ -39,16 +41,6 @@
|
||||
(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]
|
||||
@@ -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))))
|
||||
@@ -113,6 +105,12 @@
|
||||
|
||||
(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]
|
||||
@@ -134,10 +132,11 @@
|
||||
(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)))
|
||||
|
||||
(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}]
|
||||
|
||||
@@ -263,6 +263,8 @@
|
||||
(assoc ::wrk/dedupe dedupe?)
|
||||
(assoc ::wrk/label label)
|
||||
(assoc ::wrk/params (-> params
|
||||
(dissoc :source)
|
||||
(dissoc :context)
|
||||
(dissoc :ip-addr)
|
||||
(dissoc :type)))))))
|
||||
params))
|
||||
|
||||
@@ -66,21 +66,18 @@
|
||||
(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)
|
||||
|
||||
@@ -344,6 +344,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)
|
||||
@@ -394,9 +396,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)
|
||||
@@ -448,19 +458,28 @@
|
||||
::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 [::assets :app.storage.s3/backend])
|
||||
: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))}
|
||||
|
||||
:app.storage.fs/backend
|
||||
{::sto.fs/directory (or (cf/get :storage-assets-fs-directory)
|
||||
(cf/get :objects-storage-fs-directory))}})
|
||||
|
||||
|
||||
(def worker-config
|
||||
@@ -487,7 +506,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}
|
||||
@@ -526,6 +545,7 @@
|
||||
|
||||
(defn start
|
||||
[]
|
||||
(cf/validate!)
|
||||
(ig/load-namespaces (merge system-config worker-config))
|
||||
(alter-var-root #'system (fn [sys]
|
||||
(when sys (ig/halt! sys))
|
||||
|
||||
@@ -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,22 +46,10 @@
|
||||
(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
|
||||
(sm/register! ::upload
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size :int]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
@@ -326,17 +313,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))))
|
||||
|
||||
@@ -382,7 +382,37 @@
|
||||
: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")}])
|
||||
: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")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -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;
|
||||
@@ -178,38 +178,21 @@
|
||||
(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 $
|
||||
@@ -220,7 +203,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)))
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -44,7 +43,7 @@
|
||||
: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,16 +52,14 @@
|
||||
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"}
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:create-access-token}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::db/conn conn)]
|
||||
@@ -72,21 +69,23 @@
|
||||
(-> (create-access-token cfg profile-id name expiration)
|
||||
(decode-row)))))
|
||||
|
||||
(s/def ::delete-access-token
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::us/id]))
|
||||
(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
|
||||
@@ -241,6 +243,7 @@
|
||||
|
||||
params (d/without-nils params)
|
||||
token (tokens/generate (::setup/props cfg) params)]
|
||||
|
||||
(with-meta {:token token}
|
||||
{::audit/profile-id uuid/zero})))
|
||||
|
||||
@@ -350,7 +353,7 @@
|
||||
:extra-data ptoken})))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [::db/conn] :as cfg} {:keys [token fullname theme] :as params}]
|
||||
[{: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
|
||||
@@ -380,8 +383,13 @@
|
||||
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)
|
||||
|
||||
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)
|
||||
@@ -418,6 +426,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"}
|
||||
@@ -427,10 +436,12 @@
|
||||
(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)
|
||||
@@ -462,7 +473,8 @@
|
||||
[:map {:title "register-profile"}
|
||||
[:token schema:token]
|
||||
[:fullname [::sm/word-string {:max 100}]]
|
||||
[:theme {:optional true} [:string {:max 10}]]])
|
||||
[:theme {:optional true} [:string {:max 10}]]
|
||||
[:create-welcome-file {:optional true} :boolean]])
|
||||
|
||||
(sv/defmethod ::register-profile
|
||||
{::rpc/auth false
|
||||
@@ -534,7 +546,6 @@
|
||||
(create-recovery-token)
|
||||
(send-email-notification conn)))))))
|
||||
|
||||
|
||||
(def schema:request-profile-recovery
|
||||
[:map {:title "request-profile-recovery"}
|
||||
[:email ::sm/email]])
|
||||
|
||||
@@ -30,9 +30,10 @@
|
||||
|
||||
;; --- 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]])
|
||||
@@ -74,9 +75,10 @@
|
||||
{:id project-id})
|
||||
result))
|
||||
|
||||
(def ^:private schema:import-binfile
|
||||
(def ^:private
|
||||
schema:import-binfile
|
||||
[:map {:title "import-binfile"}
|
||||
[:name :string]
|
||||
[:name [:string {:max 250}]]
|
||||
[:project-id ::sm/uuid]
|
||||
[:file ::media/upload]])
|
||||
|
||||
|
||||
@@ -292,7 +292,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]]])
|
||||
@@ -418,7 +418,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
|
||||
@@ -477,7 +477,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 {}}]
|
||||
|
||||
|
||||
@@ -8,29 +8,25 @@
|
||||
"A general purpose feedback module."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.email :as eml]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(declare ^:private send-feedback!)
|
||||
|
||||
(s/def ::content ::us/string)
|
||||
(s/def ::from ::us/email)
|
||||
(s/def ::subject ::us/string)
|
||||
|
||||
(s/def ::send-user-feedback
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::subject
|
||||
::content]))
|
||||
(def ^:private schema:send-user-feedback
|
||||
[:map {:title "send-user-feedback"}
|
||||
[:subject [:string {:max 250}]]
|
||||
[:content [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::send-user-feedback
|
||||
{::doc/added "1.18"}
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:send-user-feedback}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
|
||||
(when-not (contains? cf/flags :user-feedback)
|
||||
(ex/raise :type :restriction
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.desc-js-like :as-alias smdj]
|
||||
[app.common.spec :as us]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.config :as cf]
|
||||
@@ -36,7 +35,6 @@
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- FEATURES
|
||||
@@ -46,18 +44,6 @@
|
||||
(when media-id
|
||||
(str (cf/get :public-uri) "/assets/by-id/" media-id)))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
(s/def ::features ::us/set-of-strings)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::frame-id ::us/uuid)
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::is-shared ::us/boolean)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::search-term ::us/string)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def long-cache-duration
|
||||
@@ -82,6 +68,9 @@
|
||||
:max-version fmg/version))
|
||||
file))
|
||||
|
||||
|
||||
;; --- FILE DATA
|
||||
|
||||
;; --- FILE PERMISSIONS
|
||||
|
||||
(def ^:private sql:file-permissions
|
||||
@@ -189,12 +178,12 @@
|
||||
[:map {:title "File"}
|
||||
[:id ::sm/uuid]
|
||||
[:features ::cfeat/features]
|
||||
[:has-media-trimmed :boolean]
|
||||
[:comment-thread-seqn {:min 0} :int]
|
||||
[:name :string]
|
||||
[:revn {:min 0} :int]
|
||||
[:has-media-trimmed ::sm/boolean]
|
||||
[:comment-thread-seqn [::sm/int {:min 0}]]
|
||||
[:name [:string {:max 250}]]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:modified-at ::dt/instant]
|
||||
[:is-shared :boolean]
|
||||
[:is-shared ::sm/boolean]
|
||||
[:project-id ::sm/uuid]
|
||||
[:created-at ::dt/instant]
|
||||
[:data {:optional true} :any]]))
|
||||
@@ -272,18 +261,19 @@
|
||||
(let [params (merge {:id id}
|
||||
(when (some? project-id)
|
||||
{:project-id project-id}))
|
||||
file (-> (db/get conn :file params
|
||||
{::db/check-deleted (not include-deleted?)
|
||||
::db/remove-deleted (not include-deleted?)
|
||||
::sql/for-update lock-for-update?})
|
||||
(decode-row))]
|
||||
file (->> (db/get conn :file params
|
||||
{::db/check-deleted (not include-deleted?)
|
||||
::db/remove-deleted (not include-deleted?)
|
||||
::sql/for-update lock-for-update?})
|
||||
(feat.fdata/resolve-file-data cfg)
|
||||
(decode-row))]
|
||||
(if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file)
|
||||
file)))
|
||||
|
||||
(defn get-minimal-file
|
||||
[cfg id & {:as opts}]
|
||||
(let [opts (assoc opts ::sql/columns [:id :modified-at :revn])]
|
||||
(let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :data-ref-id :data-backend])]
|
||||
(db/get cfg :file {:id id} opts)))
|
||||
|
||||
(defn get-file-etag
|
||||
@@ -342,8 +332,10 @@
|
||||
|
||||
(defn- get-file-fragment
|
||||
[cfg file-id fragment-id]
|
||||
(some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id})
|
||||
(update :data blob/decode)))
|
||||
(let [resolve-file-data (partial feat.fdata/resolve-file-data cfg)]
|
||||
(some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id})
|
||||
(resolve-file-data)
|
||||
(update :data blob/decode))))
|
||||
|
||||
(sv/defmethod ::get-file-fragment
|
||||
"Retrieve a file fragment by its ID. Only authenticated users."
|
||||
@@ -416,7 +408,7 @@
|
||||
"Checks if the file has libraries. Returns a boolean"
|
||||
{::doc/added "1.15.1"
|
||||
::sm/params schema:has-file-libraries
|
||||
::sm/result :boolean}
|
||||
::sm/result ::sm/boolean}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
@@ -495,7 +487,7 @@
|
||||
[:file-id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]
|
||||
[:object-id {:optional true} [:or ::sm/uuid ::sm/coll-of-uuid]]
|
||||
[:object-id {:optional true} [:or ::sm/uuid [::sm/set ::sm/uuid]]]
|
||||
[:features {:optional true} ::cfeat/features]])
|
||||
|
||||
(sv/defmethod ::get-page
|
||||
@@ -737,6 +729,23 @@
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(db/tx-run! cfg get-file-summary (assoc params :profile-id profile-id)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-file-info
|
||||
|
||||
(defn- get-file-info
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(db/get* conn :file
|
||||
{:id id}
|
||||
{::sql/columns [:id]}))
|
||||
|
||||
(sv/defmethod ::get-file-info
|
||||
"Retrieve minimal file info by its ID."
|
||||
{::rpc/auth false
|
||||
::doc/added "2.2.0"
|
||||
::sm/params schema:get-file}
|
||||
[cfg params]
|
||||
(db/tx-run! cfg get-file-info params))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MUTATION COMMANDS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -759,19 +768,19 @@
|
||||
[:map {:title "RenameFileEvent"}
|
||||
[:id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:name [:string {:max 250}]]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]
|
||||
|
||||
::sm/params
|
||||
[:map {:title "RenameFileParams"}
|
||||
[:name {:min 1} :string]
|
||||
[:name [:string {:min 1 :max 250}]]
|
||||
[:id ::sm/uuid]]
|
||||
|
||||
::sm/result
|
||||
[:map {:title "SimplifiedFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:name [:string {:max 250}]]
|
||||
[:created-at ::dt/instant]
|
||||
[:modified-at ::dt/instant]]}
|
||||
|
||||
@@ -816,7 +825,8 @@
|
||||
(db/update! cfg :file
|
||||
{:revn (inc (:revn file))
|
||||
:data (blob/encode (:data file))
|
||||
:modified-at (dt/now)}
|
||||
:modified-at (dt/now)
|
||||
:has-media-trimmed false}
|
||||
{:id file-id})
|
||||
|
||||
(feat.fdata/persist-pointers! cfg file-id))))
|
||||
@@ -907,7 +917,7 @@
|
||||
(sm/define
|
||||
[:map {:title "set-file-shared"}
|
||||
[:id ::sm/uuid]
|
||||
[:is-shared :boolean]]))
|
||||
[:is-shared ::sm/boolean]]))
|
||||
|
||||
(sv/defmethod ::set-file-shared
|
||||
{::doc/added "1.17"
|
||||
@@ -1045,14 +1055,16 @@
|
||||
{:id file-id}
|
||||
{::db/return-keys true}))
|
||||
|
||||
(s/def ::ignore-file-library-sync-status
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::date]))
|
||||
(def ^:private schema:ignore-file-library-sync-status
|
||||
[:map {:title "ignore-file-library-sync-status"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:date ::dt/instant]])
|
||||
|
||||
;; TODO: improve naming
|
||||
(sv/defmethod ::ignore-file-library-sync-status
|
||||
"Ignore updates in linked files"
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:ignore-file-library-sync-status}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
@@ -88,10 +88,10 @@
|
||||
|
||||
(def ^:private schema:create-file
|
||||
[:map {:title "create-file"}
|
||||
[:name :string]
|
||||
[:name [:string {:max 250}]]
|
||||
[:project-id ::sm/uuid]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:is-shared {:optional true} :boolean]
|
||||
[:is-shared {:optional true} ::sm/boolean]
|
||||
[:features {:optional true} ::cfeat/features]])
|
||||
|
||||
(sv/defmethod ::create-file
|
||||
|
||||
@@ -7,29 +7,24 @@
|
||||
(ns app.rpc.commands.files-share
|
||||
"Share link related rpc mutation methods."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::who-comment ::us/string)
|
||||
(s/def ::who-inspect ::us/string)
|
||||
(s/def ::pages (s/every ::us/uuid :kind set?))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; --- MUTATION: Create Share Link
|
||||
|
||||
(declare create-share-link)
|
||||
|
||||
(s/def ::create-share-link
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::who-comment ::who-inspect ::pages]))
|
||||
(def ^:private schema:create-share-link
|
||||
[:map {:title "create-share-link"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:who-comment [:string {:max 250}]]
|
||||
[:who-inspect [:string {:max 250}]]
|
||||
[:pages [:set ::sm/uuid]]])
|
||||
|
||||
(sv/defmethod ::create-share-link
|
||||
"Creates a share-link object.
|
||||
@@ -37,7 +32,8 @@
|
||||
Share links are resources that allows external users access to specific
|
||||
pages of a file with specific permissions (who-comment and who-inspect)."
|
||||
{::doc/added "1.18"
|
||||
::doc/module :files}
|
||||
::doc/module :files
|
||||
::sm/params schema:create-share-link}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
@@ -58,13 +54,14 @@
|
||||
|
||||
;; --- MUTATION: Delete Share Link
|
||||
|
||||
(s/def ::delete-share-link
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::us/id]))
|
||||
(def ^:private schema:delete-share-link
|
||||
[:map {:title "delete-share-link"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::delete-share-link
|
||||
{::doc/added "1.18"
|
||||
::doc/module ::files}
|
||||
::doc/module ::files
|
||||
::sm/params schema:delete-share-link}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [slink (db/get-by-id conn :share-link id)]
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.main :as-alias main]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -34,20 +36,21 @@
|
||||
:code :authentication-required
|
||||
:hint "only admins allowed")))
|
||||
|
||||
(def sql:get-file-snapshots
|
||||
"SELECT id, label, revn, created_at
|
||||
FROM file_change
|
||||
WHERE file_id = ?
|
||||
AND created_at < ?
|
||||
AND label IS NOT NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?")
|
||||
|
||||
(defn get-file-snapshots
|
||||
[{:keys [::db/conn]} {:keys [file-id limit start-at]
|
||||
:or {limit Long/MAX_VALUE}}]
|
||||
(let [query (str "select id, label, revn, created_at "
|
||||
" from file_change "
|
||||
" where file_id = ? "
|
||||
" and created_at < ? "
|
||||
" and data is not null "
|
||||
" order by created_at desc "
|
||||
" limit ?")
|
||||
start-at (or start-at (dt/now))
|
||||
(let [start-at (or start-at (dt/now))
|
||||
limit (min limit 20)]
|
||||
|
||||
(->> (db/exec! conn [query file-id start-at limit])
|
||||
(->> (db/exec! conn [sql:get-file-snapshots file-id start-at limit])
|
||||
(mapv (fn [row]
|
||||
(update row :created-at dt/format-instant :rfc1123))))))
|
||||
|
||||
@@ -63,8 +66,8 @@
|
||||
(db/run! cfg get-file-snapshots params))
|
||||
|
||||
(defn restore-file-snapshot!
|
||||
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id id]}]
|
||||
(let [storage (media/configure-assets-storage storage conn)
|
||||
[{:keys [::db/conn] :as cfg} {:keys [file-id id]}]
|
||||
(let [storage (sto/resolve cfg {::db/reuse-conn true})
|
||||
file (files/get-minimal-file conn file-id {::db/for-update true})
|
||||
snapshot (db/get* conn :file-change
|
||||
{:file-id file-id
|
||||
@@ -78,43 +81,53 @@
|
||||
:id id
|
||||
:file-id file-id))
|
||||
|
||||
(when-not (:data snapshot)
|
||||
(ex/raise :type :precondition
|
||||
:code :snapshot-without-data
|
||||
:hint "snapshot has no data"
|
||||
:label (:label snapshot)
|
||||
:file-id file-id))
|
||||
(let [snapshot (feat.fdata/resolve-file-data cfg snapshot)]
|
||||
(when-not (:data snapshot)
|
||||
(ex/raise :type :precondition
|
||||
:code :snapshot-without-data
|
||||
:hint "snapshot has no data"
|
||||
:label (:label snapshot)
|
||||
:file-id file-id))
|
||||
|
||||
(l/dbg :hint "restoring snapshot"
|
||||
:file-id (str file-id)
|
||||
:label (:label snapshot)
|
||||
:snapshot-id (str (:id snapshot)))
|
||||
(l/dbg :hint "restoring snapshot"
|
||||
:file-id (str file-id)
|
||||
:label (:label snapshot)
|
||||
:snapshot-id (str (:id snapshot)))
|
||||
|
||||
(db/update! conn :file
|
||||
{:data (:data snapshot)
|
||||
:revn (inc (:revn file))
|
||||
:features (:features snapshot)}
|
||||
{:id file-id})
|
||||
;; If the file was already offloaded, on restring the snapshot
|
||||
;; we are going to replace the file data, so we need to touch
|
||||
;; the old referenced storage object and avoid possible leaks
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(sto/touch-object! storage (:data-ref-id file)))
|
||||
|
||||
;; clean object thumbnails
|
||||
(let [sql (str "update file_tagged_object_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(db/update! conn :file
|
||||
{:data (:data snapshot)
|
||||
:revn (inc (:revn file))
|
||||
:version (:version snapshot)
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:has-media-trimmed false
|
||||
:features (:features snapshot)}
|
||||
{:id file-id})
|
||||
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
;; clean object thumbnails
|
||||
(let [sql (str "update file_tagged_object_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
|
||||
;; clean object thumbnails
|
||||
(let [sql (str "update file_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
;; clean file thumbnails
|
||||
(let [sql (str "update file_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
|
||||
{:id (:id snapshot)
|
||||
:label (:label snapshot)}))
|
||||
{:id (:id snapshot)
|
||||
:label (:label snapshot)})))
|
||||
|
||||
(defn- resolve-snapshot-by-label
|
||||
[conn file-id label]
|
||||
@@ -146,21 +159,33 @@
|
||||
(merge (resolve-snapshot-by-label conn file-id label)))]
|
||||
(restore-file-snapshot! cfg params)))))
|
||||
|
||||
(defn- get-file
|
||||
[cfg file-id]
|
||||
(let [file (->> (db/get cfg :file {:id file-id})
|
||||
(feat.fdata/resolve-file-data cfg))]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(-> file
|
||||
(update :data blob/decode)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(update :data blob/encode)))))
|
||||
|
||||
(defn take-file-snapshot!
|
||||
[cfg {:keys [file-id label]}]
|
||||
(let [conn (db/get-connection cfg)
|
||||
file (db/get conn :file {:id file-id})
|
||||
[cfg {:keys [file-id label ::rpc/profile-id]}]
|
||||
(let [file (get-file cfg file-id)
|
||||
id (uuid/next)]
|
||||
|
||||
(l/debug :hint "creating file snapshot"
|
||||
:file-id (str file-id)
|
||||
:label label)
|
||||
|
||||
(db/insert! conn :file-change
|
||||
(db/insert! cfg :file-change
|
||||
{:id id
|
||||
:revn (:revn file)
|
||||
:data (:data file)
|
||||
:version (:version file)
|
||||
:features (:features file)
|
||||
:profile-id profile-id
|
||||
:file-id (:id file)
|
||||
:label label}
|
||||
{::db/return-keys false})
|
||||
|
||||
@@ -35,12 +35,12 @@
|
||||
|
||||
(def ^:private schema:create-temp-file
|
||||
[:map {:title "create-temp-file"}
|
||||
[:name :string]
|
||||
[:name [:string {:max 250}]]
|
||||
[:project-id ::sm/uuid]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:is-shared :boolean]
|
||||
[:is-shared ::sm/boolean]
|
||||
[:features ::cfeat/features]
|
||||
[:create-page :boolean]])
|
||||
[:create-page ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::create-temp-file
|
||||
{::doc/added "1.17"
|
||||
@@ -83,7 +83,7 @@
|
||||
(def ^:private schema:update-temp-file
|
||||
[:map {:title "update-temp-file"}
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:revn {:min 0} :int]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:session-id ::sm/uuid]
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- FEATURES
|
||||
@@ -86,8 +85,8 @@
|
||||
::doc/module :files
|
||||
::sm/params [:map {:title "get-file-object-thumbnails"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:tag {:optional true} :string]]
|
||||
::sm/result [:map-of :string :string]}
|
||||
[:tag {:optional true} [:string {:max 50}]]]
|
||||
::sm/result [:map-of [:string {:max 250}] [:string {:max 250}]]}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id tag] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
@@ -190,7 +189,7 @@
|
||||
(sm/define
|
||||
[:map {:title "PartialFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} :int]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:page :any]]))
|
||||
|
||||
(sv/defmethod ::get-file-data-for-thumbnail
|
||||
@@ -234,7 +233,7 @@
|
||||
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (file_id, object_id, tag)
|
||||
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
|
||||
DO UPDATE SET updated_at=?, media_id=?, deleted_at=?
|
||||
RETURNING *")
|
||||
|
||||
(defn- persist-thumbnail!
|
||||
@@ -252,17 +251,19 @@
|
||||
:content-type mtype
|
||||
:bucket "file-object-thumbnail"})))
|
||||
|
||||
|
||||
|
||||
(defn- create-file-object-thumbnail!
|
||||
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
|
||||
(let [tsnow (dt/now)
|
||||
media (persist-thumbnail! storage media tsnow)
|
||||
[{:keys [::sto/storage] :as cfg} file object-id media tag]
|
||||
(let [file-id (:id file)
|
||||
timestamp (dt/now)
|
||||
media (persist-thumbnail! storage media timestamp)
|
||||
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
|
||||
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
|
||||
file-id object-id tag (:id media)
|
||||
tsnow (:id media)])]
|
||||
file-id object-id tag
|
||||
(:id media)
|
||||
timestamp
|
||||
(:id media)
|
||||
(:deleted-at file)])]
|
||||
[th1 th2])))]
|
||||
|
||||
(when (and (some? th1)
|
||||
@@ -276,9 +277,9 @@
|
||||
schema:create-file-object-thumbnail
|
||||
[:map {:title "create-file-object-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:object-id :string]
|
||||
[:object-id [:string {:max 250}]]
|
||||
[:media ::media/upload]
|
||||
[:tag {:optional true} :string]])
|
||||
[:tag {:optional true} [:string {:max 50}]]])
|
||||
|
||||
(sv/defmethod ::create-file-object-thumbnail
|
||||
{::doc/added "1.19"
|
||||
@@ -295,9 +296,8 @@
|
||||
(media/validate-media-size! media)
|
||||
|
||||
(db/run! cfg files/check-edition-permissions! profile-id file-id)
|
||||
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))
|
||||
(when-let [file (files/get-minimal-file cfg file-id {::db/check-deleted false})]
|
||||
(create-file-object-thumbnail! cfg file object-id media (or tag "frame"))))
|
||||
|
||||
;; --- MUTATION COMMAND: delete-file-object-thumbnail
|
||||
|
||||
@@ -314,19 +314,21 @@
|
||||
:object-id object-id
|
||||
:tag tag})))
|
||||
|
||||
(s/def ::delete-file-object-thumbnail
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::object-id]))
|
||||
(def ^:private schema:delete-file-object-thumbnail
|
||||
[:map {:title "delete-file-object-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:object-id [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::delete-file-object-thumbnail
|
||||
{::doc/added "1.19"
|
||||
::doc/module :files
|
||||
::sm/params schema:delete-file-object-thumbnail
|
||||
::audit/skip true}
|
||||
[cfg {:keys [::rpc/profile-id file-id object-id]}]
|
||||
(files/check-edition-permissions! cfg profile-id file-id)
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage conn)
|
||||
(update ::sto/storage sto/configure conn)
|
||||
(delete-file-object-thumbnail! file-id object-id))
|
||||
nil)))
|
||||
|
||||
@@ -385,7 +387,7 @@
|
||||
schema:create-file-thumbnail
|
||||
[:map {:title "create-file-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:revn ::sm/int]
|
||||
[:media ::media/upload]])
|
||||
|
||||
(sv/defmethod ::create-file-thumbnail
|
||||
@@ -404,7 +406,6 @@
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(when-not (db/read-only? conn)
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)
|
||||
media (create-file-thumbnail! cfg params)]
|
||||
(let [media (create-file-thumbnail! cfg params)]
|
||||
{:uri (files/resolve-public-uri (:id media))
|
||||
:id (:id media)})))))
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
@@ -37,6 +38,20 @@
|
||||
[clojure.set :as set]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(declare ^:private get-lagged-changes)
|
||||
(declare ^:private send-notifications!)
|
||||
(declare ^:private update-file)
|
||||
(declare ^:private update-file*)
|
||||
(declare ^:private process-changes-and-validate)
|
||||
(declare ^:private take-snapshot?)
|
||||
(declare ^:private delete-old-snapshots!)
|
||||
|
||||
;; PUBLIC API; intended to be used outside of this module
|
||||
(declare update-file!)
|
||||
(declare update-file-data!)
|
||||
(declare persist-file!)
|
||||
(declare get-file)
|
||||
|
||||
;; --- SCHEMA
|
||||
|
||||
(def ^:private
|
||||
@@ -44,15 +59,15 @@
|
||||
[:map {:title "update-file"}
|
||||
[:id ::sm/uuid]
|
||||
[:session-id ::sm/uuid]
|
||||
[:revn {:min 0} :int]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:changes {:optional true} [:vector ::cpc/change]]
|
||||
[:changes-with-metadata {:optional true}
|
||||
[:vector [:map
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:hint-origin {:optional true} :keyword]
|
||||
[:hint-events {:optional true} [:vector :string]]]]]
|
||||
[:skip-validate {:optional true} :boolean]])
|
||||
[:hint-events {:optional true} [:vector [:string {:max 250}]]]]]]
|
||||
[:skip-validate {:optional true} ::sm/boolean]])
|
||||
|
||||
(def ^:private
|
||||
schema:update-file-result
|
||||
@@ -61,7 +76,7 @@
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:file-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} :int]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:session-id ::sm/uuid]]])
|
||||
|
||||
;; --- HELPERS
|
||||
@@ -96,40 +111,6 @@
|
||||
(or (contains? library-change-types type)
|
||||
(contains? file-change-types type)))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(defn get-file
|
||||
[conn id]
|
||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
(update file :features db/decode-pgarray #{})))
|
||||
|
||||
(defn- wrap-with-pointer-map-context
|
||||
[f]
|
||||
(fn [cfg {:keys [id] :as file}]
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [result (f cfg file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
result))))
|
||||
|
||||
(declare get-lagged-changes)
|
||||
(declare send-notifications!)
|
||||
(declare update-file)
|
||||
(declare update-file*)
|
||||
(declare update-file-data)
|
||||
(declare take-snapshot?)
|
||||
|
||||
;; If features are specified from params and the final feature
|
||||
;; set is different than the persisted one, update it on the
|
||||
;; database.
|
||||
@@ -145,7 +126,8 @@
|
||||
::sm/result schema:update-file-result
|
||||
::doc/module :files
|
||||
::doc/added "1.17"}
|
||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||
[{:keys [::mtx/metrics] :as cfg}
|
||||
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(db/xact-lock! conn id)
|
||||
@@ -159,14 +141,30 @@
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file) (:features params)))
|
||||
|
||||
params (assoc params
|
||||
:profile-id profile-id
|
||||
:features features
|
||||
:team team
|
||||
:file file)
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features)
|
||||
(assoc :team team)
|
||||
(assoc :file file)
|
||||
(assoc :changes changes))
|
||||
|
||||
cfg (assoc cfg ::timestamp (dt/now))
|
||||
|
||||
tpoint (dt/tpoint)]
|
||||
|
||||
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it.
|
||||
(when (not= features (:features team))
|
||||
@@ -175,73 +173,124 @@
|
||||
{:features features}
|
||||
{:id (:id team)})))
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [l/*context* (some-> (meta params)
|
||||
(get :app.http/request)
|
||||
(errors/request->context))]
|
||||
(-> (update-file cfg params)
|
||||
(-> (update-file* cfg params)
|
||||
(rph/with-defer #(let [elapsed (tpoint)]
|
||||
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))))
|
||||
|
||||
(defn update-file
|
||||
[{:keys [::mtx/metrics] :as cfg}
|
||||
{:keys [file features changes changes-with-metadata] :as params}]
|
||||
(let [features (-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file)))
|
||||
|
||||
update-fn (cond-> update-file*
|
||||
(contains? features "fdata/pointer-map")
|
||||
(wrap-with-pointer-map-context))
|
||||
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))]
|
||||
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(let [file (assoc file :features features)
|
||||
params (-> params
|
||||
(assoc :file file)
|
||||
(assoc :changes changes)
|
||||
(assoc ::created-at (dt/now)))]
|
||||
|
||||
(-> (update-fn cfg params)
|
||||
(vary-meta assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))))
|
||||
|
||||
(defn- update-file*
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg}
|
||||
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}]
|
||||
(let [;; Process the file data on separated thread for avoid to do
|
||||
;; the CPU intensive operation on vthread.
|
||||
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))
|
||||
features (db/create-array conn "text" (:features file))]
|
||||
"Internal function, part of the update-file process, that encapsulates
|
||||
the changes application offload to a separated thread and emit all
|
||||
corresponding notifications.
|
||||
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:features (db/create-array conn "text" (:features file))
|
||||
:data (when (take-snapshot? file)
|
||||
(:data file))
|
||||
:changes (blob/encode changes)}
|
||||
Follow the inner implementation to `update-file-data!` function.
|
||||
|
||||
Only intended for internal use on this module."
|
||||
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
|
||||
{:keys [profile-id file features changes session-id skip-validate] :as params}]
|
||||
|
||||
(let [;; Retrieve the file data
|
||||
file (feat.fdata/resolve-file-data cfg file)
|
||||
|
||||
file (assoc file :features
|
||||
(-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file))))
|
||||
|
||||
;; Process the file data on separated thread for avoid to do
|
||||
;; the CPU intensive operation on vthread.
|
||||
file (px/invoke! executor
|
||||
(fn []
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(update-file-data! cfg file
|
||||
process-changes-and-validate
|
||||
changes skip-validate))))]
|
||||
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
||||
(some->> (:data-ref-id file) (sto/touch-object! storage))))
|
||||
|
||||
;; TODO: move this to asynchronous task
|
||||
(when (::snapshot-data file)
|
||||
(delete-old-snapshots! cfg file))
|
||||
|
||||
(persist-file! cfg file)
|
||||
|
||||
(let [params (assoc params :file file)
|
||||
response {:revn (:revn file)
|
||||
:lagged (get-lagged-changes conn params)}
|
||||
features (db/create-array conn "text" (:features file))]
|
||||
|
||||
;; Insert change (xlog)
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at timestamp
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:version (:version file)
|
||||
:features features
|
||||
:label (::snapshot-label file)
|
||||
:data (::snapshot-data file)
|
||||
:changes (blob/encode changes)}
|
||||
{::db/return-keys false})
|
||||
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications! cfg params)
|
||||
|
||||
(vary-meta response assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))
|
||||
|
||||
(defn update-file!
|
||||
"A public api that allows apply a transformation to a file with all context setup."
|
||||
[cfg file-id update-fn & args]
|
||||
(let [file (get-file cfg file-id)
|
||||
file (apply update-file-data! cfg file update-fn args)]
|
||||
(persist-file! cfg file)))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(defn get-file
|
||||
"Get not-decoded file, only decodes the features set."
|
||||
[conn id]
|
||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
(update file :features db/decode-pgarray #{})))
|
||||
|
||||
(defn persist-file!
|
||||
"Function responsible of persisting already encoded file. Should be
|
||||
used together with `get-file` and `update-file-data!`.
|
||||
|
||||
It also updates the project modified-at attr."
|
||||
[{:keys [::db/conn ::timestamp]} file]
|
||||
(let [features (db/create-array conn "text" (:features file))
|
||||
;; The timestamp can be nil because this function is also
|
||||
;; intended to be used outside of this module
|
||||
modified-at (or timestamp (dt/now))]
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at modified-at}
|
||||
{:id (:project-id file)}
|
||||
{::db/return-keys false})
|
||||
|
||||
(db/update! conn :file
|
||||
@@ -250,20 +299,96 @@
|
||||
:version (:version file)
|
||||
:features features
|
||||
:data-backend nil
|
||||
:modified-at created-at
|
||||
:data-ref-id nil
|
||||
:modified-at modified-at
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)})
|
||||
{:id (:id file)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at created-at}
|
||||
{:id (:project-id file)})
|
||||
(defn- update-file-data!
|
||||
"Perform a file data transformation in with all update context setup.
|
||||
|
||||
(let [params (assoc params :file file)]
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications! cfg params)
|
||||
This function expected not-decoded file and transformation function. Returns
|
||||
an encoded file.
|
||||
|
||||
{:revn (:revn file)
|
||||
:lagged (get-lagged-changes conn params)})))
|
||||
This function is not responsible of saving the file. It only saves
|
||||
fdata/pointer-map modified fragments."
|
||||
|
||||
[cfg {:keys [id] :as file} update-fn & args]
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file)))))
|
||||
|
||||
;; For avoid unnecesary overhead of creating multiple pointers
|
||||
;; and handly internally with objects map in their worst
|
||||
;; case (when probably all shapes and all pointers will be
|
||||
;; readed in any case), we just realize/resolve them before
|
||||
;; applying the migration to the file
|
||||
file (if (fmg/need-migration? file)
|
||||
(-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
file)
|
||||
|
||||
file (apply update-fn cfg file args)
|
||||
|
||||
;; TODO: reuse operations if file is migrated
|
||||
;; TODO: move encoding to a separated thread
|
||||
file (if (take-snapshot? file)
|
||||
(let [tpoint (dt/tpoint)
|
||||
snapshot (-> (:data file)
|
||||
(feat.fdata/process-pointers deref)
|
||||
(feat.fdata/process-objects (partial into {}))
|
||||
(blob/encode))
|
||||
elapsed (tpoint)
|
||||
label (str "internal/snapshot/" (:revn file))]
|
||||
|
||||
(l/trc :hint "take snapshot"
|
||||
:file-id (str (:id file))
|
||||
:revn (:revn file)
|
||||
:label label
|
||||
:elapsed (dt/format-duration elapsed))
|
||||
|
||||
(-> file
|
||||
(assoc ::snapshot-data snapshot)
|
||||
(assoc ::snapshot-label label)))
|
||||
file)
|
||||
|
||||
file (cond-> file
|
||||
(contains? cfeat/*current* "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map)
|
||||
|
||||
(contains? cfeat/*current* "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map)
|
||||
|
||||
:always
|
||||
(update :data blob/encode))]
|
||||
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
|
||||
file)))
|
||||
|
||||
(defn- get-file-libraries
|
||||
"A helper for preload file libraries, mainly used for perform file
|
||||
semantical and structural validation"
|
||||
[{:keys [::db/conn] :as cfg} file]
|
||||
(->> (files/get-file-libraries conn (:id file))
|
||||
(into [file] (map (fn [{:keys [id]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* nil]
|
||||
;; We do not resolve the objects maps here
|
||||
;; because there is a lower probability that all
|
||||
;; shapes needed to be loded into memory, so we
|
||||
;; leeave it on lazy status
|
||||
(-> (files/get-file cfg id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))))))
|
||||
(d/index-by :id)))
|
||||
|
||||
(defn- soft-validate-file-schema!
|
||||
[file]
|
||||
@@ -280,51 +405,20 @@
|
||||
(l/error :hint "file validation error"
|
||||
:cause cause))))
|
||||
|
||||
(defn- update-file-data
|
||||
[{:keys [::db/conn] :as cfg} file changes skip-validate]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file)))))
|
||||
|
||||
;; For avoid unnecesary overhead of creating multiple pointers
|
||||
;; and handly internally with objects map in their worst
|
||||
;; case (when probably all shapes and all pointers will be
|
||||
;; readed in any case), we just realize/resolve them before
|
||||
;; applying the migration to the file
|
||||
file (if (fmg/need-migration? file)
|
||||
(-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))
|
||||
file)
|
||||
|
||||
;; WARNING: this ruins performance; maybe we need to find
|
||||
(defn- process-changes-and-validate
|
||||
[cfg file changes skip-validate]
|
||||
(let [;; WARNING: this ruins performance; maybe we need to find
|
||||
;; some other way to do general validation
|
||||
libs (when (and (or (contains? cf/flags :file-validation)
|
||||
(contains? cf/flags :soft-file-validation))
|
||||
(not skip-validate))
|
||||
(->> (files/get-file-libraries conn (:id file))
|
||||
(into [file] (map (fn [{:keys [id]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* nil]
|
||||
;; We do not resolve the objects maps here
|
||||
;; because there is a lower probability that all
|
||||
;; shapes needed to be loded into memory, so we
|
||||
;; leeave it on lazy status
|
||||
(-> (files/get-file cfg id :migrate? false)
|
||||
(update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file))))))
|
||||
(d/index-by :id)))
|
||||
|
||||
(get-file-libraries cfg file))
|
||||
|
||||
file (-> (files/check-version! file)
|
||||
(update :revn inc)
|
||||
(update :data cpc/process-changes changes)
|
||||
(update :data d/without-nils))]
|
||||
|
||||
|
||||
(binding [pmap/*tracked* nil]
|
||||
(when (contains? cf/flags :soft-file-validation)
|
||||
(soft-validate-file! file libs))
|
||||
@@ -340,29 +434,49 @@
|
||||
(not skip-validate))
|
||||
(val/validate-file-schema! file)))
|
||||
|
||||
(cond-> file
|
||||
(contains? cfeat/*current* "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map)
|
||||
|
||||
(contains? cfeat/*current* "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map)
|
||||
|
||||
:always
|
||||
(update :data blob/encode))))
|
||||
file))
|
||||
|
||||
(defn- take-snapshot?
|
||||
"Defines the rule when file `data` snapshot should be saved."
|
||||
[{:keys [revn modified-at] :as file}]
|
||||
(let [freq (or (cf/get :file-change-snapshot-every) 20)
|
||||
timeout (or (cf/get :file-change-snapshot-timeout)
|
||||
(dt/duration {:hours 1}))]
|
||||
(or (= 1 freq)
|
||||
(zero? (mod revn freq))
|
||||
(> (inst-ms (dt/diff modified-at (dt/now)))
|
||||
(inst-ms timeout)))))
|
||||
(when (contains? cf/flags :auto-file-snapshot)
|
||||
(let [freq (or (cf/get :auto-file-snapshot-every) 20)
|
||||
timeout (or (cf/get :auto-file-snapshot-timeout)
|
||||
(dt/duration {:hours 1}))]
|
||||
|
||||
(def ^:private
|
||||
sql:lagged-changes
|
||||
(or (= 1 freq)
|
||||
(zero? (mod revn freq))
|
||||
(> (inst-ms (dt/diff modified-at (dt/now)))
|
||||
(inst-ms timeout))))))
|
||||
|
||||
;; Get the latest available snapshots without exceeding the total
|
||||
;; snapshot limit.
|
||||
(def ^:private sql:get-latest-snapshots
|
||||
"SELECT fch.id, fch.created_at
|
||||
FROM file_change AS fch
|
||||
WHERE fch.file_id = ?
|
||||
AND fch.label LIKE 'internal/%'
|
||||
ORDER BY fch.created_at DESC
|
||||
LIMIT ?")
|
||||
|
||||
;; Mark all snapshots that are outside the allowed total threshold
|
||||
;; available for the GC.
|
||||
(def ^:private sql:delete-snapshots
|
||||
"UPDATE file_change
|
||||
SET label = NULL
|
||||
WHERE file_id = ?
|
||||
AND label LIKE 'internal/%'
|
||||
AND created_at < ?")
|
||||
|
||||
(defn- delete-old-snapshots!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(when-let [snapshots (not-empty (db/exec! conn [sql:get-latest-snapshots id
|
||||
(cf/get :auto-file-snapshot-total 10)]))]
|
||||
(let [last-date (-> snapshots peek :created-at)
|
||||
result (db/exec-one! conn [sql:delete-snapshots id last-date])]
|
||||
(l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result)))))
|
||||
|
||||
(def ^:private sql:lagged-changes
|
||||
"select s.id, s.revn, s.file_id,
|
||||
s.session_id, s.changes
|
||||
from file_change as s
|
||||
|
||||
@@ -95,12 +95,11 @@
|
||||
[cfg {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id))))))
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
|
||||
::quotes/profile-id profile-id
|
||||
::quotes/team-id team-id})
|
||||
(create-font-variant cfg (assoc params :profile-id profile-id)))))
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
|
||||
@@ -203,14 +202,13 @@
|
||||
::sm/params schema:delete-font}
|
||||
[cfg {:keys [::rpc/profile-id id team-id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn ::sto/storage] :as cfg}]
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(let [fonts (db/query conn :team-font-variant
|
||||
{:team-id team-id
|
||||
:font-id id
|
||||
:deleted-at nil}
|
||||
{::sql/for-update true})
|
||||
storage (media/configure-assets-storage storage conn)
|
||||
tnow (dt/now)]
|
||||
|
||||
(when-not (seq fonts)
|
||||
@@ -220,11 +218,7 @@
|
||||
(doseq [font fonts]
|
||||
(db/update! conn :team-font-variant
|
||||
{:deleted-at tnow}
|
||||
{:id (:id font)})
|
||||
(some->> (:woff1-file-id font) (sto/touch-object! storage))
|
||||
(some->> (:woff2-file-id font) (sto/touch-object! storage))
|
||||
(some->> (:ttf-file-id font) (sto/touch-object! storage))
|
||||
(some->> (:otf-file-id font) (sto/touch-object! storage)))
|
||||
{:id (:id font)}))
|
||||
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:id id
|
||||
@@ -245,22 +239,16 @@
|
||||
::sm/params schema:delete-font-variant}
|
||||
[cfg {:keys [::rpc/profile-id id team-id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn ::sto/storage] :as cfg}]
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(let [variant (db/get conn :team-font-variant
|
||||
{:id id :team-id team-id}
|
||||
{::sql/for-update true})
|
||||
storage (media/configure-assets-storage storage conn)]
|
||||
{::sql/for-update true})]
|
||||
|
||||
(db/update! conn :team-font-variant
|
||||
{:deleted-at (dt/now)}
|
||||
{:id (:id variant)})
|
||||
|
||||
(some->> (:woff1-file-id variant) (sto/touch-object! storage))
|
||||
(some->> (:woff2-file-id variant) (sto/touch-object! storage))
|
||||
(some->> (:ttf-file-id variant) (sto/touch-object! storage))
|
||||
(some->> (:otf-file-id variant) (sto/touch-object! storage))
|
||||
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:font-family (:font-family variant)
|
||||
:font-id (:font-id variant)}})))))
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
[app.auth.ldap :as ldap]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
@@ -19,27 +19,25 @@
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; --- COMMAND: login-with-ldap
|
||||
|
||||
(declare login-or-register)
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::password ::us/string)
|
||||
(s/def ::invitation-token ::us/string)
|
||||
|
||||
(s/def ::login-with-ldap
|
||||
(s/keys :req-un [::email ::password]
|
||||
:opt-un [::invitation-token]))
|
||||
(def schema:login-with-ldap
|
||||
[:map {:title "login-with-ldap"}
|
||||
[:email ::sm/email]
|
||||
[:password auth/schema:password]
|
||||
[:invitation-token {:optional true} auth/schema:token]])
|
||||
|
||||
(sv/defmethod ::login-with-ldap
|
||||
"Performs the authentication using LDAP backend. Only works if LDAP
|
||||
is properly configured and enabled with `login-with-ldap` flag."
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth}
|
||||
::doc/module :auth
|
||||
::sm/params schema:login-with-ldap}
|
||||
[{:keys [::setup/props ::ldap/provider] :as cfg} params]
|
||||
(when-not provider
|
||||
(ex/raise :type :restriction
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
(sm/define
|
||||
[:map {:title "duplicate-file"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:name {:optional true} :string]]))
|
||||
[:name {:optional true} [:string {:max 250}]]]))
|
||||
|
||||
(sv/defmethod ::duplicate-file
|
||||
"Duplicate a single file in the same team."
|
||||
@@ -153,7 +153,7 @@
|
||||
(sm/define
|
||||
[:map {:title "duplicate-project"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:name {:optional true} :string]]))
|
||||
[:name {:optional true} [:string {:max 250}]]]))
|
||||
|
||||
(sv/defmethod ::duplicate-project
|
||||
"Duplicate an entire project with all the files"
|
||||
@@ -382,10 +382,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:move-project
|
||||
(sm/define
|
||||
[:map {:title "move-project"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]]))
|
||||
[:map {:title "move-project"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:project-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::move-project
|
||||
"Move projects between teams"
|
||||
@@ -397,8 +396,8 @@
|
||||
|
||||
;; --- COMMAND: Clone Template
|
||||
|
||||
(defn- clone-template
|
||||
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
|
||||
(defn clone-template
|
||||
[cfg {:keys [project-id profile-id] :as params} template]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
||||
;; NOTE: the importation process performs some operations that
|
||||
;; are not very friendly with virtual threads, and for avoid
|
||||
@@ -417,6 +416,7 @@
|
||||
(doseq [file-id result]
|
||||
(let [props (assoc props :id file-id)
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/profile-id profile-id)
|
||||
(assoc ::audit/name "create-file")
|
||||
(assoc ::audit/props props))]
|
||||
(audit/submit! cfg event))))
|
||||
@@ -425,10 +425,9 @@
|
||||
|
||||
(def ^:private
|
||||
schema:clone-template
|
||||
(sm/define
|
||||
[:map {:title "clone-template"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:template-id ::sm/word-string]]))
|
||||
[:map {:title "clone-template"}
|
||||
[:project-id ::sm/uuid]
|
||||
[:template-id ::sm/word-string]])
|
||||
|
||||
(sv/defmethod ::clone-template
|
||||
"Clone into the specified project the template by its id."
|
||||
@@ -439,7 +438,8 @@
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
|
||||
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
|
||||
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
||||
template (tmpl/get-template-stream cfg template-id)]
|
||||
template (tmpl/get-template-stream cfg template-id)
|
||||
params (assoc params :profile-id profile-id)]
|
||||
|
||||
(when-not template
|
||||
(ex/raise :type :not-found
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -25,7 +25,6 @@
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]
|
||||
[promesa.exec :as px]))
|
||||
@@ -39,43 +38,37 @@
|
||||
:quality 85
|
||||
:format :jpeg})
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(declare create-file-media-object)
|
||||
|
||||
(s/def ::content ::media/upload)
|
||||
(s/def ::is-local ::us/boolean)
|
||||
|
||||
(s/def ::upload-file-media-object
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::is-local ::name ::content]
|
||||
:opt-un [::id]))
|
||||
(def ^:private schema:upload-file-media-object
|
||||
[:map {:title "upload-file-media-object"}
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:is-local ::sm/boolean]
|
||||
[:name [:string {:max 250}]]
|
||||
[:content ::media/upload]])
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:upload-file-media-object
|
||||
::climit/id [[:process-image/by-profile ::rpc/profile-id]
|
||||
[:process-image/global]]}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(media/validate-media-type! content)
|
||||
(media/validate-media-size! content)
|
||||
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(media/validate-media-type! content)
|
||||
(media/validate-media-size! content)
|
||||
|
||||
(db/run! cfg (fn [cfg]
|
||||
(let [object (create-file-media-object cfg params)
|
||||
props {:name (:name params)
|
||||
:file-id file-id
|
||||
:is-local (:is-local params)
|
||||
:size (:size content)
|
||||
:mtype (:mtype content)}]
|
||||
(with-meta object
|
||||
{::audit/replace-props props}))))))
|
||||
(db/run! cfg (fn [cfg]
|
||||
(let [object (create-file-media-object cfg params)
|
||||
props {:name (:name params)
|
||||
:file-id file-id
|
||||
:is-local (:is-local params)
|
||||
:size (:size content)
|
||||
:mtype (:mtype content)}]
|
||||
(with-meta object
|
||||
{::audit/replace-props props})))))
|
||||
|
||||
(defn- big-enough-for-thumbnail?
|
||||
"Checks if the provided image info is big enough for
|
||||
@@ -176,18 +169,20 @@
|
||||
|
||||
(declare ^:private create-file-media-object-from-url)
|
||||
|
||||
(s/def ::create-file-media-object-from-url
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::is-local ::url]
|
||||
:opt-un [::id ::name]))
|
||||
(def ^:private schema:create-file-media-object-from-url
|
||||
[:map {:title "create-file-media-object-from-url"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:is-local ::sm/boolean]
|
||||
[:url ::sm/uri]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} [:string {:max 250}]]])
|
||||
|
||||
(sv/defmethod ::create-file-media-object-from-url
|
||||
{::doc/added "1.17"
|
||||
::doc/deprecated "1.19"}
|
||||
::sm/params schema:create-file-media-object-from-url}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(create-file-media-object-from-url cfg (assoc params :profile-id profile-id))))
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
(create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))
|
||||
|
||||
(defn download-image
|
||||
[{:keys [::http/client]} uri]
|
||||
@@ -255,12 +250,15 @@
|
||||
|
||||
(declare clone-file-media-object)
|
||||
|
||||
(s/def ::clone-file-media-object
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::file-id ::is-local ::id]))
|
||||
(def ^:private schema:clone-file-media-object
|
||||
[:map {:title "clone-file-media-object"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:is-local ::sm/boolean]
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::clone-file-media-object
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:clone-file-media-object}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
[:id ::sm/uuid]
|
||||
[:fullname [::sm/word-string {:max 250}]]
|
||||
[:email ::sm/email]
|
||||
[:is-active {:optional true} :boolean]
|
||||
[:is-blocked {:optional true} :boolean]
|
||||
[:is-demo {:optional true} :boolean]
|
||||
[:is-muted {:optional true} :boolean]
|
||||
[:is-active {:optional true} ::sm/boolean]
|
||||
[:is-blocked {:optional true} ::sm/boolean]
|
||||
[:is-demo {:optional true} ::sm/boolean]
|
||||
[:is-muted {:optional true} ::sm/boolean]
|
||||
[:created-at {:optional true} ::sm/inst]
|
||||
[:modified-at {:optional true} ::sm/inst]
|
||||
[:default-project-id {:optional true} ::sm/uuid]
|
||||
@@ -210,8 +210,7 @@
|
||||
[cfg {:keys [::rpc/profile-id file] :as params}]
|
||||
;; Validate incoming mime type
|
||||
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(update-profile-photo cfg (assoc params :profile-id profile-id))))
|
||||
(update-profile-photo cfg (assoc params :profile-id profile-id)))
|
||||
|
||||
(defn update-profile-photo
|
||||
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
|
||||
@@ -361,27 +360,31 @@
|
||||
[:map {:title "update-profile-props"}
|
||||
[:props [:map-of :keyword :any]]]))
|
||||
|
||||
(defn update-profile-props
|
||||
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
(if (nil? v)
|
||||
(dissoc props k)
|
||||
(assoc props k v))
|
||||
props))
|
||||
(:props profile)
|
||||
props)]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
|
||||
(filter-props props)))
|
||||
|
||||
(sv/defmethod ::update-profile-props
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-props}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id props]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
(if (nil? v)
|
||||
(dissoc props k)
|
||||
(assoc props k v))
|
||||
props))
|
||||
(:props profile)
|
||||
props)]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
|
||||
(filter-props props))))
|
||||
[cfg {:keys [::rpc/profile-id props]}]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(update-profile-props cfg profile-id props))))
|
||||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
@@ -21,11 +21,7 @@
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
[app.worker :as wrk]))
|
||||
|
||||
;; --- Check Project Permissions
|
||||
|
||||
@@ -75,13 +71,13 @@
|
||||
|
||||
(declare get-projects)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::get-projects
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id]))
|
||||
(def ^:private schema:get-projects
|
||||
[:map {:title "get-projects"}
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::get-projects
|
||||
{::doc/added "1.18"}
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:get-projects}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
@@ -112,11 +108,12 @@
|
||||
|
||||
(declare get-all-projects)
|
||||
|
||||
(s/def ::get-all-projects
|
||||
(s/keys :req [::rpc/profile-id]))
|
||||
(def ^:private schema:get-all-projects
|
||||
[:map {:title "get-all-projects"}])
|
||||
|
||||
(sv/defmethod ::get-all-projects
|
||||
{::doc/added "1.18"}
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:get-all-projects}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(get-all-projects conn profile-id)))
|
||||
@@ -154,12 +151,13 @@
|
||||
|
||||
;; --- QUERY: Get project
|
||||
|
||||
(s/def ::get-project
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]))
|
||||
(def ^:private schema:get-project
|
||||
[:map {:title "get-project"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::get-project
|
||||
{::doc/added "1.18"}
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:get-project}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(let [project (db/get-by-id conn :project id)]
|
||||
@@ -170,14 +168,16 @@
|
||||
|
||||
;; --- MUTATION: Create Project
|
||||
|
||||
(s/def ::create-project
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id ::name]
|
||||
:opt-un [::id]))
|
||||
(def ^:private schema:create-project
|
||||
[:map {:title "create-project"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:name [:string {:max 250 :min 1}]]
|
||||
[:id {:optional true} ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::create-project
|
||||
{::doc/added "1.18"
|
||||
::webhooks/event? true}
|
||||
::webhooks/event? true
|
||||
::sm/params schema:create-project}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
@@ -205,14 +205,15 @@
|
||||
on conflict (team_id, project_id, profile_id)
|
||||
do update set is_pinned=?")
|
||||
|
||||
(s/def ::is-pinned ::us/boolean)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::update-project-pin
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id ::team-id ::is-pinned]))
|
||||
(def ^:private schema:update-project-pin
|
||||
[:map {:title "update-project-pin"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:is-pinned ::sm/boolean]
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::update-project-pin
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:update-project-pin
|
||||
::webhooks/batch-timeout (dt/duration "5s")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
::webhooks/event? true}
|
||||
@@ -226,12 +227,14 @@
|
||||
|
||||
(declare rename-project)
|
||||
|
||||
(s/def ::rename-project
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::name ::id]))
|
||||
(def ^:private schema:rename-project
|
||||
[:map {:title "rename-project"}
|
||||
[:name [:string {:max 250 :min 1}]]
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::rename-project
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:rename-project
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
@@ -266,12 +269,14 @@
|
||||
|
||||
project))
|
||||
|
||||
(s/def ::delete-project
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]))
|
||||
|
||||
(def ^:private schema:delete-project
|
||||
[:map {:title "delete-project"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::delete-project
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:delete-project
|
||||
::webhooks/event? true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
|
||||
(ns app.rpc.commands.search
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :refer [resolve-public-uri]]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(def ^:private sql:search-files
|
||||
"with projects as (
|
||||
@@ -65,16 +64,14 @@
|
||||
(assoc :thumbnail-uri (resolve-public-uri media-id)))
|
||||
(dissoc row :media-id))))))
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::search-files ::us/string)
|
||||
|
||||
(s/def ::search-files
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id]
|
||||
:opt-un [::search-term]))
|
||||
(def ^:private schema:search-files
|
||||
[:map {:title "search-files"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:search-term {:optional true} :string]])
|
||||
|
||||
(sv/defmethod ::search-files
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files}
|
||||
::doc/module :files
|
||||
::sm/params schema:search-files}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}]
|
||||
(some->> search-term (search-files pool profile-id team-id)))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.email :as eml]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
@@ -28,6 +29,7 @@
|
||||
[app.setup :as-alias setup]
|
||||
[app.storage :as sto]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
@@ -80,6 +82,37 @@
|
||||
(cond-> row
|
||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
||||
|
||||
|
||||
|
||||
(defn- check-valid-email-muted
|
||||
"Check if the member's email is part of the global bounce report."
|
||||
[conn member]
|
||||
(let [email (profile/clean-email (:email member))]
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
:email email
|
||||
:hint "the profile has reported repeatedly as spam or has bounces"))))
|
||||
|
||||
(defn- check-valid-email-bounce
|
||||
"Check if the email is part of the global complain report"
|
||||
[conn email show?]
|
||||
(when (eml/has-bounce-reports? conn email)
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-permanent-bounces
|
||||
:email (if show? email "private")
|
||||
:hint "this email has been repeatedly reported as bounce")))
|
||||
|
||||
(defn- check-valid-email-spam
|
||||
"Check if the member email is part of the global complain report"
|
||||
[conn email show?]
|
||||
(when (eml/has-complaint-reports? conn email)
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-complaints
|
||||
:email (if show? email "private")
|
||||
:hint "this email has been repeatedly reported as spam")))
|
||||
|
||||
|
||||
;; --- Query: Teams
|
||||
|
||||
(declare get-teams)
|
||||
@@ -333,6 +366,24 @@
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
(get-team-invitations conn team-id)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-team-info
|
||||
|
||||
(defn- get-team-info
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(db/get* conn :team
|
||||
{:id id}
|
||||
{::sql/columns [:id :is-default]}))
|
||||
|
||||
(sv/defmethod ::get-team-info
|
||||
"Retrieve minimal team info by its ID."
|
||||
{::rpc/auth false
|
||||
::doc/added "2.2.0"
|
||||
::sm/params schema:get-team}
|
||||
[cfg params]
|
||||
(db/tx-run! cfg get-team-info params))
|
||||
|
||||
|
||||
;; --- Mutation: Create Team
|
||||
|
||||
(declare create-team)
|
||||
@@ -674,8 +725,7 @@
|
||||
[cfg {:keys [::rpc/profile-id file] :as params}]
|
||||
;; Validate incoming mime type
|
||||
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(update-team-photo cfg (assoc params :profile-id profile-id))))
|
||||
(update-team-photo cfg (assoc params :profile-id profile-id)))
|
||||
|
||||
(defn update-team-photo
|
||||
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
@@ -728,25 +778,10 @@
|
||||
(let [email (profile/clean-email email)
|
||||
member (profile/get-profile-by-email conn email)]
|
||||
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
:email email
|
||||
:hint "the profile has reported repeatedly as spam or has bounces"))
|
||||
(check-valid-email-muted conn member)
|
||||
(check-valid-email-bounce conn email true)
|
||||
(check-valid-email-spam conn email true)
|
||||
|
||||
;; Secondly check if the invited member email is part of the global bounce report.
|
||||
(when (eml/has-bounce-reports? conn email)
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-permanent-bounces
|
||||
:email email
|
||||
:hint "the email you invite has been repeatedly reported as bounce"))
|
||||
|
||||
;; Secondly check if the invited member email is part of the global complain report.
|
||||
(when (eml/has-complaint-reports? conn email)
|
||||
(ex/raise :type :restriction
|
||||
:code :email-has-complaints
|
||||
:email email
|
||||
:hint "the email you invite has been repeatedly reported as spam"))
|
||||
|
||||
;; When we have email verification disabled and invitation user is
|
||||
;; already present in the database, we proceed to add it to the
|
||||
@@ -815,11 +850,67 @@
|
||||
|
||||
itoken))))
|
||||
|
||||
(defn- add-user-to-team
|
||||
[conn profile team email role]
|
||||
|
||||
(let [team-id (:id team)
|
||||
member (db/get* conn :profile
|
||||
{:email (str/lower email)}
|
||||
{::sql/columns [:id :email]})
|
||||
params (merge
|
||||
{:team-id team-id
|
||||
:profile-id (:id member)}
|
||||
(role->params role))]
|
||||
|
||||
;; Do not allow blocked users to join teams.
|
||||
(when (:is-blocked member)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/profiles-per-team
|
||||
::quotes/profile-id (:id member)
|
||||
::quotes/team-id team-id})
|
||||
|
||||
;; Insert the member to the team
|
||||
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
|
||||
|
||||
;; Delete any request
|
||||
(db/delete! conn :team-access-request
|
||||
{:team-id team-id :requester-id (:id member)})
|
||||
|
||||
;; Delete any invitation
|
||||
(db/delete! conn :team-invitation
|
||||
{:team-id team-id :email-to (:email member)})
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/join-team
|
||||
:public-uri (cf/get :public-uri)
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:team-id (:id team)})))
|
||||
|
||||
(def sql:valid-requests-email
|
||||
"SELECT p.email
|
||||
FROM team_access_request AS tr
|
||||
JOIN profile AS p ON (tr.requester_id = p.id)
|
||||
WHERE tr.team_id = ?
|
||||
AND tr.auto_join_until > now()")
|
||||
|
||||
(defn- get-valid-requests-email
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:valid-requests-email team-id]))
|
||||
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role schema:role]
|
||||
[:emails ::sm/set-of-emails]])
|
||||
[:emails [::sm/set ::sm/email]]])
|
||||
|
||||
(def ^:private max-invitations-by-request-threshold
|
||||
"The number of invitations can be sent in a single rpc request"
|
||||
25)
|
||||
|
||||
(sv/defmethod ::create-team-invitations
|
||||
"A rpc call that allow to send a single or multiple invitations to
|
||||
@@ -833,6 +924,12 @@
|
||||
team (db/get-by-id conn :team team-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
|
||||
(run! (partial quotes/check-quote! conn)
|
||||
(list {::quotes/id ::quotes/invitations-per-team
|
||||
::quotes/profile-id profile-id
|
||||
@@ -847,13 +944,14 @@
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
;; First check if the current profile is allowed to send emails.
|
||||
(when-not (eml/allow-send-emails? conn profile)
|
||||
(ex/raise :type :validation
|
||||
:code :profile-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
;; Check if the current profile is allowed to send emails.
|
||||
(check-valid-email-muted conn profile)
|
||||
|
||||
(let [cfg (assoc cfg ::db/conn conn)
|
||||
|
||||
(let [requested (into #{} (map :email) (get-valid-requests-email conn team-id))
|
||||
emails-to-add (filter #(contains? requested %) emails)
|
||||
emails (remove #(contains? requested %) emails)
|
||||
cfg (assoc cfg ::db/conn conn)
|
||||
members (->> (db/exec! conn [sql:team-members team-id])
|
||||
(into #{} (map :email)))
|
||||
|
||||
@@ -869,6 +967,10 @@
|
||||
(assoc :role role))))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
;; For requested invitations, do not send invitation emails, add the user directly to the team
|
||||
(doseq [email emails-to-add]
|
||||
(add-user-to-team conn profile team email role))
|
||||
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}})))))
|
||||
@@ -877,10 +979,10 @@
|
||||
|
||||
(def ^:private schema:create-team-with-invitations
|
||||
[:map {:title "create-team-with-invitations"}
|
||||
[:name :string]
|
||||
[:name [:string {:max 250}]]
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:emails ::sm/set-of-emails]
|
||||
[:emails [::sm/set ::sm/email]]
|
||||
[:role schema:role]])
|
||||
|
||||
(sv/defmethod ::create-team-with-invitations
|
||||
@@ -902,6 +1004,12 @@
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
|
||||
(when (> (count emails) max-invitations-by-request-threshold)
|
||||
(ex/raise :type :validation
|
||||
:code :max-invitations-by-request
|
||||
:hint "the maximum of invitation on single request is reached"
|
||||
:threshold max-invitations-by-request-threshold))
|
||||
|
||||
(let [props {:name name :features features}
|
||||
event (-> (audit/event-from-rpc-params params)
|
||||
(assoc ::audit/name "create-team")
|
||||
@@ -1007,3 +1115,130 @@
|
||||
:email-to (profile/clean-email email)}
|
||||
{::db/return-keys true})]
|
||||
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}})))))
|
||||
|
||||
|
||||
|
||||
|
||||
;; --- Mutation: Request Team Invitation
|
||||
|
||||
(def sql:upsert-team-access-request
|
||||
"INSERT INTO team_access_request (id, team_id, requester_id, valid_until, auto_join_until)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON conflict(id)
|
||||
DO UPDATE SET valid_until = ?, auto_join_until = ?, updated_at = now()
|
||||
RETURNING *")
|
||||
|
||||
|
||||
(def sql:team-access-request
|
||||
"SELECT id, (valid_until < now()) AS expired
|
||||
FROM team_access_request
|
||||
WHERE team_id = ?
|
||||
AND requester_id = ?")
|
||||
|
||||
(def sql:team-owner
|
||||
"SELECT profile_id
|
||||
FROM team_profile_rel
|
||||
WHERE team_id = ?
|
||||
AND is_owner = true")
|
||||
|
||||
|
||||
(defn- create-team-access-request
|
||||
[{:keys [::db/conn] :as cfg} {:keys [team requester team-owner file is-viewer] :as params}]
|
||||
(let [old-request (->> (db/exec-one! conn [sql:team-access-request (:id team) (:id requester)])
|
||||
(decode-row))]
|
||||
(when (false? (:expired old-request))
|
||||
(ex/raise :type :validation
|
||||
:code :request-already-sent
|
||||
:hint "you have already made a request to join this team less than 24 hours ago"))
|
||||
|
||||
(let [id (or (:id old-request) (uuid/next))
|
||||
valid_until (dt/in-future "24h")
|
||||
auto_join_until (dt/in-future "168h") ;; 7 days
|
||||
request (db/exec-one! conn [sql:upsert-team-access-request
|
||||
id (:id team) (:id requester) valid_until auto_join_until
|
||||
valid_until auto_join_until])
|
||||
factory (cond
|
||||
(and (some? file) (:is-default team) is-viewer)
|
||||
eml/request-file-access-yourpenpot-view
|
||||
(and (some? file) (:is-default team))
|
||||
eml/request-file-access-yourpenpot
|
||||
(some? file)
|
||||
eml/request-file-access
|
||||
:else
|
||||
eml/request-team-access)
|
||||
page-id (when (some? file)
|
||||
(-> file :data :pages first))]
|
||||
|
||||
;; TODO needs audit?
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory factory
|
||||
:public-uri (cf/get :public-uri)
|
||||
:to (:email team-owner)
|
||||
:requested-by (:fullname requester)
|
||||
:requested-by-email (:email requester)
|
||||
:team-name (:name team)
|
||||
:team-id (:id team)
|
||||
:file-name (:name file)
|
||||
:file-id (:id file)
|
||||
:page-id page-id})
|
||||
|
||||
request)))
|
||||
|
||||
|
||||
(def ^:private schema:create-team-access-request
|
||||
[:and
|
||||
[:map {:title "create-team-access-request"}
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:team-id {:optional true} ::sm/uuid]
|
||||
[:is-viewer {:optional true} ::sm/boolean]]
|
||||
|
||||
[:fn (fn [params]
|
||||
(or (contains? params :file-id)
|
||||
(contains? params :team-id)))]])
|
||||
|
||||
|
||||
(sv/defmethod ::create-team-access-request
|
||||
"A rpc call that allow to request for an invitations to join the team."
|
||||
{::doc/added "2.2.0"
|
||||
::sm/params schema:create-team-access-request}
|
||||
[cfg {:keys [::rpc/profile-id file-id team-id is-viewer] :as params}]
|
||||
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
|
||||
(let [requester (db/get-by-id conn :profile profile-id)
|
||||
team-id (if (some? team-id)
|
||||
team-id
|
||||
(:id (get-team-for-file conn file-id)))
|
||||
team (db/get-by-id conn :team team-id)
|
||||
owner-id (->> (db/exec! conn [sql:team-owner (:id team)])
|
||||
(map decode-row)
|
||||
(first)
|
||||
:profile-id)
|
||||
team-owner (db/get-by-id conn :profile owner-id)
|
||||
file (when (some? file-id)
|
||||
(db/get* conn :file
|
||||
{:id file-id}
|
||||
{::sql/columns [:id :name :data]}))
|
||||
file (when (some? file)
|
||||
(assoc file :data (blob/decode (:data file))))]
|
||||
|
||||
;;TODO needs quotes?
|
||||
|
||||
(when (or (nil? requester) (nil? team) (nil? team-owner) (and (some? file-id) (nil? file)))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-parameters))
|
||||
|
||||
;; Check that the requester is not muted
|
||||
(check-valid-email-muted conn requester)
|
||||
|
||||
;; Check that the owner is not marked as bounce nor spam
|
||||
(check-valid-email-bounce conn (:email team-owner) false)
|
||||
(check-valid-email-spam conn (:email team-owner) true)
|
||||
|
||||
(let [request (create-team-access-request
|
||||
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]
|
||||
(when request
|
||||
(with-meta {:request request}
|
||||
{::audit/props {:request 1}})))))))
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(ns app.rpc.commands.verify-token
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
@@ -24,21 +24,19 @@
|
||||
[app.tokens :as tokens]
|
||||
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::iss keyword?)
|
||||
(s/def ::exp ::us/inst)
|
||||
[app.util.time :as dt]))
|
||||
|
||||
(defmulti process-token (fn [_ _ claims] (:iss claims)))
|
||||
|
||||
(s/def ::verify-token
|
||||
(s/keys :req-un [::token]
|
||||
:opt [::rpc/profile-id]))
|
||||
(def ^:private schema:verify-token
|
||||
[:map {:title "verify-token"}
|
||||
[:token [:string {:max 5000}]]])
|
||||
|
||||
(sv/defmethod ::verify-token
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth}
|
||||
::doc/module :auth
|
||||
::sm/params schema:verify-token}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [claims (tokens/verify (::setup/props cfg) {:token token})
|
||||
@@ -84,16 +82,8 @@
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/get-profile conn profile-id {::sql/for-update true})
|
||||
props (merge (:props profile)
|
||||
(:props claims))]
|
||||
(when (not= props (:props profile))
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id}))
|
||||
|
||||
(let [profile (assoc profile :props props)]
|
||||
(assoc claims :profile profile))))
|
||||
(let [profile (profile/get-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
@@ -130,28 +120,34 @@
|
||||
(db/delete! conn :team-invitation
|
||||
{:team-id team-id :email-to member-email})
|
||||
|
||||
;; Delete any request
|
||||
(db/delete! conn :team-access-request
|
||||
{:team-id team-id :requester-id (:id member)})
|
||||
|
||||
(assoc member :is-active true)))
|
||||
|
||||
(s/def ::spec.team-invitation/profile-id ::us/uuid)
|
||||
(s/def ::spec.team-invitation/role ::us/keyword)
|
||||
(s/def ::spec.team-invitation/team-id ::us/uuid)
|
||||
(s/def ::spec.team-invitation/member-email ::us/email)
|
||||
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
|
||||
(def schema:team-invitation-claims
|
||||
[:map {:title "TeamInvitationClaims"}
|
||||
[:iss :keyword]
|
||||
[:exp ::dt/instant]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:role teams/schema:role]
|
||||
[:team-id ::sm/uuid]
|
||||
[:member-email ::sm/email]
|
||||
[:member-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(s/def ::team-invitation-claims
|
||||
(s/keys :req-un [::iss ::exp
|
||||
::spec.team-invitation/profile-id
|
||||
::spec.team-invitation/role
|
||||
::spec.team-invitation/team-id
|
||||
::spec.team-invitation/member-email]
|
||||
:opt-un [::spec.team-invitation/member-id]))
|
||||
(def valid-team-invitation-claims?
|
||||
(sm/lazy-validator schema:team-invitation-claims))
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[{:keys [conn] :as cfg}
|
||||
{:keys [::rpc/profile-id token] :as params}
|
||||
{:keys [member-id team-id member-email] :as claims}]
|
||||
|
||||
(us/verify! ::team-invitation-claims claims)
|
||||
(when-not (valid-team-invitation-claims? claims)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-invitation-token
|
||||
:hint "invitation token contains unexpected data"))
|
||||
|
||||
(let [invitation (db/get* conn :team-invitation
|
||||
{:team-id team-id :email-to member-email})
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
(assoc ::perms perms)
|
||||
(assoc :profile-id profile-id))]
|
||||
|
||||
;; When we have neither profile nor share, we just return a not
|
||||
;; When we have neither profile nor share, we just return a not
|
||||
;; found response to the user.
|
||||
(when-not perms
|
||||
(ex/raise :type :not-found
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
@@ -19,7 +19,6 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn decode-row
|
||||
@@ -29,18 +28,6 @@
|
||||
|
||||
;; --- Mutation: Create Webhook
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::uri ::us/uri)
|
||||
(s/def ::is-active ::us/boolean)
|
||||
(s/def ::mtype
|
||||
#{"application/json"
|
||||
"application/transit+json"})
|
||||
|
||||
(s/def ::create-webhook
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id ::uri ::mtype]
|
||||
:opt-un [::is-active]))
|
||||
|
||||
;; NOTE: for now the quote is hardcoded but this need to be solved in
|
||||
;; a more universal way for handling properly object quotes
|
||||
(def max-hooks-for-team 8)
|
||||
@@ -99,31 +86,49 @@
|
||||
{::db/return-keys true})
|
||||
(decode-row)))
|
||||
|
||||
|
||||
(def valid-mtypes
|
||||
#{"application/json"
|
||||
"application/transit+json"})
|
||||
|
||||
(def ^:private schema:create-webhook
|
||||
[:map {:title "create-webhook"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:uri ::sm/uri]
|
||||
[:mtype [::sm/one-of {:format "string"} valid-mtypes]]])
|
||||
|
||||
(sv/defmethod ::create-webhook
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-webhook}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
|
||||
(check-edition-permissions! pool profile-id team-id)
|
||||
(validate-quotes! cfg params)
|
||||
(validate-webhook! cfg nil params)
|
||||
(insert-webhook! cfg params))
|
||||
|
||||
(s/def ::update-webhook
|
||||
(s/keys :req-un [::id ::uri ::mtype ::is-active]))
|
||||
(def ^:private schema:update-webhook
|
||||
[:map {:title "update-webhook"}
|
||||
[:id ::sm/uuid]
|
||||
[:uri ::sm/uri]
|
||||
[:mtype [::sm/one-of {:format "string"} valid-mtypes]]
|
||||
[:is-active ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::update-webhook
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:update-webhook}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
|
||||
(check-edition-permissions! pool profile-id (:team-id whook))
|
||||
(validate-webhook! cfg whook params)
|
||||
(update-webhook! cfg whook params)))
|
||||
|
||||
(s/def ::delete-webhook
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::id]))
|
||||
(def ^:private schema:delete-webhook
|
||||
[:map {:title "delete-webhook"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::delete-webhook
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:delete-webhook}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [whook (-> (db/get conn :webhook {:id id}) decode-row)]
|
||||
@@ -133,16 +138,17 @@
|
||||
|
||||
;; --- Query: Webhooks
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::get-webhooks
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::team-id]))
|
||||
|
||||
(def sql:get-webhooks
|
||||
"select id, uri, mtype, is_active, error_code, error_count
|
||||
from webhook where team_id = ? order by uri")
|
||||
|
||||
(def ^:private schema:get-webhooks
|
||||
[:map {:title "get-webhooks"}
|
||||
[:team-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::get-webhooks
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:get-webhooks}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! conn profile-id team-id)
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[malli.transform :as mt]
|
||||
[pretty-spec.core :as ps]
|
||||
[ring.response :as-alias rres]))
|
||||
|
||||
@@ -98,77 +97,79 @@
|
||||
;; OPENAPI / SWAGGER (v3.1)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def output-transformer
|
||||
(mt/transformer
|
||||
sm/default-transformer
|
||||
(mt/key-transformer {:encode str/camel
|
||||
:decode (comp keyword str/kebab)})))
|
||||
|
||||
(defn prepare-openapi-context
|
||||
[methods]
|
||||
(letfn [(gen-response-doc [tsx schema]
|
||||
(let [schema (sm/schema schema)
|
||||
example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:default
|
||||
{:description "A default response"
|
||||
:content
|
||||
{"application/json"
|
||||
{:schema tsx
|
||||
:example example}}}}))
|
||||
(let [definitions (atom {})
|
||||
options {:registry sr/default-registry
|
||||
::oapi/definitions-path "#/components/schemas/"
|
||||
::oapi/definitions definitions}
|
||||
|
||||
(gen-params-doc [tsx schema]
|
||||
(let [example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:required true
|
||||
:content
|
||||
{"application/json"
|
||||
{:schema tsx
|
||||
:example example}}}))
|
||||
output-transformer
|
||||
(sm/json-transformer)
|
||||
|
||||
(gen-method-doc [options mdata]
|
||||
(let [pschema (::sm/params mdata)
|
||||
rschema (::sm/result mdata)
|
||||
gen-response-doc
|
||||
(fn [tsx schema]
|
||||
(let [schema (sm/schema schema)
|
||||
example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:default
|
||||
{:description "A default response"
|
||||
:content
|
||||
{"application/json"
|
||||
{:schema tsx
|
||||
:example example}}}}))
|
||||
|
||||
sparams (-> pschema (oapi/transform options) (gen-params-doc pschema))
|
||||
sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema))
|
||||
gen-params-doc
|
||||
(fn [tsx schema]
|
||||
(let [example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:required true
|
||||
:content
|
||||
{"application/json"
|
||||
{:schema tsx
|
||||
:example example}}}))
|
||||
|
||||
rpost {:description (::sv/docstring mdata)
|
||||
:deprecated (::deprecated mdata false)
|
||||
:requestBody sparams}
|
||||
gen-method-doc
|
||||
(fn [mdata]
|
||||
(let [pschema (::sm/params mdata)
|
||||
rschema (::sm/result mdata)
|
||||
|
||||
rpost (cond-> rpost
|
||||
(some? sresp)
|
||||
(assoc :responses sresp))]
|
||||
sparams (-> pschema (oapi/transform options) (gen-params-doc pschema))
|
||||
sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema))
|
||||
|
||||
{:name (-> mdata ::sv/name d/name)
|
||||
:module (-> (:ns mdata) (str/split ".") last)
|
||||
:repr {:post rpost}}))]
|
||||
rpost {:description (::sv/docstring mdata)
|
||||
:deprecated (::deprecated mdata false)
|
||||
:requestBody sparams}
|
||||
|
||||
(let [definitions (atom {})
|
||||
options {:registry sr/default-registry
|
||||
::oapi/definitions-path "#/components/schemas/"
|
||||
::oapi/definitions definitions}
|
||||
rpost (cond-> rpost
|
||||
(some? sresp)
|
||||
(assoc :responses sresp))]
|
||||
|
||||
paths (binding [oapi/*definitions* definitions]
|
||||
(->> methods
|
||||
(map (comp first val))
|
||||
(filter ::sm/params)
|
||||
(map (partial gen-method-doc options))
|
||||
(sort-by (juxt :module :name))
|
||||
(map (fn [doc]
|
||||
[(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
|
||||
(into {})))]
|
||||
{:openapi "3.0.0"
|
||||
:info {:version (:main cf/version)}
|
||||
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
|
||||
{:name (-> mdata ::sv/name d/name)
|
||||
:module (-> (:ns mdata) (str/split ".") last)
|
||||
:repr {:post rpost}}))
|
||||
|
||||
paths
|
||||
(binding [oapi/*definitions* definitions]
|
||||
(->> methods
|
||||
(map (comp first val))
|
||||
(filter ::sm/params)
|
||||
(map gen-method-doc)
|
||||
(sort-by (juxt :module :name))
|
||||
(map (fn [doc]
|
||||
[(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
|
||||
(into {})))]
|
||||
|
||||
{:openapi "3.0.0"
|
||||
:info {:version (:main cf/version)}
|
||||
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
|
||||
;; :description "penpot backend"
|
||||
}]
|
||||
:security
|
||||
{:api_key []}
|
||||
}]
|
||||
:security
|
||||
{:api_key []}
|
||||
|
||||
:paths paths
|
||||
:components {:schemas @definitions}})))
|
||||
:paths paths
|
||||
:components {:schemas @definitions}}))
|
||||
|
||||
(defn openapi-json-handler
|
||||
[context]
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(sm/def! ::permissions
|
||||
(sm/register! ::permissions
|
||||
[:map {:title "Permissions"}
|
||||
[:type {:gen/elements [:membership :share-link]} :keyword]
|
||||
[:is-owner :boolean]
|
||||
[:is-admin :boolean]
|
||||
[:can-edit :boolean]
|
||||
[:can-read :boolean]
|
||||
[:is-logged :boolean]])
|
||||
[:is-owner ::sm/boolean]
|
||||
[:is-admin ::sm/boolean]
|
||||
[:can-edit ::sm/boolean]
|
||||
[:can-read ::sm/boolean]
|
||||
[:is-logged ::sm/boolean]])
|
||||
|
||||
|
||||
(s/def ::role #{:admin :owner :editor :viewer})
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
[::team-id {:optional true} ::sm/uuid]
|
||||
[::project-id {:optional true} ::sm/uuid]
|
||||
[::file-id {:optional true} ::sm/uuid]
|
||||
[::incr {:optional true} [:int {:min 0}]]
|
||||
[::incr {:optional true} [::sm/int {:min 0}]]
|
||||
[::id :keyword]
|
||||
[::profile-id ::sm/uuid]]))
|
||||
|
||||
|
||||
64
backend/src/app/setup/welcome_file.clj
Normal file
64
backend/src/app/setup/welcome_file.clj
Normal file
@@ -0,0 +1,64 @@
|
||||
;; 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.setup.welcome-file
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.files-update :as fupdate]
|
||||
[app.rpc.commands.management :as management]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.templates :as tmpl]
|
||||
[app.worker :as-alias wrk]))
|
||||
|
||||
(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb")
|
||||
(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445")
|
||||
|
||||
(def ^:private update-path
|
||||
[:data :pages-index page-id :objects shape-id
|
||||
:content :children 0 :children 0 :children 0])
|
||||
|
||||
(def ^:private sql:mark-file-object-thumbnails-deleted
|
||||
"UPDATE file_tagged_object_thumbnail
|
||||
SET deleted_at = now()
|
||||
WHERE file_id = ?")
|
||||
|
||||
(def ^:private sql:mark-file-thumbnail-deleted
|
||||
"UPDATE file_thumbnail
|
||||
SET deleted_at = now()
|
||||
WHERE file_id = ?")
|
||||
|
||||
(defn- update-welcome-shape
|
||||
[_ file name]
|
||||
(let [text (str "Welcome to Penpot, " name "!")]
|
||||
(-> file
|
||||
(update-in update-path assoc :text text)
|
||||
(update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!")
|
||||
(update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data))))
|
||||
|
||||
(defn create-welcome-file
|
||||
[cfg {:keys [id fullname] :as profile}]
|
||||
(try
|
||||
(let [cfg (dissoc cfg ::db/conn)
|
||||
params {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)}
|
||||
template-stream (tmpl/get-template-stream cfg "welcome")
|
||||
file-id (-> (management/clone-template cfg params template-stream)
|
||||
first)]
|
||||
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(fupdate/update-file! cfg file-id update-welcome-shape fullname)
|
||||
(profile/update-profile-props cfg id {:welcome-file-id file-id})
|
||||
(db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id])
|
||||
(db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id]))))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error on create welcome file " :cause cause))))
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
:created-at (:created-at file)
|
||||
:modified-at (:modified-at file)
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)})))
|
||||
|
||||
|
||||
@@ -727,13 +727,15 @@
|
||||
deleted 0
|
||||
total 0]
|
||||
(if-let [email (first emails)]
|
||||
(if-let [profile (db/get* system :profile
|
||||
{:email (str/lower email)}
|
||||
{::db/remove-deleted false})]
|
||||
(if-let [profile (some-> (db/get* system :profile
|
||||
{:email (str/lower email)}
|
||||
{::db/remove-deleted false})
|
||||
(profile/decode-row))]
|
||||
(do
|
||||
(audit/insert! system
|
||||
{::audit/name "delete-profile"
|
||||
::audit/type "action"
|
||||
::audit/profile-id (:id profile)
|
||||
::audit/tracked-at deleted-at
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/context {:triggered-by "srepl"
|
||||
@@ -752,7 +754,7 @@
|
||||
{:deleted deleted :total total})))]
|
||||
|
||||
(let [path (fs/path path)
|
||||
deleted-at (dt/minus (dt/now) cf/deletion-delay)]
|
||||
deleted-at (dt/minus (dt/now) (cf/get-deletion-delay))]
|
||||
|
||||
(when-not (fs/exists? path)
|
||||
(throw (ex-info "path does not exists" {:path path})))
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
(ns app.storage
|
||||
"Objects storage abstraction layer."
|
||||
(:refer-clojure :exclude [resolve])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.storage.fs :as sfs]
|
||||
[app.storage.impl :as impl]
|
||||
@@ -18,16 +20,23 @@
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.fs :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p])
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
java.io.InputStream))
|
||||
|
||||
(defn get-legacy-backend
|
||||
[]
|
||||
(let [name (cf/get :assets-storage-backend)]
|
||||
(case name
|
||||
:assets-fs :fs
|
||||
:assets-s3 :s3
|
||||
:fs)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Storage Module State
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::id #{:assets-fs :assets-s3 :s3 :fs})
|
||||
(s/def ::id #{:assets-fs :assets-s3 :fs :s3})
|
||||
(s/def ::s3 ::ss3/backend)
|
||||
(s/def ::fs ::sfs/backend)
|
||||
(s/def ::type #{:fs :s3})
|
||||
@@ -45,11 +54,13 @@
|
||||
[_ {:keys [::backends ::db/pool] :as cfg}]
|
||||
(-> (d/without-nils cfg)
|
||||
(assoc ::backends (d/without-nils backends))
|
||||
(assoc ::db/pool-or-conn pool)))
|
||||
(assoc ::backend (or (get-legacy-backend)
|
||||
(cf/get :objects-storage-backend :fs)))
|
||||
(assoc ::db/connectable pool)))
|
||||
|
||||
(s/def ::backend keyword?)
|
||||
(s/def ::storage
|
||||
(s/keys :req [::backends ::db/pool ::db/pool-or-conn]
|
||||
(s/keys :req [::backends ::db/pool ::db/connectable]
|
||||
:opt [::backend]))
|
||||
|
||||
(s/def ::storage-with-backend
|
||||
@@ -61,23 +72,26 @@
|
||||
|
||||
(defn get-metadata
|
||||
[params]
|
||||
(into {}
|
||||
(remove (fn [[k _]] (qualified-keyword? k)))
|
||||
params))
|
||||
(reduce-kv (fn [res k _]
|
||||
(if (qualified-keyword? k)
|
||||
(dissoc res k)
|
||||
res))
|
||||
params
|
||||
params))
|
||||
|
||||
(defn- get-database-object-by-hash
|
||||
[pool-or-conn backend bucket hash]
|
||||
[connectable backend bucket hash]
|
||||
(let [sql (str "select * from storage_object "
|
||||
" where (metadata->>'~:hash') = ? "
|
||||
" and (metadata->>'~:bucket') = ? "
|
||||
" and backend = ?"
|
||||
" and deleted_at is null"
|
||||
" limit 1")]
|
||||
(some-> (db/exec-one! pool-or-conn [sql hash bucket (name backend)])
|
||||
(some-> (db/exec-one! connectable [sql hash bucket (name backend)])
|
||||
(update :metadata db/decode-transit-pgobject))))
|
||||
|
||||
(defn- create-database-object
|
||||
[{:keys [::backend ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}]
|
||||
[{:keys [::backend ::db/connectable]} {:keys [::content ::expired-at ::touched-at ::touch] :as params}]
|
||||
(let [id (or (:id params) (uuid/random))
|
||||
mdata (cond-> (get-metadata params)
|
||||
(satisfies? impl/IContentHash content)
|
||||
@@ -86,7 +100,9 @@
|
||||
:always
|
||||
(dissoc :id))
|
||||
|
||||
;; FIXME: touch object on deduplicated put operation ??
|
||||
touched-at (if touch
|
||||
(or touched-at (dt/now))
|
||||
touched-at)
|
||||
|
||||
;; NOTE: for now we don't reuse the deleted objects, but in
|
||||
;; futute we can consider reusing deleted objects if we
|
||||
@@ -95,10 +111,20 @@
|
||||
result (when (and (::deduplicate? params)
|
||||
(:hash mdata)
|
||||
(:bucket mdata))
|
||||
(get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata)))
|
||||
(let [result (get-database-object-by-hash connectable backend
|
||||
(:bucket mdata)
|
||||
(:hash mdata))]
|
||||
(if touch
|
||||
(do
|
||||
(db/update! connectable :storage-object
|
||||
{:touched-at touched-at}
|
||||
{:id (:id result)}
|
||||
{::db/return-keys false})
|
||||
(assoc result :touced-at touched-at))
|
||||
result)))
|
||||
|
||||
result (or result
|
||||
(-> (db/insert! pool-or-conn :storage-object
|
||||
(-> (db/insert! connectable :storage-object
|
||||
{:id id
|
||||
:size (impl/get-size content)
|
||||
:backend (name backend)
|
||||
@@ -154,9 +180,9 @@
|
||||
(dm/export impl/object?)
|
||||
|
||||
(defn get-object
|
||||
[{:keys [::db/pool-or-conn] :as storage} id]
|
||||
[{:keys [::db/connectable] :as storage} id]
|
||||
(us/assert! ::storage storage)
|
||||
(retrieve-database-object pool-or-conn id))
|
||||
(retrieve-database-object connectable id))
|
||||
|
||||
(defn put-object!
|
||||
"Creates a new object with the provided content."
|
||||
@@ -172,10 +198,10 @@
|
||||
|
||||
(defn touch-object!
|
||||
"Mark object as touched."
|
||||
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
|
||||
[{:keys [::db/connectable] :as storage} object-or-id]
|
||||
(us/assert! ::storage storage)
|
||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
|
||||
(-> (db/update! pool-or-conn :storage-object
|
||||
(-> (db/update! connectable :storage-object
|
||||
{:touched-at (dt/now)}
|
||||
{:id id})
|
||||
(db/get-update-count)
|
||||
@@ -195,11 +221,10 @@
|
||||
"Returns a byte array of object content."
|
||||
[storage object]
|
||||
(us/assert! ::storage storage)
|
||||
(if (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(when (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(-> (impl/resolve-backend storage (:backend object))
|
||||
(impl/get-object-bytes object))
|
||||
(p/resolved nil)))
|
||||
(impl/get-object-bytes object))))
|
||||
|
||||
(defn get-object-url
|
||||
([storage object]
|
||||
@@ -223,13 +248,26 @@
|
||||
(-> (impl/get-object-url backend object nil) file-url->path))))
|
||||
|
||||
(defn del-object!
|
||||
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
|
||||
[{:keys [::db/connectable] :as storage} object-or-id]
|
||||
(us/assert! ::storage storage)
|
||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
|
||||
res (db/update! pool-or-conn :storage-object
|
||||
res (db/update! connectable :storage-object
|
||||
{:deleted-at (dt/now)}
|
||||
{:id id})]
|
||||
(pos? (db/get-update-count res))))
|
||||
|
||||
(dm/export impl/resolve-backend)
|
||||
(dm/export impl/calculate-hash)
|
||||
|
||||
(defn configure
|
||||
[storage connectable]
|
||||
(assoc storage ::db/connectable connectable))
|
||||
|
||||
(defn resolve
|
||||
"Resolves the storage instance with preconfigured backend. You can
|
||||
specify to reuse the database connection from provided
|
||||
cfg/system (default false)."
|
||||
[cfg & {:as opts}]
|
||||
(let [storage (::storage cfg)]
|
||||
(if (::db/reuse-conn opts false)
|
||||
(configure storage (db/get-connectable cfg))
|
||||
storage)))
|
||||
|
||||
@@ -121,5 +121,3 @@
|
||||
:total total)
|
||||
|
||||
{:deleted total}))))))
|
||||
|
||||
|
||||
|
||||
@@ -28,58 +28,80 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private sql:get-team-font-variant-nrefs
|
||||
"SELECT ((SELECT count(*) FROM team_font_variant WHERE woff1_file_id = ?) +
|
||||
(SELECT count(*) FROM team_font_variant WHERE woff2_file_id = ?) +
|
||||
(SELECT count(*) FROM team_font_variant WHERE otf_file_id = ?) +
|
||||
(SELECT count(*) FROM team_font_variant WHERE ttf_file_id = ?)) AS nrefs")
|
||||
(def ^:private sql:has-team-font-variant-refs
|
||||
"SELECT ((SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE woff1_file_id = ?)) OR
|
||||
(SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE woff2_file_id = ?)) OR
|
||||
(SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE otf_file_id = ?)) OR
|
||||
(SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE ttf_file_id = ?))) AS has_refs")
|
||||
|
||||
(defn- get-team-font-variant-nrefs
|
||||
(defn- has-team-font-variant-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:get-team-font-variant-nrefs id id id id])
|
||||
(get :nrefs)))
|
||||
|
||||
(-> (db/exec-one! conn [sql:has-team-font-variant-refs id id id id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:get-file-media-object-nrefs
|
||||
"SELECT ((SELECT count(*) FROM file_media_object WHERE media_id = ?) +
|
||||
(SELECT count(*) FROM file_media_object WHERE thumbnail_id = ?)) AS nrefs")
|
||||
sql:has-file-media-object-refs
|
||||
"SELECT ((SELECT EXISTS (SELECT 1 FROM file_media_object WHERE media_id = ?)) OR
|
||||
(SELECT EXISTS (SELECT 1 FROM file_media_object WHERE thumbnail_id = ?))) AS has_refs")
|
||||
|
||||
(defn- get-file-media-object-nrefs
|
||||
(defn- has-file-media-object-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:get-file-media-object-nrefs id id])
|
||||
(get :nrefs)))
|
||||
(-> (db/exec-one! conn [sql:has-file-media-object-refs id id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private sql:has-profile-refs
|
||||
"SELECT ((SELECT EXISTS (SELECT 1 FROM profile WHERE photo_id = ?)) OR
|
||||
(SELECT EXISTS (SELECT 1 FROM team WHERE photo_id = ?))) AS has_refs")
|
||||
|
||||
(def ^:private sql:get-profile-nrefs
|
||||
"SELECT ((SELECT count(*) FROM profile WHERE photo_id = ?) +
|
||||
(SELECT count(*) FROM team WHERE photo_id = ?)) AS nrefs")
|
||||
|
||||
(defn- get-profile-nrefs
|
||||
(defn- has-profile-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:get-profile-nrefs id id])
|
||||
(get :nrefs)))
|
||||
|
||||
(-> (db/exec-one! conn [sql:has-profile-refs id id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:get-file-object-thumbnail-nrefs
|
||||
"SELECT (SELECT count(*) FROM file_tagged_object_thumbnail WHERE media_id = ?) AS nrefs")
|
||||
sql:has-file-object-thumbnail-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file_tagged_object_thumbnail WHERE media_id = ?) AS has_refs")
|
||||
|
||||
(defn- get-file-object-thumbnails
|
||||
(defn- has-file-object-thumbnails-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:get-file-object-thumbnail-nrefs id])
|
||||
(get :nrefs)))
|
||||
|
||||
(-> (db/exec-one! conn [sql:has-file-object-thumbnail-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:get-file-thumbnail-nrefs
|
||||
"SELECT (SELECT count(*) FROM file_thumbnail WHERE media_id = ?) AS nrefs")
|
||||
sql:has-file-thumbnail-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file_thumbnail WHERE media_id = ?) AS has_refs")
|
||||
|
||||
(defn- get-file-thumbnails
|
||||
(defn- has-file-thumbnails-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:get-file-thumbnail-nrefs id])
|
||||
(get :nrefs)))
|
||||
(-> (db/exec-one! conn [sql:has-file-thumbnail-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:has-file-data-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file WHERE data_ref_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-data-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-data-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:has-file-data-fragment-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file_data_fragment WHERE data_ref_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-data-fragment-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-data-fragment-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:has-file-change-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file_change WHERE data_ref_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-change-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-change-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private sql:mark-freeze-in-bulk
|
||||
"UPDATE storage_object
|
||||
@@ -91,7 +113,6 @@
|
||||
(let [ids (db/create-array conn "uuid" ids)]
|
||||
(db/exec-one! conn [sql:mark-freeze-in-bulk ids])))
|
||||
|
||||
|
||||
(def ^:private sql:mark-delete-in-bulk
|
||||
"UPDATE storage_object
|
||||
SET deleted_at = now(),
|
||||
@@ -123,25 +144,24 @@
|
||||
"file-media-object"))
|
||||
|
||||
(defn- process-objects!
|
||||
[conn get-fn ids bucket]
|
||||
[conn has-refs? ids bucket]
|
||||
(loop [to-freeze #{}
|
||||
to-delete #{}
|
||||
ids (seq ids)]
|
||||
(if-let [id (first ids)]
|
||||
(let [nrefs (get-fn conn id)]
|
||||
(if (pos? nrefs)
|
||||
(do
|
||||
(l/debug :hint "processing object"
|
||||
:id (str id)
|
||||
:status "freeze"
|
||||
:bucket bucket :refs nrefs)
|
||||
(recur (conj to-freeze id) to-delete (rest ids)))
|
||||
(do
|
||||
(l/debug :hint "processing object"
|
||||
:id (str id)
|
||||
:status "delete"
|
||||
:bucket bucket :refs nrefs)
|
||||
(recur to-freeze (conj to-delete id) (rest ids)))))
|
||||
(if (has-refs? conn id)
|
||||
(do
|
||||
(l/debug :hint "processing object"
|
||||
:id (str id)
|
||||
:status "freeze"
|
||||
:bucket bucket)
|
||||
(recur (conj to-freeze id) to-delete (rest ids)))
|
||||
(do
|
||||
(l/debug :hint "processing object"
|
||||
:id (str id)
|
||||
:status "delete"
|
||||
:bucket bucket)
|
||||
(recur to-freeze (conj to-delete id) (rest ids))))
|
||||
(do
|
||||
(some->> (seq to-freeze) (mark-freeze-in-bulk! conn))
|
||||
(some->> (seq to-delete) (mark-delete-in-bulk! conn))
|
||||
@@ -150,15 +170,26 @@
|
||||
(defn- process-bucket!
|
||||
[conn bucket ids]
|
||||
(case bucket
|
||||
"file-media-object" (process-objects! conn get-file-media-object-nrefs ids bucket)
|
||||
"team-font-variant" (process-objects! conn get-team-font-variant-nrefs ids bucket)
|
||||
"file-object-thumbnail" (process-objects! conn get-file-object-thumbnails ids bucket)
|
||||
"file-thumbnail" (process-objects! conn get-file-thumbnails ids bucket)
|
||||
"profile" (process-objects! conn get-profile-nrefs ids bucket)
|
||||
"file-media-object" (process-objects! conn has-file-media-object-refs? ids bucket)
|
||||
"team-font-variant" (process-objects! conn has-team-font-variant-refs? ids bucket)
|
||||
"file-object-thumbnail" (process-objects! conn has-file-object-thumbnails-refs? ids bucket)
|
||||
"file-thumbnail" (process-objects! conn has-file-thumbnails-refs? ids bucket)
|
||||
"profile" (process-objects! conn has-profile-refs? ids bucket)
|
||||
"file-data" (process-objects! conn has-file-data-refs? ids bucket)
|
||||
"file-data-fragment" (process-objects! conn has-file-data-fragment-refs? ids bucket)
|
||||
"file-change" (process-objects! conn has-file-change-refs? ids bucket)
|
||||
(ex/raise :type :internal
|
||||
:code :unexpected-unknown-reference
|
||||
:hint (dm/fmt "unknown reference %" bucket))))
|
||||
:hint (dm/fmt "unknown reference '%'" bucket))))
|
||||
|
||||
(defn process-chunk!
|
||||
[{:keys [::db/conn]} chunk]
|
||||
(reduce-kv (fn [[nfo ndo] bucket ids]
|
||||
(let [[nfo' ndo'] (process-bucket! conn bucket ids)]
|
||||
[(+ nfo nfo')
|
||||
(+ ndo ndo')]))
|
||||
[0 0]
|
||||
(d/group-by lookup-bucket :id #{} chunk)))
|
||||
|
||||
(def ^:private
|
||||
sql:get-touched-storage-objects
|
||||
@@ -167,29 +198,22 @@
|
||||
WHERE so.touched_at IS NOT NULL
|
||||
ORDER BY touched_at ASC
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
SKIP LOCKED
|
||||
LIMIT 10")
|
||||
|
||||
(defn- group-by-bucket
|
||||
[row]
|
||||
(d/group-by lookup-bucket :id #{} row))
|
||||
|
||||
(defn- get-buckets
|
||||
(defn get-chunk
|
||||
[conn]
|
||||
(sequence
|
||||
(comp (map impl/decode-row)
|
||||
(partition-all 25)
|
||||
(mapcat group-by-bucket))
|
||||
(db/cursor conn sql:get-touched-storage-objects)))
|
||||
(->> (db/exec! conn [sql:get-touched-storage-objects])
|
||||
(map impl/decode-row)
|
||||
(not-empty)))
|
||||
|
||||
(defn- process-touched!
|
||||
[{:keys [::db/conn]}]
|
||||
(loop [buckets (get-buckets conn)
|
||||
freezed 0
|
||||
[{:keys [::db/pool] :as cfg}]
|
||||
(loop [freezed 0
|
||||
deleted 0]
|
||||
(if-let [[bucket ids] (first buckets)]
|
||||
(let [[nfo ndo] (process-bucket! conn bucket ids)]
|
||||
(recur (rest buckets)
|
||||
(+ freezed nfo)
|
||||
(if-let [chunk (get-chunk pool)]
|
||||
(let [[nfo ndo] (db/tx-run! cfg process-chunk! chunk)]
|
||||
(recur (+ freezed nfo)
|
||||
(+ deleted ndo)))
|
||||
(do
|
||||
(l/inf :hint "task finished"
|
||||
@@ -198,11 +222,14 @@
|
||||
|
||||
{:freeze freezed :delete deleted}))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HANDLER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [_]
|
||||
(db/tx-run! cfg process-touched!)))
|
||||
(fn [_] (process-touched! cfg)))
|
||||
|
||||
|
||||
@@ -207,15 +207,13 @@
|
||||
(str "blake2b:" result)))
|
||||
|
||||
(defn resolve-backend
|
||||
[{:keys [::db/pool] :as storage} backend-id]
|
||||
[storage backend-id]
|
||||
(let [backend (get-in storage [::sto/backends backend-id])]
|
||||
(when-not backend
|
||||
(ex/raise :type :internal
|
||||
:code :backend-not-configured
|
||||
:hint (dm/fmt "backend '%' not configured" backend-id)))
|
||||
(-> backend
|
||||
(assoc ::sto/id backend-id)
|
||||
(assoc ::db/pool pool))))
|
||||
(assoc backend ::sto/id backend-id)))
|
||||
|
||||
(defrecord StorageObject [id size created-at expired-at touched-at backend])
|
||||
|
||||
|
||||
@@ -21,78 +21,31 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.media :as media]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare ^:private clean-file!)
|
||||
(declare ^:private get-file)
|
||||
(declare ^:private decode-file)
|
||||
(declare ^:private persist-file!)
|
||||
|
||||
(defn- decode-file
|
||||
[cfg {:keys [id] :as file}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(-> file
|
||||
(update :features db/decode-pgarray #{})
|
||||
(update :data blob/decode)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(update :data assoc :id id)
|
||||
(fmg/migrate-file))))
|
||||
|
||||
(defn- update-file!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [file (feat.fdata/enable-pointer-map file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
file))
|
||||
file)
|
||||
|
||||
file (-> file
|
||||
(update :features db/encode-pgarray conn "text")
|
||||
(update :data blob/encode))]
|
||||
|
||||
(db/update! conn :file
|
||||
{:has-media-trimmed true
|
||||
:features (:features file)
|
||||
:version (:version file)
|
||||
:data (:data file)}
|
||||
{:id id}
|
||||
{::db/return-keys true})))
|
||||
|
||||
(def ^:private
|
||||
sql:get-candidates
|
||||
"SELECT f.id,
|
||||
(def ^:private sql:get-snapshots
|
||||
"SELECT f.file_id AS id,
|
||||
f.data,
|
||||
f.revn,
|
||||
f.version,
|
||||
f.features,
|
||||
f.modified_at
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < now() - ?::interval
|
||||
AND f.deleted_at IS NULL
|
||||
ORDER BY f.modified_at DESC
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- get-candidates
|
||||
[{:keys [::db/conn ::min-age ::file-id]}]
|
||||
(if (uuid? file-id)
|
||||
(do
|
||||
(l/warn :hint "explicit file id passed on params" :file-id (str file-id))
|
||||
(db/query conn :file {:id file-id}))
|
||||
|
||||
(let [min-age (db/interval min-age)]
|
||||
(db/cursor conn [sql:get-candidates min-age] {:chunk-size 1}))))
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file_change AS f
|
||||
WHERE f.file_id = ?
|
||||
AND f.label IS NOT NULL
|
||||
ORDER BY f.created_at ASC")
|
||||
|
||||
(def ^:private sql:mark-file-media-object-deleted
|
||||
"UPDATE file_media_object
|
||||
@@ -100,10 +53,17 @@
|
||||
WHERE file_id = ? AND id != ALL(?::uuid[])
|
||||
RETURNING id")
|
||||
|
||||
(def ^:private xf:collect-used-media
|
||||
(comp (map :data) (mapcat bfc/collect-used-media)))
|
||||
|
||||
(defn- clean-file-media!
|
||||
"Performs the garbage collection of file media objects."
|
||||
[{:keys [::db/conn]} {:keys [id data] :as file}]
|
||||
(let [used (bfc/collect-used-media data)
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [used (into #{}
|
||||
xf:collect-used-media
|
||||
(cons file
|
||||
(->> (db/cursor conn [sql:get-snapshots id])
|
||||
(map (partial decode-file cfg)))))
|
||||
ids (db/create-array conn "uuid" used)
|
||||
unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids])
|
||||
(into #{} (map :id)))]
|
||||
@@ -172,9 +132,14 @@
|
||||
|
||||
file))
|
||||
|
||||
|
||||
(def ^:private sql:get-files-for-library
|
||||
"SELECT f.id, f.data, f.modified_at, f.features, f.version
|
||||
"SELECT f.id,
|
||||
f.data,
|
||||
f.modified_at,
|
||||
f.features,
|
||||
f.version,
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file AS f
|
||||
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
|
||||
WHERE fl.library_file_id = ?
|
||||
@@ -230,11 +195,6 @@
|
||||
(l/dbg :hint "clean" :rel "components" :file-id (str file-id) :total (count unused))
|
||||
file))
|
||||
|
||||
(def ^:private sql:get-changes
|
||||
"SELECT id, data FROM file_change
|
||||
WHERE file_id = ? AND data IS NOT NULL
|
||||
ORDER BY created_at ASC")
|
||||
|
||||
(def ^:private sql:mark-deleted-data-fragments
|
||||
"UPDATE file_data_fragment
|
||||
SET deleted_at = now()
|
||||
@@ -250,8 +210,7 @@
|
||||
|
||||
(defn- clean-data-fragments!
|
||||
[{:keys [::db/conn]} {:keys [id] :as file}]
|
||||
(let [used (into #{} xf:collect-pointers
|
||||
(cons file (db/cursor conn [sql:get-changes id])))
|
||||
(let [used (into #{} xf:collect-pointers [file])
|
||||
|
||||
unused (let [ids (db/create-array conn "uuid" used)]
|
||||
(->> (db/exec! conn [sql:mark-deleted-data-fragments id ids])
|
||||
@@ -274,17 +233,83 @@
|
||||
(cfv/validate-file-schema! file)
|
||||
file))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.id,
|
||||
f.data,
|
||||
f.revn,
|
||||
f.version,
|
||||
f.features,
|
||||
f.modified_at,
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < now() - ?::interval
|
||||
AND f.deleted_at IS NULL
|
||||
AND f.id = ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- get-file
|
||||
[{:keys [::db/conn ::min-age ::file-id]}]
|
||||
(->> (db/exec! conn [sql:get-file min-age file-id])
|
||||
(first)))
|
||||
|
||||
(defn- decode-file
|
||||
[cfg {:keys [id] :as file}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(-> (feat.fdata/resolve-file-data cfg file)
|
||||
(update :features db/decode-pgarray #{})
|
||||
(update :data blob/decode)
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(update :data assoc :id id)
|
||||
(fmg/migrate-file))))
|
||||
|
||||
(defn- persist-file!
|
||||
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [id] :as file}]
|
||||
(let [file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [file (feat.fdata/enable-pointer-map file)]
|
||||
(feat.fdata/persist-pointers! cfg id)
|
||||
file))
|
||||
file)
|
||||
|
||||
file (-> file
|
||||
(update :features db/encode-pgarray conn "text")
|
||||
(update :data blob/encode))]
|
||||
|
||||
;; If file was already offloaded, we touch the underlying storage
|
||||
;; object for properly trigger storage-gc-touched task
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(some->> (:data-ref-id file) (sto/touch-object! storage)))
|
||||
|
||||
(db/update! conn :file
|
||||
{:has-media-trimmed true
|
||||
:features (:features file)
|
||||
:version (:version file)
|
||||
:data (:data file)
|
||||
:data-backend nil
|
||||
:data-ref-id nil}
|
||||
{:id id}
|
||||
{::db/return-keys true})))
|
||||
|
||||
(defn- process-file!
|
||||
[cfg file]
|
||||
(try
|
||||
[cfg]
|
||||
(if-let [file (get-file cfg)]
|
||||
(let [file (decode-file cfg file)
|
||||
file (clean-media! cfg file)
|
||||
file (update-file! cfg file)]
|
||||
(clean-data-fragments! cfg file))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "error on cleaning file (skiping)"
|
||||
:file-id (str (:id file))
|
||||
:cause cause))))
|
||||
file (persist-file! cfg file)]
|
||||
(clean-data-fragments! cfg file)
|
||||
true)
|
||||
|
||||
(do
|
||||
(l/dbg :hint "skip" :file-id (str (::file-id cfg)))
|
||||
false)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HANDLER
|
||||
@@ -293,33 +318,29 @@
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool ::sto/storage]))
|
||||
|
||||
(defmethod ig/prep-key ::handler
|
||||
[_ cfg]
|
||||
(assoc cfg ::min-age cf/deletion-delay))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [min-age (dt/duration (or (:min-age props) (::min-age cfg)))
|
||||
cfg (-> cfg
|
||||
(update ::sto/storage media/configure-assets-storage conn)
|
||||
(assoc ::file-id (:file-id props))
|
||||
(assoc ::min-age min-age))
|
||||
(let [min-age (dt/duration (or (:min-age props)
|
||||
(cf/get-deletion-delay)))
|
||||
cfg (-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::file-id (:file-id props))
|
||||
(assoc ::min-age (db/interval min-age)))]
|
||||
|
||||
total (reduce (fn [total file]
|
||||
(process-file! cfg file)
|
||||
(inc total))
|
||||
0
|
||||
(get-candidates cfg))]
|
||||
(try
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [cfg (update cfg ::sto/storage sto/configure conn)
|
||||
processed? (process-file! cfg)]
|
||||
(when (and processed? (contains? cf/flags :tiered-file-data-storage))
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :offload-file-data)
|
||||
(assoc ::wrk/params props)
|
||||
(assoc ::wrk/priority 10)
|
||||
(assoc ::wrk/delay 1000))))
|
||||
processed?)))
|
||||
|
||||
(l/inf :hint "task finished"
|
||||
:min-age (dt/format-duration min-age)
|
||||
:processed total)
|
||||
|
||||
;; Allow optional rollback passed by params
|
||||
(when (:rollback? props)
|
||||
(db/rollback! conn))
|
||||
|
||||
{:processed total})))))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "error on cleaning file"
|
||||
:file-id (str (:file-id props))
|
||||
:cause cause))))))
|
||||
|
||||
64
backend/src/app/tasks/file_gc_scheduler.clj
Normal file
64
backend/src/app/tasks/file_gc_scheduler.clj
Normal file
@@ -0,0 +1,64 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.tasks.file-gc-scheduler
|
||||
"A maintenance task that is responsible of properly scheduling the
|
||||
file-gc task for all files that matches the eligibility threshold."
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private
|
||||
sql:get-candidates
|
||||
"SELECT f.id,
|
||||
f.modified_at
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < now() - ?::interval
|
||||
AND f.deleted_at IS NULL
|
||||
ORDER BY f.modified_at DESC
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- get-candidates
|
||||
[{:keys [::db/conn ::min-age] :as cfg}]
|
||||
(let [min-age (db/interval min-age)]
|
||||
(db/cursor conn [sql:get-candidates min-age] {:chunk-size 10})))
|
||||
|
||||
(defn- schedule!
|
||||
[{:keys [::min-age] :as cfg}]
|
||||
(let [total (reduce (fn [total {:keys [id]}]
|
||||
(let [params {:file-id id :min-age min-age}]
|
||||
(wrk/submit! (assoc cfg ::wrk/params params))
|
||||
(inc total)))
|
||||
0
|
||||
(get-candidates cfg))]
|
||||
|
||||
{:processed total}))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
|
||||
(defmethod ig/prep-key ::handler
|
||||
[_ cfg]
|
||||
(assoc cfg ::min-age (cf/get-deletion-delay)))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (dt/duration (or (:min-age props) (::min-age cfg)))]
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::min-age min-age)
|
||||
(assoc ::wrk/task :file-gc)
|
||||
(assoc ::wrk/priority 10)
|
||||
(assoc ::wrk/mark-retries 0)
|
||||
(assoc ::wrk/delay 1000)
|
||||
(db/tx-run! schedule!)))))
|
||||
@@ -10,35 +10,59 @@
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def ^:private
|
||||
sql:delete-files-xlog
|
||||
"delete from file_change
|
||||
where created_at < now() - ?::interval
|
||||
and label is NULL")
|
||||
"DELETE FROM file_change
|
||||
WHERE id IN (SELECT id FROM file_change
|
||||
WHERE label IS NULL
|
||||
AND created_at < ?
|
||||
ORDER BY created_at LIMIT ?)
|
||||
RETURNING id, data_backend, data_ref_id")
|
||||
|
||||
(def xf:filter-offloded
|
||||
(comp
|
||||
(filter feat.fdata/offloaded?)
|
||||
(keep :data-ref-id)))
|
||||
|
||||
(defn- delete-in-chunks
|
||||
[{:keys [::chunk-size ::threshold] :as cfg}]
|
||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
||||
(loop [total 0]
|
||||
(let [chunk (db/exec! cfg [sql:delete-files-xlog threshold chunk-size])
|
||||
length (count chunk)]
|
||||
|
||||
;; touch all references on offloaded changes entries
|
||||
(doseq [data-ref-id (sequence xf:filter-offloded chunk)]
|
||||
(l/trc :hint "touching referenced storage object"
|
||||
:storage-object-id (str data-ref-id))
|
||||
(sto/touch-object! storage data-ref-id))
|
||||
|
||||
(if (pos? length)
|
||||
(recur (+ total length))
|
||||
total)))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool]))
|
||||
|
||||
(defmethod ig/prep-key ::handler
|
||||
[_ cfg]
|
||||
(assoc cfg ::min-age (dt/duration {:hours 72})))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [::db/pool] :as cfg}]
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (or (:min-age props) (::min-age cfg))]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [interval (db/interval min-age)
|
||||
result (db/exec-one! conn [sql:delete-files-xlog interval])
|
||||
result (db/get-update-count result)]
|
||||
(let [min-age (or (:min-age props)
|
||||
(dt/duration "72h"))
|
||||
chunk-size (:chunk-size props 5000)
|
||||
threshold (dt/minus (dt/now) min-age)]
|
||||
|
||||
(l/info :hint "task finished" :min-age (dt/format-duration min-age) :total result)
|
||||
|
||||
(when (:rollback? props)
|
||||
(db/rollback! conn))
|
||||
|
||||
result)))))
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback props false))
|
||||
(assoc ::threshold threshold)
|
||||
(assoc ::chunk-size chunk-size)
|
||||
(db/tx-run! (fn [cfg]
|
||||
(let [total (delete-in-chunks cfg)]
|
||||
(l/trc :hint "file xlog cleaned" :total total)
|
||||
total)))))))
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.media :as media]
|
||||
[app.storage :as sto]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -126,7 +125,7 @@
|
||||
0)))
|
||||
|
||||
(def ^:private sql:get-files
|
||||
"SELECT id, deleted_at, project_id
|
||||
"SELECT id, deleted_at, project_id, data_backend, data_ref_id
|
||||
FROM file
|
||||
WHERE deleted_at IS NOT NULL
|
||||
AND deleted_at < now() - ?::interval
|
||||
@@ -136,15 +135,18 @@
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-files!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
|
||||
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-files min-age chunk-size] {:chunk-size 1})
|
||||
(reduce (fn [total {:keys [id deleted-at project-id]}]
|
||||
(reduce (fn [total {:keys [id deleted-at project-id] :as file}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file"
|
||||
:id (str id)
|
||||
:project-id (str project-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
|
||||
(when (= "objects-storage" (:data-backend file))
|
||||
(sto/touch-object! storage (:data-ref-id file)))
|
||||
|
||||
;; And finally, permanently delete the file.
|
||||
(db/delete! conn :file {:id id})
|
||||
|
||||
@@ -210,7 +212,7 @@
|
||||
0)))
|
||||
|
||||
(def ^:private sql:get-file-data-fragments
|
||||
"SELECT file_id, id, deleted_at
|
||||
"SELECT file_id, id, deleted_at, data_ref_id
|
||||
FROM file_data_fragment
|
||||
WHERE deleted_at IS NOT NULL
|
||||
AND deleted_at < now() - ?::interval
|
||||
@@ -220,15 +222,16 @@
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-file-data-fragments!
|
||||
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
|
||||
[{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}]
|
||||
(->> (db/cursor conn [sql:get-file-data-fragments min-age chunk-size] {:chunk-size 1})
|
||||
(reduce (fn [total {:keys [file-id id deleted-at]}]
|
||||
(reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-data-fragment"
|
||||
:id (str id)
|
||||
:file-id (str file-id)
|
||||
:deleted-at (dt/format-instant deleted-at))
|
||||
|
||||
(some->> data-ref-id (sto/touch-object! storage))
|
||||
(db/delete! conn :file-data-fragment {:file-id file-id :id id})
|
||||
|
||||
(inc total))
|
||||
@@ -292,16 +295,14 @@
|
||||
(defmethod ig/prep-key ::handler
|
||||
[_ cfg]
|
||||
(assoc cfg
|
||||
::min-age cf/deletion-delay
|
||||
::min-age (cf/get-deletion-delay)
|
||||
::chunk-size 10))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [min-age (dt/duration (or (:min-age props) (::min-age cfg)))
|
||||
cfg (-> cfg
|
||||
(assoc ::min-age (db/interval min-age))
|
||||
(update ::sto/storage media/configure-assets-storage))]
|
||||
cfg (assoc cfg ::min-age (db/interval min-age))]
|
||||
|
||||
(loop [procs (map deref deletion-proc-vars)
|
||||
total 0]
|
||||
|
||||
124
backend/src/app/tasks/offload_file_data.clj
Normal file
124
backend/src/app/tasks/offload_file_data.clj
Normal file
@@ -0,0 +1,124 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.tasks.offload-file-data
|
||||
"A maintenance task responsible of moving file data from hot
|
||||
storage (the database row) to a cold storage (fs or s3)."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.storage :as sto]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- offload-file-data!
|
||||
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
|
||||
(let [file (db/get conn :file {:id file-id}
|
||||
{::sql/for-update true})]
|
||||
(when (nil? (:data file))
|
||||
(ex/raise :hint "file already offloaded"
|
||||
:type :internal
|
||||
:code :file-already-offloaded
|
||||
:file-id file-id))
|
||||
|
||||
(let [data (sto/content (:data file))
|
||||
sobj (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/touch true
|
||||
:bucket "file-data"
|
||||
:content-type "application/octet-stream"
|
||||
:file-id file-id})]
|
||||
|
||||
(l/trc :hint "offload file data"
|
||||
:file-id (str file-id)
|
||||
:storage-id (str (:id sobj)))
|
||||
|
||||
(db/update! conn :file
|
||||
{:data-backend "objects-storage"
|
||||
:data-ref-id (:id sobj)
|
||||
:data nil}
|
||||
{:id file-id}
|
||||
{::db/return-keys false}))))
|
||||
|
||||
(defn- offload-file-data-fragments!
|
||||
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
|
||||
(doseq [fragment (db/query conn :file-data-fragment
|
||||
{:file-id file-id
|
||||
:deleted-at nil
|
||||
:data-backend nil}
|
||||
{::db/for-update true})]
|
||||
(let [data (sto/content (:data fragment))
|
||||
sobj (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/touch true
|
||||
:bucket "file-data-fragment"
|
||||
:content-type "application/octet-stream"
|
||||
:file-id file-id
|
||||
:file-fragment-id (:id fragment)})]
|
||||
|
||||
(l/trc :hint "offload file data fragment"
|
||||
:file-id (str file-id)
|
||||
:file-fragment-id (str (:id fragment))
|
||||
:storage-id (str (:id sobj)))
|
||||
|
||||
(db/update! conn :file-data-fragment
|
||||
{:data-backend "objects-storage"
|
||||
:data-ref-id (:id sobj)
|
||||
:data nil}
|
||||
{:id (:id fragment)}
|
||||
{::db/return-keys false}))))
|
||||
|
||||
(def sql:get-snapshots
|
||||
"SELECT fc.*
|
||||
FROM file_change AS fc
|
||||
WHERE fc.file_id = ?
|
||||
AND fc.label IS NOT NULL
|
||||
AND fc.data IS NOT NULL
|
||||
AND fc.data_backend IS NULL")
|
||||
|
||||
(defn- offload-file-snapshots!
|
||||
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
|
||||
(doseq [snapshot (db/exec! conn [sql:get-snapshots file-id])]
|
||||
(let [data (sto/content (:data snapshot))
|
||||
sobj (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/touch true
|
||||
:bucket "file-change"
|
||||
:content-type "application/octet-stream"
|
||||
:file-id file-id
|
||||
:file-change-id (:id snapshot)})]
|
||||
|
||||
(l/trc :hint "offload file change"
|
||||
:file-id (str file-id)
|
||||
:file-change-id (str (:id snapshot))
|
||||
:storage-id (str (:id sobj)))
|
||||
|
||||
(db/update! conn :file-change
|
||||
{:data-backend "objects-storage"
|
||||
:data-ref-id (:id sobj)
|
||||
:data nil}
|
||||
{:id (:id snapshot)}
|
||||
{::db/return-keys false}))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HANDLER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req [::db/pool ::sto/storage]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::file-id (:file-id props))
|
||||
(db/tx-run! (fn [cfg]
|
||||
(offload-file-data! cfg)
|
||||
(offload-file-data-fragments! cfg)
|
||||
(offload-file-snapshots! cfg))))))
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
(defmethod ig/prep-key ::handler
|
||||
[_ cfg]
|
||||
(assoc cfg ::min-age cf/deletion-delay))
|
||||
(assoc cfg ::min-age (cf/get-deletion-delay)))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [::db/pool ::min-age] :as cfg}]
|
||||
|
||||
@@ -62,19 +62,25 @@
|
||||
[conn]
|
||||
(-> (db/exec-one! conn ["SELECT count(*) AS count FROM file"]) :count))
|
||||
|
||||
(def ^:private sql:num-file-changes
|
||||
"SELECT count(*) AS count
|
||||
FROM file_change
|
||||
WHERE created_at < date_trunc('day', now()) + '24 hours'::interval
|
||||
AND created_at > date_trunc('day', now())")
|
||||
|
||||
(defn- get-num-file-changes
|
||||
[conn]
|
||||
(let [sql (str "SELECT count(*) AS count "
|
||||
" FROM file_change "
|
||||
" where date_trunc('day', created_at) = date_trunc('day', now())")]
|
||||
(-> (db/exec-one! conn [sql]) :count)))
|
||||
(-> (db/exec-one! conn [sql:num-file-changes]) :count))
|
||||
|
||||
(def ^:private sql:num-touched-files
|
||||
"SELECT count(distinct file_id) AS count
|
||||
FROM file_change
|
||||
WHERE created_at < date_trunc('day', now()) + '24 hours'::interval
|
||||
AND created_at > date_trunc('day', now())")
|
||||
|
||||
(defn- get-num-touched-files
|
||||
[conn]
|
||||
(let [sql (str "SELECT count(distinct file_id) AS count "
|
||||
" FROM file_change "
|
||||
" where date_trunc('day', created_at) = date_trunc('day', now())")]
|
||||
(-> (db/exec-one! conn [sql]) :count)))
|
||||
(-> (db/exec-one! conn [sql:num-touched-files]) :count))
|
||||
|
||||
(defn- get-num-users
|
||||
[conn]
|
||||
|
||||
39
backend/src/app/util/overrides.clj
Normal file
39
backend/src/app/util/overrides.clj
Normal file
@@ -0,0 +1,39 @@
|
||||
;; 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.util.overrides
|
||||
"A utility ns for declare default overrides over clojure runtime"
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as-alias oapi]
|
||||
[clojure.pprint :as pprint]
|
||||
[datoteka.fs :as fs]))
|
||||
|
||||
(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)
|
||||
|
||||
(sm/register! ::fs/path
|
||||
{:type ::fs/path
|
||||
:pred fs/path?
|
||||
:type-properties
|
||||
{:title "path"
|
||||
:description "filesystem path"
|
||||
:error/message "expected a valid fs path instance"
|
||||
:error/code "errors.invalid-path"
|
||||
:gen/gen (sg/generator :string)
|
||||
:decode/string fs/path
|
||||
::oapi/type "string"
|
||||
::oapi/format "unix-path"}})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user